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");
+ }
+}