Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.ConfigurationPropertiesValidationResult;
import org.apache.camel.tooling.model.MainModel;

/**
* The application.properties checks of {@link SourceValidator}: unknown camel.* options with the option meant, an
Expand All @@ -46,6 +48,56 @@ public static List<String> validateProperties(
return validatePropertiesLines(content, line -> validatePropertyLine(line, catalog, extraPropertyLine));
}

/** camel.<group>.<rest>=: the option groups of the main model (resilience4j, faulttolerance, threadpool...). */
static final Pattern GROUP_KEY_PATTERN = Pattern.compile("^\\s*camel\\.([a-zA-Z0-9-]+)\\.([^=\\s]+)\\s*=");

/** The camel.* prefixes whose keys legitimately nest, or that other checks own. */
private static final Set<String> NESTING_GROUPS = Set.of("component", "dataformat", "language", "beans", "variable",
"kamelet", "jbang", "route-template", "routeTemplate", "main", "rest", "server", "management");

/**
* camel.resilience4j.circuitbreaker.supplierCircuitBreaker.slidingWindowSize=4, an invented per-id form: the
* catalog accepts it and the run dies at startup ("Cannot find getter method: supplierCircuitBreaker on bean: class
* java.lang.String"). The options of a group are global, one segment after the group; resilience4j is also set per
* circuit breaker in the route (CAMEL-24856).
*/
static String nestedGroupKeyHint(String line, CamelCatalog catalog) {
Matcher m = GROUP_KEY_PATTERN.matcher(line);
if (!m.find()) {
return null;
}
String group = m.group(1);
String rest = m.group(2);
if (!rest.contains(".") || rest.contains("[") || NESTING_GROUPS.contains(group)) {
return null;
}
MainModel mm = catalog.mainModel();
if (mm == null) {
return null; // a catalog without the main model: the key goes to the catalog as is
}
String prefix = "camel." + group + ".";
List<String> options = new ArrayList<>();
for (var o : mm.getOptions()) {
if (o.getName().startsWith(prefix) && !o.getName().substring(prefix.length()).contains(".")) {
options.add(o.getName().substring(prefix.length()));
}
}
if (options.isEmpty()) {
return null; // not a known group: the catalog reports the key
}
String first = rest.substring(0, rest.indexOf('.'));
String last = rest.substring(rest.lastIndexOf('.') + 1);
String option = options.contains(last) ? last : closestName(last, options);
String example = "camel." + group + "." + (option != null ? option : options.get(0)) + "=...";
String more = "resilience4j".equals(group)
? ", or per circuit breaker in the route: circuitBreaker: {resilience4jConfiguration: {"
+ (option != null ? option : "...") + ": ...}}"
: "; the options are " + String.join(", ", options.size() > 8 ? options.subList(0, 8) : options)
+ (options.size() > 8 ? ", ..." : "");
return first + " Unknown option (camel." + group + " has no nested settings such as " + first
+ ": its options are global, " + example + more + ")";
}

/** Validates one properties line: a {@code camel.*} key against the catalog, any other with the extra check. */
static final Pattern COMPONENT_KEY_PATTERN
= Pattern.compile("^\\s*camel\\.(component|dataformat|language)\\.([A-Za-z0-9-]+)\\.");
Expand Down Expand Up @@ -76,6 +128,11 @@ public static String validatePropertyLine(String line, CamelCatalog catalog, Fun
return name + " Unknown " + kind + (closest != null ? " (did you mean " + closest + "?)" : "");
}
}
// camel.resilience4j.circuitbreaker.<id>.<option>: a nested segment under a known option group (CAMEL-24856)
String nested = nestedGroupKeyHint(line, catalog);
if (nested != null) {
return nested;
}
try {
ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(line);
if (result.isAccepted()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ void unknownComponentInAKeyIsReportedWithTheClosestName() {
assertThat(msgs.get(1)).startsWith("Line 3: jacksn Unknown dataformat").contains("did you mean jackson");
}

/**
* CAMEL-24856: a nested segment under an option group is not a property; the options are global or in the route.
*/
@Test
void aNestedKeyUnderAnOptionGroupIsReported() {
List<String> msgs = SourceValidator.validateProperties("""
camel.resilience4j.circuitbreaker.supplierCircuitBreaker.slidingWindowSize=4
camel.resilience4j.slidingWindowSize=4
camel.faulttolerance.bulkhead.myPool.enabled=true
""", catalog, null);
assertThat(msgs).hasSize(2);
assertThat(msgs.get(0))
.startsWith("Line 1: circuitbreaker Unknown option (camel.resilience4j has no nested settings")
.contains("camel.resilience4j.slidingWindowSize=...")
.contains("circuitBreaker: {resilience4jConfiguration: {slidingWindowSize: ...}}");
assertThat(msgs.get(1)).startsWith("Line 3: bulkhead Unknown option (camel.faulttolerance has no nested settings")
.contains("the options are");
}

@Test
void wrongMainKeyGetsTheClosestOption() {
List<String> msgs = SourceValidator.validateProperties("""
Expand Down
Loading