This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel-spring-boot.git


The following commit(s) were added to refs/heads/main by this push:
     new 9740f89a743 CAMEL-24260: Add Spring Boot configuration metadata dev 
console (#1864)
9740f89a743 is described below

commit 9740f89a7432b853e88c9c2b0cbb68b7246dc7c9
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Jul 27 10:53:02 2026 +0200

    CAMEL-24260: Add Spring Boot configuration metadata dev console (#1864)
    
    Add a dev console that scans all META-INF/spring-configuration-metadata.json
    resources on the classpath, merges them, and exposes the combined property
    metadata as a JSON endpoint. This allows the TUI to provide quick docs for
    Spring Boot properties (server.port, spring.datasource.url, etc.) alongside
    the existing Camel catalog metadata.
    
    The scan is lazy (first call) and cached. Supports filter option for
    pattern-based property name matching. Includes deprecated properties.
    
    Co-authored-by: Claude Opus 4.6 <[email protected]>
---
 .../console/CamelDevConsoleAutoConfiguration.java  |   6 +
 .../SpringBootConfigurationMetadataDevConsole.java | 222 +++++++++++++++++++++
 ...ingBootConfigurationMetadataDevConsoleTest.java | 117 +++++++++++
 3 files changed, 345 insertions(+)

diff --git 
a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/console/CamelDevConsoleAutoConfiguration.java
 
b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/console/CamelDevConsoleAutoConfiguration.java
index d5d918be5f8..21c6a8d6198 100644
--- 
a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/console/CamelDevConsoleAutoConfiguration.java
+++ 
b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/console/CamelDevConsoleAutoConfiguration.java
@@ -40,4 +40,10 @@ public class CamelDevConsoleAutoConfiguration {
         return new CamelDevConsoleEndpoint(camelContext);
     }
 
+    @Bean
+    @ConditionalOnMissingBean(SpringBootConfigurationMetadataDevConsole.class)
+    public SpringBootConfigurationMetadataDevConsole 
springBootConfigurationMetadataDevConsole() {
+        return new SpringBootConfigurationMetadataDevConsole();
+    }
+
 }
diff --git 
a/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/console/SpringBootConfigurationMetadataDevConsole.java
 
b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/console/SpringBootConfigurationMetadataDevConsole.java
new file mode 100644
index 00000000000..2dedc31c68c
--- /dev/null
+++ 
b/core/camel-spring-boot/src/main/java/org/apache/camel/spring/boot/actuate/console/SpringBootConfigurationMetadataDevConsole.java
@@ -0,0 +1,222 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.spring.boot.actuate.console;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import org.apache.camel.spi.Metadata;
+import org.apache.camel.support.PatternHelper;
+import org.apache.camel.support.console.AbstractDevConsole;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Dev console that scans all {@code 
META-INF/spring-configuration-metadata.json} resources on the classpath, merges
+ * them, and exposes the combined property metadata as JSON. This allows the 
TUI to provide quick docs for Spring Boot
+ * properties (e.g. {@code server.port}, {@code spring.datasource.url}) 
alongside the existing Camel catalog metadata.
+ */
+public class SpringBootConfigurationMetadataDevConsole extends 
AbstractDevConsole {
+
+    private static final Logger LOG = 
LoggerFactory.getLogger(SpringBootConfigurationMetadataDevConsole.class);
+
+    private static final String METADATA_RESOURCE = 
"META-INF/spring-configuration-metadata.json";
+
+    @Metadata(label = "query", description = "Filters the properties matching 
by name pattern", javaType = "java.lang.String")
+    public static final String FILTER = "filter";
+
+    private volatile List<JsonObject> cachedProperties;
+
+    public SpringBootConfigurationMetadataDevConsole() {
+        super("spring", "spring-boot-configuration", "Spring Boot 
Configuration",
+                "Displays Spring Boot configuration metadata from classpath");
+    }
+
+    @Override
+    protected void doStop() throws Exception {
+        super.doStop();
+        cachedProperties = null;
+    }
+
+    private List<JsonObject> getProperties() {
+        if (cachedProperties == null) {
+            cachedProperties = loadAndMergeMetadata();
+            LOG.debug("Loaded {} Spring Boot configuration properties from 
classpath", cachedProperties.size());
+        }
+        return cachedProperties;
+    }
+
+    @Override
+    protected String doCallText(Map<String, Object> options) {
+        String filter = optionString(options, FILTER);
+
+        StringBuilder sb = new StringBuilder();
+        sb.append("Spring Boot Configuration Properties:");
+        sb.append("\n");
+
+        List<JsonObject> properties = getProperties();
+        if (properties != null) {
+            for (JsonObject prop : properties) {
+                String name = prop.getString("name");
+                if (!accept(name, filter)) {
+                    continue;
+                }
+                String type = prop.getString("type");
+                String description = prop.getString("description");
+                Object defaultValue = prop.get("defaultValue");
+
+                sb.append(String.format("    %s", name));
+                if (type != null) {
+                    sb.append(String.format(" (%s)", type));
+                }
+                if (defaultValue != null) {
+                    sb.append(String.format(" = %s", defaultValue));
+                }
+                sb.append("\n");
+                if (description != null) {
+                    sb.append(String.format("        %s%n", description));
+                }
+            }
+        }
+
+        return sb.toString();
+    }
+
+    @Override
+    protected JsonObject doCallJson(Map<String, Object> options) {
+        String filter = optionString(options, FILTER);
+
+        JsonObject root = new JsonObject();
+        JsonArray arr = new JsonArray();
+
+        List<JsonObject> properties = getProperties();
+        if (properties != null) {
+            for (JsonObject prop : properties) {
+                String name = prop.getString("name");
+                if (!accept(name, filter)) {
+                    continue;
+                }
+                arr.add(prop);
+            }
+        }
+
+        root.put("properties", arr);
+        return root;
+    }
+
+    private List<JsonObject> loadAndMergeMetadata() {
+        Map<String, JsonObject> merged = new LinkedHashMap<>();
+
+        try {
+            ClassLoader cl = Thread.currentThread().getContextClassLoader();
+            if (cl == null) {
+                cl = getClass().getClassLoader();
+            }
+            Enumeration<URL> resources = cl.getResources(METADATA_RESOURCE);
+            while (resources.hasMoreElements()) {
+                URL url = resources.nextElement();
+                try {
+                    parseMetadataResource(url, merged);
+                } catch (Exception e) {
+                    LOG.debug("Failed to parse Spring Boot configuration 
metadata from: {}", url, e);
+                }
+            }
+        } catch (Exception e) {
+            LOG.debug("Failed to scan for Spring Boot configuration metadata 
resources", e);
+        }
+
+        return new ArrayList<>(merged.values());
+    }
+
+    @SuppressWarnings("unchecked")
+    private void parseMetadataResource(URL url, Map<String, JsonObject> 
merged) throws Exception {
+        String json;
+        try (InputStream is = url.openStream();
+             BufferedReader reader = new BufferedReader(new 
InputStreamReader(is, StandardCharsets.UTF_8))) {
+            json = reader.lines().collect(Collectors.joining("\n"));
+        }
+
+        Object parsed = Jsoner.deserialize(json);
+        if (!(parsed instanceof JsonObject root)) {
+            return;
+        }
+
+        Object propertiesObj = root.get("properties");
+        if (!(propertiesObj instanceof JsonArray propertiesArr)) {
+            return;
+        }
+
+        for (Object item : propertiesArr) {
+            if (!(item instanceof JsonObject property)) {
+                continue;
+            }
+            String name = property.getString("name");
+            if (name == null || name.isEmpty()) {
+                continue;
+            }
+
+            JsonObject entry = new JsonObject();
+            entry.put("name", name);
+            putIfNotNull(entry, "type", property.getString("type"));
+            putIfNotNull(entry, "description", 
property.getString("description"));
+            putIfNotNull(entry, "sourceType", 
property.getString("sourceType"));
+            if (property.containsKey("defaultValue")) {
+                entry.put("defaultValue", property.get("defaultValue"));
+            }
+            if (Boolean.TRUE.equals(property.get("deprecated"))) {
+                entry.put("deprecated", true);
+                Object deprecation = property.get("deprecation");
+                if (deprecation instanceof JsonObject deprecationObj) {
+                    JsonObject depInfo = new JsonObject();
+                    putIfNotNull(depInfo, "replacement", 
deprecationObj.getString("replacement"));
+                    putIfNotNull(depInfo, "since", 
deprecationObj.getString("since"));
+                    if (!depInfo.isEmpty()) {
+                        entry.put("deprecation", depInfo);
+                    }
+                }
+            }
+
+            // merge: last-wins for duplicate property names across JARs
+            merged.put(name, entry);
+        }
+    }
+
+    private static void putIfNotNull(JsonObject obj, String key, String value) 
{
+        if (value != null) {
+            obj.put(key, value);
+        }
+    }
+
+    private static boolean accept(String name, String filter) {
+        if (filter == null || filter.isBlank()) {
+            return true;
+        }
+        return PatternHelper.matchPattern(name, filter);
+    }
+}
diff --git 
a/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/actuate/console/SpringBootConfigurationMetadataDevConsoleTest.java
 
b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/actuate/console/SpringBootConfigurationMetadataDevConsoleTest.java
new file mode 100644
index 00000000000..1f01dfc1e95
--- /dev/null
+++ 
b/core/camel-spring-boot/src/test/java/org/apache/camel/spring/boot/actuate/console/SpringBootConfigurationMetadataDevConsoleTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.spring.boot.actuate.console;
+
+import java.util.Map;
+
+import org.apache.camel.console.DevConsole;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class SpringBootConfigurationMetadataDevConsoleTest {
+
+    private SpringBootConfigurationMetadataDevConsole console;
+
+    @BeforeEach
+    void setUp() {
+        console = new SpringBootConfigurationMetadataDevConsole();
+    }
+
+    @Test
+    void testConsoleMetadata() {
+        assertEquals("spring", console.getGroup());
+        assertEquals("spring-boot-configuration", console.getId());
+        assertEquals("Spring Boot Configuration", console.getDisplayName());
+    }
+
+    @Test
+    void testJsonReturnsProperties() {
+        JsonObject result = (JsonObject) 
console.call(DevConsole.MediaType.JSON, Map.of());
+
+        assertNotNull(result);
+        JsonArray properties = (JsonArray) result.get("properties");
+        assertNotNull(properties, "Should have a 'properties' array");
+        assertFalse(properties.isEmpty(), "Should find properties from 
classpath metadata");
+    }
+
+    @Test
+    void testJsonContainsCamelProperties() {
+        JsonObject result = (JsonObject) 
console.call(DevConsole.MediaType.JSON, Map.of());
+        JsonArray properties = (JsonArray) result.get("properties");
+
+        boolean found = false;
+        for (Object item : properties) {
+            JsonObject prop = (JsonObject) item;
+            String name = prop.getString("name");
+            if (name != null && name.startsWith("camel.")) {
+                found = true;
+                assertNotNull(prop.getString("type"), "Property should have a 
type");
+                break;
+            }
+        }
+        assertTrue(found, "Should find camel.* properties from this project's 
own metadata");
+    }
+
+    @Test
+    void testFilterOption() {
+        JsonObject all = (JsonObject) console.call(DevConsole.MediaType.JSON, 
Map.of());
+        JsonArray allProps = (JsonArray) all.get("properties");
+
+        JsonObject filtered = (JsonObject) 
console.call(DevConsole.MediaType.JSON,
+                Map.of("filter", "camel.main.*"));
+        JsonArray filteredProps = (JsonArray) filtered.get("properties");
+
+        assertFalse(filteredProps.isEmpty(), "Filter should return matching 
properties");
+        assertTrue(filteredProps.size() < allProps.size(),
+                "Filtered result should be smaller than unfiltered");
+
+        for (Object item : filteredProps) {
+            JsonObject prop = (JsonObject) item;
+            assertTrue(prop.getString("name").startsWith("camel.main."),
+                    "Filtered properties should match the pattern");
+        }
+    }
+
+    @Test
+    void testTextOutput() {
+        String text = (String) console.call(DevConsole.MediaType.TEXT, 
Map.of());
+
+        assertNotNull(text);
+        assertTrue(text.startsWith("Spring Boot Configuration Properties:"));
+        assertTrue(text.contains("camel."), "Text output should contain camel 
properties");
+    }
+
+    @Test
+    void testPropertyHasExpectedFields() {
+        JsonObject result = (JsonObject) 
console.call(DevConsole.MediaType.JSON,
+                Map.of("filter", "camel.main.name"));
+        JsonArray properties = (JsonArray) result.get("properties");
+
+        assertFalse(properties.isEmpty(), "Should find camel.main.name 
property");
+        JsonObject prop = (JsonObject) properties.get(0);
+        assertEquals("camel.main.name", prop.getString("name"));
+        assertNotNull(prop.getString("description"), "Should have 
description");
+        assertNotNull(prop.getString("type"), "Should have type");
+    }
+}

Reply via email to