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.git
The following commit(s) were added to refs/heads/main by this push:
new c114e65c33e4 CAMEL-25118: camel-catalog - Endpoint and configuration
validation: fix bugs found in a deep review (#27022)
c114e65c33e4 is described below
commit c114e65c33e4aea0a730d6846845c2ae77024a3e
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Sep 28 23:20:59 2026 +0200
CAMEL-25118: camel-catalog - Endpoint and configuration validation: fix
bugs found in a deep review (#27022)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../camel/catalog/CamelCatalogEdgeCasesTest.java | 244 ++++++++++++++++++++
.../org/apache/camel/catalog/CamelCatalogTest.java | 10 +-
.../ConfigurationPropertiesValidationResult.java | 10 +
.../camel/catalog/EndpointValidationResult.java | 10 +
.../camel/catalog/impl/AbstractCamelCatalog.java | 245 ++++++++++++---------
.../apache/camel/catalog/impl/CatalogHelper.java | 17 +-
6 files changed, 427 insertions(+), 109 deletions(-)
diff --git
a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogEdgeCasesTest.java
b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogEdgeCasesTest.java
new file mode 100644
index 000000000000..948be7dfac8f
--- /dev/null
+++
b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogEdgeCasesTest.java
@@ -0,0 +1,244 @@
+/*
+ * 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.catalog;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import org.junit.jupiter.api.BeforeAll;
+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;
+
+/**
+ * Edge cases of parsing, validating and building endpoint uris and
configuration properties.
+ */
+public class CamelCatalogEdgeCasesTest {
+
+ static DefaultCamelCatalog catalog;
+
+ @BeforeAll
+ public static void createCamelCatalog() {
+ catalog = new DefaultCamelCatalog();
+ }
+
+ @Test
+ public void testObjectOptionWithStringValue() {
+ EndpointValidationResult result =
catalog.validateEndpointProperties("file:inbox?scheduler=spring");
+ assertTrue(result.isSuccess(), result.summaryErrorMessage(false));
+
+ result =
catalog.validateEndpointProperties("google-storage:b?storageClass=STANDARD");
+ assertTrue(result.isSuccess(), result.summaryErrorMessage(false));
+
+ result =
catalog.validateEndpointProperties("jms:queue:foo?connectionFactory={{cf}}");
+ assertTrue(result.isSuccess(), result.summaryErrorMessage(false));
+
+ // a reference is still validated
+ result =
catalog.validateEndpointProperties("jms:queue:foo?connectionFactory=cf");
+ assertFalse(result.isSuccess());
+ assertNotNull(result.getInvalidReference());
+ }
+
+ @Test
+ public void testOptionalPathOptionInMiddle() throws Exception {
+ Map<String, String> map = catalog.endpointProperties("ftp://h:21");
+ assertEquals("h", map.get("host"));
+ assertEquals("21", map.get("port"));
+ assertFalse(map.containsKey("directoryName"));
+
+ map = catalog.endpointProperties("xmpp://h:5222");
+ assertEquals("h", map.get("host"));
+ assertEquals("5222", map.get("port"));
+ assertFalse(map.containsKey("participant"));
+
+ map = catalog.endpointProperties("ftp://h/dir");
+ assertEquals("h", map.get("host"));
+ assertEquals("dir", map.get("directoryName"));
+ assertFalse(map.containsKey("port"));
+
+ // separators that are the same cannot tell which option is omitted
+ EndpointValidationResult result =
catalog.validateEndpointProperties("spring-ws:http://foo.com/bar");
+ assertTrue(result.isSuccess(), result.summaryErrorMessage(false));
+
+ result = catalog.validateEndpointProperties("ftp://h:abc");
+ assertFalse(result.isSuccess());
+ assertEquals("abc", result.getInvalidInteger().get("port"));
+ }
+
+ @Test
+ public void testAsEndpointUriPlaceholderInPath() throws Exception {
+ Map<String, String> map = new LinkedHashMap<>();
+ map.put("host", "h");
+ map.put("directoryName", "{{dir}}");
+ assertEquals("ftp:h/{{dir}}", catalog.asEndpointUri("ftp", map,
false));
+
+ map.put("directoryName", "d/{{x}}");
+ assertEquals("ftp:h/d/{{x}}", catalog.asEndpointUri("ftp", map,
false));
+
+ map.put("directoryName", "a/{{env:X:def}}");
+ assertEquals("ftp:h/a/{{env:X:def}}", catalog.asEndpointUri("ftp",
map, false));
+
+ map.put("port", "21");
+ map.remove("directoryName");
+ assertEquals("ftp:h:21", catalog.asEndpointUri("ftp", map, false));
+ }
+
+ @Test
+ public void testEnvOrSysPlaceholderInPath() throws Exception {
+ EndpointValidationResult result =
catalog.validateEndpointProperties("kafka:{{env:TOPIC}}");
+ assertTrue(result.isSuccess(), result.summaryErrorMessage(false));
+
+ result = catalog.validateEndpointProperties("jms:queue:{{sys:Q}}");
+ assertTrue(result.isSuccess(), result.summaryErrorMessage(false));
+
+ result =
catalog.validateEndpointProperties("netty-http:http://foo.{{env:NS:default}}.svc/x");
+ assertTrue(result.isSuccess(), result.summaryErrorMessage(false));
+
+ Map<String, String> map =
catalog.endpointProperties("kafka:{{env:TOPIC}}");
+ assertEquals("{{env:TOPIC}}", map.get("topic"));
+
+ map =
catalog.endpointProperties("netty-http:http://foo.{{env:NS:default}}.svc:8080/x");
+ assertEquals("foo.{{env:NS:default}}.svc", map.get("host"));
+ assertEquals("8080", map.get("port"));
+ }
+
+ @Test
+ public void testRawValueWithAmpersand() throws Exception {
+ EndpointValidationResult result =
catalog.validateEndpointProperties("aws2-s3://b?secretKey=RAW(a&b)");
+ assertTrue(result.isSuccess(), result.summaryErrorMessage(false));
+
+ result =
catalog.validateEndpointProperties("ftp://h/d?password=RAW{a&b=c}&binary=true");
+ assertTrue(result.isSuccess(), result.summaryErrorMessage(false));
+
+ Map<String, String> map =
catalog.endpointProperties("ftp://h/d?password=RAW(a&b=c)&binary=true");
+ assertEquals("RAW(a&b=c)", map.get("password"));
+ assertEquals("true", map.get("binary"));
+ }
+
+ @Test
+ public void testRepeatedQueryParameter() throws Exception {
+ Map<String, String> map =
catalog.endpointProperties("http://h/p?x=a&x=b");
+ assertEquals("[a, b]", map.get("x"));
+
+ map = catalog.endpointProperties("http://h/p?x=a&x=b&x=c");
+ assertEquals("[a, b, c]", map.get("x"));
+ }
+
+ @Test
+ public void testOptionalPrefix() {
+ EndpointValidationResult result =
catalog.validateEndpointProperties("timer:foo?consumer.delay=1000");
+ assertFalse(result.isSuccess());
+ assertTrue(result.getUnknown().contains("consumer.delay"));
+
+ result =
catalog.validateEndpointProperties("file:inbox?consumer.exceptionHandler=#myHandler");
+ assertTrue(result.isSuccess(), result.summaryErrorMessage(false));
+ }
+
+ @Test
+ public void testLongOption() {
+ EndpointValidationResult result =
catalog.validateEndpointProperties("timer:foo?repeatCount=5000000000");
+ assertTrue(result.isSuccess(), result.summaryErrorMessage(false));
+
+ result =
catalog.validateEndpointProperties("timer:foo?repeatCount=5000000000000000000000");
+ assertFalse(result.isSuccess());
+ }
+
+ @Test
+ public void testInvalidDurationSummary() {
+ EndpointValidationResult result =
catalog.validateEndpointProperties("timer:foo?period=abc");
+ assertFalse(result.isSuccess());
+ String summary = result.summaryErrorMessage(false);
+ assertTrue(summary.contains("Invalid duration value: abc"), summary);
+ }
+
+ @Test
+ public void testConfigurationPropertySuggestions() {
+ ConfigurationPropertiesValidationResult result
+ =
catalog.validateConfigurationProperty("camel.component.kafka.brokerz=foo");
+ assertFalse(result.isSuccess());
+
assertTrue(result.getUnknown().contains("camel.component.kafka.brokerz"));
+ String[] suggestions =
result.getUnknownSuggestions().get("camel.component.kafka.brokerz");
+ assertNotNull(suggestions);
+ assertTrue(Arrays.asList(suggestions).contains("brokers"),
Arrays.toString(suggestions));
+ }
+
+ @Test
+ public void testConfigurationPropertyWithoutOption() {
+ ConfigurationPropertiesValidationResult result =
catalog.validateConfigurationProperty("camel.component.kafka=foo");
+ assertFalse(result.isSuccess());
+ assertTrue(result.getUnknown().contains("camel.component.kafka"));
+ }
+
+ @Test
+ public void testValidatePropertiesLenient() {
+ Map<String, String> map = new LinkedHashMap<>();
+ map.put("timerName", "foo");
+ map.put("lenient", "true");
+ map.put("myExtra", "123");
+ EndpointValidationResult result = catalog.validateProperties("timer",
map);
+ assertTrue(result.isSuccess(), result.summaryErrorMessage(false));
+
+ // http is lenient
+ map = new LinkedHashMap<>();
+ map.put("httpUri", "localhost:8080/foo");
+ map.put("myExtra", "123");
+ result = catalog.validateProperties("http", map);
+ assertTrue(result.isSuccess(), result.summaryErrorMessage(false));
+
+ result = catalog.validateProperties("unknownxyz", map);
+ assertEquals("unknownxyz", result.getUnknownComponent());
+ }
+
+ @Test
+ public void testLanguageNotOnClasspathWithOptions() {
+ LanguageValidationResult result =
catalog.validateLanguageExpression(null, "xquery?resultType=String", "/foo");
+ assertNotNull(result);
+ }
+
+ @Test
+ public void testSecretValueWithParenthesis() throws Exception {
+ Map<String, String> map = new LinkedHashMap<>();
+ map.put("bucketNameOrArn", "b");
+ map.put("secretKey", "a)&b");
+ String uri = catalog.asEndpointUri("aws2-s3", map, false);
+ assertEquals("aws2-s3://b?secretKey=RAW{a)&b}", uri);
+ assertEquals("RAW{a)&b}",
catalog.endpointProperties(uri).get("secretKey"));
+
+ map.put("secretKey", "a(b");
+ assertEquals("aws2-s3://b?secretKey=RAW(a(b)",
catalog.asEndpointUri("aws2-s3", map, false));
+ }
+
+ @Test
+ public void testUserInfo() throws Exception {
+ Map<String, String> map =
catalog.endpointProperties("ftp://u:p:ss@host/dir");
+ assertEquals("u", map.get("username"));
+ assertEquals("p:ss", map.get("password"));
+ assertEquals("host", map.get("host"));
+ assertEquals("dir", map.get("directoryName"));
+
+ map = catalog.endpointProperties("ftp://u:p%40ss@host:2121/dir");
+ assertEquals("u", map.get("username"));
+ assertEquals("p%40ss", map.get("password"));
+ assertEquals("host", map.get("host"));
+ assertEquals("2121", map.get("port"));
+ assertEquals("dir", map.get("directoryName"));
+ }
+}
diff --git
a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
index 684e34aedf5b..385601dd797d 100644
---
a/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
+++
b/catalog/camel-catalog/src/test/java/org/apache/camel/catalog/CamelCatalogTest.java
@@ -961,8 +961,12 @@ public class CamelCatalogTest {
result =
catalog.validateEndpointProperties("timer://foo?fixedRate=#fixed&delay=#myDelay");
assertTrue(result.isSuccess());
- // optional consumer. prefix
+ // consumer. prefix is only optional for a few options
result =
catalog.validateEndpointProperties("file:inbox?consumer.delay=5000&consumer.greedy=true");
+ assertFalse(result.isSuccess());
+ result =
catalog.validateEndpointProperties("file:inbox?consumer.bridgeErrorHandler=true");
+ assertFalse(result.isSuccess());
+ result =
catalog.validateEndpointProperties("file:inbox?consumer.exceptionHandler=#myHandler");
assertTrue(result.isSuccess());
// optional without consumer. prefix
@@ -971,7 +975,7 @@ public class CamelCatalogTest {
// mixed optional without consumer. prefix
result =
catalog.validateEndpointProperties("file:inbox?delay=5000&consumer.greedy=true");
- assertTrue(result.isSuccess());
+ assertFalse(result.isSuccess());
// prefix
result =
catalog.validateEndpointProperties("file:inbox?delay=5000&scheduler.foo=123&scheduler.bar=456");
@@ -1017,7 +1021,7 @@ public class CamelCatalogTest {
// lenient on rss consumer only
result = catalog.validateEndpointProperties(
-
"rss:file:src/test/data/rss20.xml?splitEntries=true&sortEntries=true&consumer.delay=50&foo=bar",
false, true,
+
"rss:file:src/test/data/rss20.xml?splitEntries=true&sortEntries=true&delay=50&foo=bar",
false, true,
false);
assertTrue(result.isSuccess());
assertEquals("foo", result.getLenient().iterator().next());
diff --git
a/core/camel-api/src/main/java/org/apache/camel/catalog/ConfigurationPropertiesValidationResult.java
b/core/camel-api/src/main/java/org/apache/camel/catalog/ConfigurationPropertiesValidationResult.java
index e9ed7ccc7982..7a688d2bcf8f 100644
---
a/core/camel-api/src/main/java/org/apache/camel/catalog/ConfigurationPropertiesValidationResult.java
+++
b/core/camel-api/src/main/java/org/apache/camel/catalog/ConfigurationPropertiesValidationResult.java
@@ -196,6 +196,16 @@ public class ConfigurationPropertiesValidationResult
extends PropertiesValidatio
}
}
}
+ if (invalidDuration != null) {
+ for (Map.Entry<String, String> entry : invalidDuration.entrySet())
{
+ boolean empty = isEmpty(entry.getValue());
+ if (empty) {
+ options.put(entry.getKey(), "Empty duration value");
+ } else {
+ options.put(entry.getKey(), "Invalid duration value: " +
entry.getValue());
+ }
+ }
+ }
if (invalidMap != null) {
for (Map.Entry<String, String> entry : invalidMap.entrySet()) {
boolean empty = isEmpty(entry.getValue());
diff --git
a/core/camel-api/src/main/java/org/apache/camel/catalog/EndpointValidationResult.java
b/core/camel-api/src/main/java/org/apache/camel/catalog/EndpointValidationResult.java
index 055d15ef8e84..848850fbfeb1 100644
---
a/core/camel-api/src/main/java/org/apache/camel/catalog/EndpointValidationResult.java
+++
b/core/camel-api/src/main/java/org/apache/camel/catalog/EndpointValidationResult.java
@@ -242,6 +242,16 @@ public class EndpointValidationResult extends
PropertiesValidationResult impleme
}
}
}
+ if (invalidDuration != null) {
+ for (Map.Entry<String, String> entry : invalidDuration.entrySet())
{
+ boolean empty = isEmpty(entry.getValue());
+ if (empty) {
+ options.put(entry.getKey(), "Empty duration value");
+ } else {
+ options.put(entry.getKey(), "Invalid duration value: " +
entry.getValue());
+ }
+ }
+ }
// build a table with the error summary nicely formatted
// lets use 24 as min length
diff --git
a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java
b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java
index b38d596d995e..5887612ab968 100644
---
a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java
+++
b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/AbstractCamelCatalog.java
@@ -74,8 +74,8 @@ import static org.apache.camel.util.StringHelper.isDashed;
public abstract class AbstractCamelCatalog {
private static final Pattern SYNTAX_PATTERN = Pattern.compile("([\\w.]+)");
- private static final Pattern ENV_OR_SYS_PATTERN =
Pattern.compile("\\{\\{(env|sys):\\w+\\}\\}");
- private static final Pattern SYNTAX_DASH_PATTERN =
Pattern.compile("([\\w.-]+)");
+ private static final Pattern ENV_OR_SYS_PATTERN =
Pattern.compile("\\{\\{(env|sys):\\w+(:[^{}]*)?\\}\\}");
+ private static final String ENV_OR_SYS_WORD = "CamelEnvOrSysPlaceholder";
private static final Pattern COMPONENT_SYNTAX_PARSER =
Pattern.compile("([^\\w-]*)([\\w-]+)");
private SuggestionStrategy suggestionStrategy = new
EditDistanceSuggestionStrategy();
@@ -213,17 +213,23 @@ public abstract class AbstractCamelCatalog {
}
public EndpointValidationResult validateProperties(String scheme,
Map<String, String> properties) {
- boolean lenient =
Boolean.getBoolean(properties.getOrDefault("lenient", "false"));
- return validateProperties(scheme, properties, lenient, false, false);
+ // the component may be lenient
+ ComponentModel model = componentModel(scheme);
+ boolean lenient =
Boolean.parseBoolean(properties.getOrDefault("lenient", "false"))
+ || model != null && model.isLenientProperties();
+ return validateProperties(scheme, model, properties, lenient, false,
false);
}
private EndpointValidationResult validateProperties(
- String scheme, Map<String, String> properties,
+ String scheme, ComponentModel model, Map<String, String>
properties,
boolean lenient, boolean consumerOnly,
boolean producerOnly) {
EndpointValidationResult result = new EndpointValidationResult(scheme);
- ComponentModel model = componentModel(scheme);
+ if (model == null) {
+ result.addUnknownComponent(scheme);
+ return result;
+ }
Map<String, BaseOptionModel> rows = new HashMap<>();
model.getComponentOptions().forEach(o -> rows.put(o.getName(), o));
// endpoint options have higher priority so overwrite component options
@@ -343,7 +349,11 @@ public abstract class AbstractCamelCatalog {
}
// is reference lookup of bean (not applicable for @UriPath,
enums, or multi-valued)
- if (!multiValue && enums == null &&
!"path".equals(row.getKind()) && "object".equals(row.getType())) {
+ // a placeholder, the default value, or any value for an
option of type Object is also valid
+ boolean defaultOrAnyValue = (defaultValue != null &&
value.equals(defaultValue.toString()))
+ || "java.lang.Object".equals(row.getJavaType());
+ if (!multiValue && !valuePlaceholder && !defaultOrAnyValue &&
enums == null && !"path".equals(row.getKind())
+ && "object".equals(row.getType())) {
// must start with # and be at least 2 characters
if (!value.startsWith("#") || value.length() <= 1) {
result.addInvalidReference(name, value);
@@ -370,8 +380,8 @@ public abstract class AbstractCamelCatalog {
// is integer
if (!multiValue && !valuePlaceholder && !lookup &&
"integer".equals(row.getType())) {
- // value must be an integer
- boolean valid = validateInteger(value);
+ // value must be an integer (or long)
+ boolean valid = validateInteger(value, row.getJavaType());
if (!valid) {
result.addInvalidInteger(name, value);
}
@@ -501,7 +511,7 @@ public abstract class AbstractCamelCatalog {
// only enable lenient properties if we should not ignore
lenient = !ignoreLenientProperties &&
model.isLenientProperties();
}
- return validateProperties(scheme, properties, lenient,
consumerOnly, producerOnly);
+ return validateProperties(scheme, model, properties, lenient,
consumerOnly, producerOnly);
} catch (URISyntaxException e) {
EndpointValidationResult result = new
EndpointValidationResult(uri);
result.addSyntaxError(e.getMessage());
@@ -538,17 +548,18 @@ public abstract class AbstractCamelCatalog {
String[] names = fields.split(":");
// grab authority part and grab username and/or password
- String authority = u.getAuthority();
+ String authority = u.getRawAuthority();
if (authority != null && authority.contains("@")) {
String username;
String password;
- // grab unserinfo part before @
- String userInfo = authority.substring(0,
authority.indexOf('@'));
- String[] parts = userInfo.split(":");
- if (parts.length == 2) {
- username = parts[0];
- password = parts[1];
+ // grab userinfo part before the last @ (the password may
contain @)
+ String userInfo = authority.substring(0,
authority.lastIndexOf('@'));
+ int pos = userInfo.indexOf(':');
+ if (pos != -1) {
+ // the password may contain colon
+ username = userInfo.substring(0, pos);
+ password = userInfo.substring(pos + 1);
} else {
// only username
username = userInfo;
@@ -572,13 +583,26 @@ public abstract class AbstractCamelCatalog {
uri = StringHelper.after(uri, ":");
String uriPath = URISupport.stripQuery(uri);
- // the uri path may use {{env:xxx}} or {{sys:xxx}} placeholders so
ignore those
+ // the uri path may use {{env:xxx}} or {{sys:xxx}} placeholders (with
optional default value), which contains
+ // colon that would be parsed as a separator, so replace those with
words and restore the placeholders afterward
Matcher matcher = ENV_OR_SYS_PATTERN.matcher(uriPath);
- uriPath = matcher.replaceAll("");
+ List<String> envOrSys = new ArrayList<>();
+ StringBuilder sbPath = new StringBuilder();
+ while (matcher.find()) {
+ matcher.appendReplacement(sbPath, ENV_OR_SYS_WORD +
envOrSys.size() + "x");
+ envOrSys.add(matcher.group());
+ }
+ matcher.appendTail(sbPath);
+ uriPath = sbPath.toString();
- // strip user info from uri path
+ // strip user info from uri path (up to the last @ in the authority as
the password may contain @)
if (!userInfoOptions.isEmpty()) {
- uriPath = StringHelper.after(uriPath, "@", uriPath);
+ int start = uriPath.startsWith("//") ? 2 : 0;
+ int end = uriPath.indexOf('/', start);
+ int at = end != -1 ? uriPath.lastIndexOf('@', end) :
uriPath.lastIndexOf('@');
+ if (at != -1) {
+ uriPath = uriPath.substring(at + 1);
+ }
}
// strip double slash in the start
@@ -596,7 +620,20 @@ public abstract class AbstractCamelCatalog {
}
}
// parse the syntax and find each token between each option
- final List<String> word2 = findTokens(syntax, scheme, uriPath);
+ // and the index of the syntax option each value belongs to (by the
separator found before the value)
+ final List<Integer> positions = new ArrayList<>();
+ final List<String> word2 = findTokens(syntax, scheme, uriPath,
positions);
+ if (!envOrSys.isEmpty()) {
+ word2.replaceAll(v -> {
+ for (int i = 0; i < envOrSys.size(); i++) {
+ v = v.replace(ENV_OR_SYS_WORD + i + "x", envOrSys.get(i));
+ }
+ return v;
+ });
+ }
+ // the positions can be used when at least one separator was found and
the syntax has no scheme name as option
+ boolean usePositions = positions.size() == word2.size() &&
positions.stream().anyMatch(p -> p > 0)
+ && positions.get(positions.size() - 1) < word.size() &&
SYNTAX_PATTERN.split(syntax).length <= word.size();
boolean defaultValueAdded = false;
@@ -637,6 +674,13 @@ public abstract class AbstractCamelCatalog {
if (allOptions) {
String value = it.next();
options.put(key, value);
+ } else if (usePositions) {
+ // optional options may be omitted in the middle of the syntax
(such as ftp:host:port/directoryName)
+ // so use the separators to know which option each value
belongs to
+ int pos = positions.indexOf(i);
+ if (pos != -1) {
+ options.put(key, word2.get(pos));
+ }
} else {
// we have a little problem as we do not not have all options
if (!option.isRequired()) {
@@ -721,13 +765,18 @@ public abstract class AbstractCamelCatalog {
return answer;
}
- private static List<String> findTokens(String syntax, String scheme,
String uriPath) {
+ private static List<String> findTokens(String syntax, String scheme,
String uriPath, List<Integer> positions) {
String[] tokens = SYNTAX_PATTERN.split(syntax);
// find the position where each option start/end
List<String> word2 = new ArrayList<>();
int prev = 0;
int prevPath = 0;
+ // the separator at index i in tokens is before the option at index i
in the syntax
+ int current = 0;
+ // the separators found and not found
+ List<String> found = new ArrayList<>();
+ List<String> skipped = new ArrayList<>();
// special for activemq/jms where the enum for destinationType causes
a token issue as it includes a colon
// for 'temp:queue' and 'temp:topic' values
@@ -737,7 +786,8 @@ public abstract class AbstractCamelCatalog {
}
}
- for (String token : tokens) {
+ for (int i = 0; i < tokens.length; i++) {
+ String token = tokens[i];
if (token.isEmpty()) {
continue;
}
@@ -757,14 +807,25 @@ public abstract class AbstractCamelCatalog {
if (idx > 0) {
String option = uriPath.substring(prev, idx);
word2.add(option);
+ positions.add(current);
+ current = i;
prev = idx + len;
prevPath = prev;
+ found.add(token);
+ } else {
+ skipped.add(token);
}
}
// special for last or if we did not add anyone
if (prev > 0 || word2.isEmpty()) {
String option = uriPath.substring(prev);
word2.add(option);
+ positions.add(current);
+ }
+ // if a similar separator was not found (such as :// vs :) then we
cannot know which option the values belong to
+ boolean ambiguous = skipped.stream().anyMatch(s ->
found.stream().anyMatch(f -> s.contains(f) || f.contains(s)));
+ if (ambiguous) {
+ positions.clear();
}
return word2;
}
@@ -1001,82 +1062,35 @@ public abstract class AbstractCamelCatalog {
sb.append(query);
}
} else {
- // TODO: revisit this and see if we can do this in another way
- // oh darn some options is missing, so we need a complex way of
building the uri
-
- // the tokens between the options in the path
- String[] tokens = SYNTAX_DASH_PATTERN.split(syntax);
-
- // parse the syntax into each options
- Matcher matcher = SYNTAX_PATTERN.matcher(originalSyntax);
- List<String> options = new ArrayList<>();
- while (matcher.find()) {
- String s = matcher.group(1);
- options.add(s);
- }
-
- // need to preserve {{ and }} from the syntax
- // (we need to use words only as its provisional placeholders)
- syntax = syntax.replace("{{", "BEGINCAMELPLACEHOLDER");
- syntax = syntax.replace("}}", "ENDCAMELPLACEHOLDER");
-
- // parse the syntax into each options
- Matcher matcher2 = SYNTAX_DASH_PATTERN.matcher(syntax);
- List<String> options2 = new ArrayList<>();
- while (matcher2.find()) {
- String s = matcher2.group(1);
- s = s.replace("BEGINCAMELPLACEHOLDER", "{{");
- s = s.replace("ENDCAMELPLACEHOLDER", "}}");
- options2.add(s);
- }
-
- // build the endpoint
- int range = 0;
+ // some options are missing, so build the uri from the syntax with
only the options that are provided
+ // (use the values as-is, as they may contain separators or {{ }}
placeholders)
boolean first = true;
- boolean hasQuestionmark = false;
- for (int i = 0; i < options.size(); i++) {
- String key = options.get(i);
- String key2 = options2.get(i);
- String token = null;
- if (tokens.length > i) {
- token = tokens[i];
- }
-
- boolean contains = properties.containsKey(key);
- if (!contains) {
- // if the key are similar we have no explicit value and
can try to find a default value if the option is required
+ Matcher matcher = COMPONENT_SYNTAX_PARSER.matcher(originalSyntax);
+ while (matcher.find()) {
+ String token = matcher.group(1);
+ String key = matcher.group(2);
+ String value = null;
+ if (properties.containsKey(key)) {
+ value = properties.get(key);
+ if (value == null) {
+ value = key;
+ }
+ } else {
+ // no explicit value so try to find a default value if the
option is required
BaseOptionModel row = rows.get(key);
- if (row != null && row.isRequired()) {
- Object value = row.getDefaultValue();
- if (!CatalogHelper.isEmpty(value)) {
- properties.put(key, key2 = value.toString());
- }
+ if (row != null && row.isRequired() &&
!CatalogHelper.isEmpty(row.getDefaultValue())) {
+ value = row.getDefaultValue().toString();
}
}
-
- // was the option provided?
- if (properties.containsKey(key)) {
- if (!first && token != null) {
+ if (value != null) {
+ if (!first) {
sb.append(token);
}
- hasQuestionmark |= key.contains("?") || (token != null &&
token.contains("?"));
- sb.append(key2);
+ sb.append(value);
first = false;
}
- range++;
- }
- // append any extra options that was in surplus for the last
- while (range < options2.size()) {
- String token = null;
- if (tokens.length > range) {
- token = tokens[range];
- }
- String key2 = options2.get(range);
- sb.append(token);
- sb.append(key2);
- hasQuestionmark |= key2.contains("?") || (token != null &&
token.contains("?"));
- range++;
}
+ boolean hasQuestionmark = sb.indexOf("?") != -1;
if (!copy.isEmpty()) {
// wrap secret values with RAW to avoid breaking URI encoding
in case of encoded values
@@ -1112,7 +1126,12 @@ public abstract class AbstractCamelCatalog {
return val;
}
- if (option.isSecret() && !val.startsWith("#") &&
!val.startsWith("RAW(")) {
+ if (option.isSecret() && !val.startsWith("#") &&
!val.startsWith("RAW(") && !val.startsWith("RAW{")) {
+ // use RAW{} when the value contains ) as that would end RAW()
(scanRaw ends at the first ")")
+ // note: a value with both ) and } cannot be safely wrapped in
either form, RAW() is used
+ if (val.indexOf(')') != -1 && val.indexOf('}') == -1) {
+ return "RAW{" + val + "}";
+ }
return "RAW(" + val + ")";
}
@@ -1185,6 +1204,11 @@ public abstract class AbstractCamelCatalog {
}
if (loader != null) {
int idx = key.indexOf('.');
+ if (idx == -1) {
+ // there is no option (such as camel.component.kafka=foo)
+ result.addUnknown(longKey);
+ return result;
+ }
String name = key.substring(0, idx);
String option = key.substring(idx + 1);
@@ -1268,9 +1292,15 @@ public abstract class AbstractCamelCatalog {
// unknown option
result.addUnknown(longKey);
if (suggestionStrategy != null) {
- String[] suggestions =
suggestionStrategy.suggestEndpointOptions(rows.keySet(), name);
+ // suggest for the name of the unknown option (the last part
of the key, or the full key when the
+ // options are named with the full key such as camel.main.xxx)
+ boolean fullKeys = rows.keySet().stream().anyMatch(k ->
k.indexOf('.') != -1);
+ String unknownOption = fullKeys
+ ? dashToCamelCase(longKey)
+ :
StringHelper.dashToCamelCase(longKey.substring(longKey.lastIndexOf('.') + 1));
+ String[] suggestions =
suggestionStrategy.suggestEndpointOptions(rows.keySet(), unknownOption);
if (suggestions != null) {
- result.addUnknownSuggestions(name, suggestions);
+ result.addUnknownSuggestions(longKey, suggestions);
}
}
} else {
@@ -1303,8 +1333,8 @@ public abstract class AbstractCamelCatalog {
// is integer
if (!optionPlaceholder && !lookup &&
"integer".equals(row.getType())) {
- // value must be an integer
- boolean valid = validateInteger(value);
+ // value must be an integer (or long)
+ boolean valid = validateInteger(value, row.getJavaType());
if (!valid) {
result.addInvalidInteger(longKey, value);
}
@@ -1718,8 +1748,8 @@ public abstract class AbstractCamelCatalog {
} catch (Exception e) {
// ignore
}
- // set options on the language
- if (options != null) {
+ // set options on the language (if the language is on the classpath)
+ if (options != null && clazz != null && instance != null) {
final Map<String, Object> fOptions = options;
final Object fInstance = instance;
ReflectionHelper.doWithFields(clazz, field -> {
@@ -1787,9 +1817,17 @@ public abstract class AbstractCamelCatalog {
}
private static boolean validateInteger(String value) {
+ return validateInteger(value, null);
+ }
+
+ private static boolean validateInteger(String value, String javaType) {
boolean valid = false;
try {
- Integer.parseInt(value);
+ if ("long".equals(javaType) || "java.lang.Long".equals(javaType)) {
+ Long.parseLong(value);
+ } else {
+ Integer.parseInt(value);
+ }
valid = true;
} catch (Exception e) {
// ignore
@@ -1826,11 +1864,10 @@ public abstract class AbstractCamelCatalog {
for (BaseOptionModel row : rows.values()) {
String optionalPrefix = row.getOptionalPrefix();
if (optionalPrefix != null && !optionalPrefix.isEmpty() &&
name.startsWith(optionalPrefix)) {
- // try again
- return stripOptionalPrefixFromName(rows,
name.substring(optionalPrefix.length()));
- } else {
- if (name.equalsIgnoreCase(row.getName())) {
- break;
+ // the optional prefix only applies to the option that uses
the prefix
+ String stripped = name.substring(optionalPrefix.length());
+ if (stripped.equalsIgnoreCase(row.getName())) {
+ return stripped;
}
}
}
diff --git
a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/CatalogHelper.java
b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/CatalogHelper.java
index db41d8eaa501..abdf623bd4a5 100644
---
a/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/CatalogHelper.java
+++
b/core/camel-core-catalog/src/main/java/org/apache/camel/catalog/impl/CatalogHelper.java
@@ -199,8 +199,8 @@ public final class CatalogHelper {
continue;
}
- // the & denote parameter is ended
- if (ch == '&') {
+ // the & denote parameter is ended (but a raw value may contain &
until the raw value is ended)
+ if (ch == '&' && !(isRaw && !isRawEnded(value))) {
// parameter is ended, as we hit & separator
String aKey = key.toString();
// the key may be a placeholder of options which we then do
not know what is
@@ -231,6 +231,18 @@ public final class CatalogHelper {
}
+ private static boolean isRawEnded(StringBuilder value) {
+ // RAW(...) ends with ) and RAW{...} ends with }
+ String s = value.toString();
+ for (int j = 0; j < URISupport.RAW_TOKEN_START.length; j++) {
+ String rawTokenStart = URISupport.RAW_TOKEN_PREFIX +
URISupport.RAW_TOKEN_START[j];
+ if (s.startsWith(rawTokenStart)) {
+ return s.length() > rawTokenStart.length() &&
s.charAt(s.length() - 1) == URISupport.RAW_TOKEN_END[j];
+ }
+ }
+ return true;
+ }
+
private static boolean isRaw(boolean isRaw, StringBuilder value) {
// are we a raw value
for (int j = 0; j < URISupport.RAW_TOKEN_START.length; j++) {
@@ -288,6 +300,7 @@ public final class CatalogHelper {
if (s != null) {
list.add(s);
}
+ list.add(newValue);
}
return list;