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;

Reply via email to