This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/camel.git
commit 1400650f37f6e11047fbeeaa15861b853952bdfa Author: Claus Ibsen <claus.ib...@gmail.com> AuthorDate: Fri Dec 20 19:41:03 2019 +0100 CAMEL-14311: Add validate configuration properties to camel-catalog. --- catalog/camel-catalog/pom.xml | 1 + .../org/apache/camel/catalog/CamelCatalog.java | 8 + .../catalog/CamelCatalogJSonSchemaResolver.java | 7 + .../org/apache/camel/catalog/CamelCatalogTest.java | 234 +++++++++++++++++++ .../ConfigurationPropertiesValidationResult.java | 41 ++++ .../runtimecatalog/EndpointValidationResult.java | 33 +++ .../camel/runtimecatalog/JSonSchemaResolver.java | 7 + .../camel/runtimecatalog/RuntimeCamelCatalog.java | 14 +- .../runtimecatalog/impl/AbstractCamelCatalog.java | 250 +++++++++++++++++++-- .../impl/CamelContextJSonSchemaResolver.java | 19 ++ .../impl/RuntimeCamelCatalogTest.java | 53 ++++- .../org/apache/camel/support/JSonSchemaHelper.java | 157 +++++++++++-- .../camel/support/PropertyBindingSupport.java | 2 +- 13 files changed, 782 insertions(+), 44 deletions(-) diff --git a/catalog/camel-catalog/pom.xml b/catalog/camel-catalog/pom.xml index 4c3e8a1..ba9a7ca 100644 --- a/catalog/camel-catalog/pom.xml +++ b/catalog/camel-catalog/pom.xml @@ -125,6 +125,7 @@ </directory> <!-- the following files are maintained in camel-api and not here, so they are copied over --> <includes> + <include>ConfigurationPropertiesValidationResult.java</include> <include>EndpointValidationResult.java</include> <include>JSonSchemaResolver.java</include> <include>LanguageValidationResult.java</include> diff --git a/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalog.java b/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalog.java index 3bcef3d..bbba0f8 100644 --- a/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalog.java +++ b/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalog.java @@ -474,6 +474,14 @@ public interface CamelCatalog { LanguageValidationResult validateLanguageExpression(ClassLoader classLoader, String language, String text); /** + * Parses and validates the configuration property + * + * @param text the configuration text + * @return validation result + */ + ConfigurationPropertiesValidationResult validateConfigurationProperty(String text); + + /** * Returns the component name from the given endpoint uri * * @param uri the endpoint uri diff --git a/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalogJSonSchemaResolver.java b/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalogJSonSchemaResolver.java index eb49604..a43c4e2 100644 --- a/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalogJSonSchemaResolver.java +++ b/catalog/camel-catalog/src/main/java/org/apache/camel/catalog/CamelCatalogJSonSchemaResolver.java @@ -105,6 +105,13 @@ public class CamelCatalogJSonSchemaResolver implements JSonSchemaResolver { } @Override + public String getMainJsonSchema() { + final String file = "META-INF/camel-main-configuration-metadata.json"; + + return loadResourceFromVersionManager(file); + } + + @Override public String getOtherJSonSchema(String name) { final String file = camelCatalog.getRuntimeProvider().getOtherJSonSchemaDirectory() + "/" + name + ".json"; 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 79864a6..ddc9c84 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 @@ -1301,4 +1301,238 @@ public class CamelCatalogTest { assertEquals("Kerberos Renew Jitter", row.get("displayName")); } + @Test + public void testValidateConfigurationPropertyComponent() throws Exception { + String text = "camel.component.seda.queueSize=1234"; + ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.seda.queue-size=1234"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.seda.queuesize=1234"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.seda.queueSize=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertEquals("abc", result.getInvalidInteger().get("camel.component.seda.queueSize")); + + text = "camel.component.seda.foo=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertTrue(result.getUnknown().contains("camel.component.seda.foo")); + + text = "camel.component.jms.acknowledgementModeName=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertEquals("abc", result.getInvalidEnum().get("camel.component.jms.acknowledgementModeName")); + List<String> list = result.getEnumChoices("camel.component.jms.acknowledgementModeName"); + assertEquals(4, list.size()); + assertEquals("SESSION_TRANSACTED", list.get(0)); + assertEquals("CLIENT_ACKNOWLEDGE", list.get(1)); + assertEquals("AUTO_ACKNOWLEDGE", list.get(2)); + assertEquals("DUPS_OK_ACKNOWLEDGE", list.get(3)); + } + + @Test + public void testValidateConfigurationPropertyLanguage() throws Exception { + String text = "camel.language.tokenize.token=;"; + ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.language.tokenize.regex=true"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.language.tokenize.regex=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertEquals("abc", result.getInvalidBoolean().get("camel.language.tokenize.regex")); + + text = "camel.language.tokenize.foo=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertTrue(result.getUnknown().contains("camel.language.tokenize.foo")); + } + + @Test + public void testValidateConfigurationPropertyDataformat() throws Exception { + String text = "camel.dataformat.bindy-csv.type=csv"; + ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.dataformat.bindy-csv.locale=us"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.dataformat.bindy-csv.allowEmptyStream=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertEquals("abc", result.getInvalidBoolean().get("camel.dataformat.bindy-csv.allowEmptyStream")); + + text = "camel.dataformat.bindy-csv.foo=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertTrue(result.getUnknown().contains("camel.dataformat.bindy-csv.foo")); + + text = "camel.dataformat.bindy-csv.type=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertEquals("abc", result.getInvalidEnum().get("camel.dataformat.bindy-csv.type")); + List<String> list = result.getEnumChoices("camel.dataformat.bindy-csv.type"); + assertEquals(3, list.size()); + assertEquals("Csv", list.get(0)); + assertEquals("Fixed", list.get(1)); + assertEquals("KeyValue", list.get(2)); + } + + @Test + public void testValidateConfigurationPropertyComponentQuartz() throws Exception { + String text = "camel.component.quartz.auto-start-scheduler=true"; + ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.quartz.properties=#myProp"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.quartz.properties=123"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + + text = "camel.component.quartz.properties.foo=123"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.quartz.properties.bar=true"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.quartz.properties[0]=yes"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.quartz.properties[1]=no"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.quartz.properties[foo]=abc"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.quartz.properties[foo].beer=yes"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.quartz.properties[foo].drink=no"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + } + + @Test + public void testValidateConfigurationPropertyComponentJClouds() throws Exception { + String text = "camel.component.jclouds.basicPropertyBinding=true"; + ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.jclouds.blobStores=#myStores"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.jclouds.blobStores=foo"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertTrue(result.getInvalidArray().containsKey("camel.component.jclouds.blobStores")); + + text = "camel.component.jclouds.blobStores[0]=foo"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.jclouds.blobStores[1]=bar"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.jclouds.blobStores[foo]=123"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertEquals("foo", result.getInvalidInteger().get("camel.component.jclouds.blobStores[foo]")); + + text = "camel.component.jclouds.blobStores[0].beer=yes"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.jclouds.blobStores[1].drink=no"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.jclouds.blobStores[foo].beer=yes"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertEquals("foo", result.getInvalidInteger().get("camel.component.jclouds.blobStores[foo].beer")); + } + + @Test + public void testValidateConfigurationPropertyMain() throws Exception { + String text = "camel.main.allow-use-original-message=true"; + ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.main.allow-use-original-message=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertEquals("abc", result.getInvalidBoolean().get("camel.main.allow-use-original-message")); + + text = "camel.main.foo=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertTrue(result.getUnknown().contains("camel.main.foo")); + + text = "camel.resilience4j.minimum-number-of-calls=123"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.resilience4j.minimum-number-of-calls=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertEquals("abc", result.getInvalidInteger().get("camel.resilience4j.minimum-number-of-calls")); + + text = "camel.resilience4j.slow-call-rate-threshold=12.5"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.resilience4j.slow-call-rate-threshold=12x5"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertEquals("12x5", result.getInvalidNumber().get("camel.resilience4j.slow-call-rate-threshold")); + + text = "camel.rest.api-properties=#foo"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.rest.api-properties=bar"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertEquals("bar", result.getInvalidMap().get("camel.rest.api-properties")); + + text = "camel.rest.api-properties.foo=abc"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.rest.api-properties.bar=123"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.rest.api-properties.beer=yes"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + // TODO: add support for [] maps for main +// text = "camel.rest.api-properties[drink]=no"; +// result = catalog.validateConfigurationProperty(text); +// assertTrue(result.isSuccess()); + } + } diff --git a/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/ConfigurationPropertiesValidationResult.java b/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/ConfigurationPropertiesValidationResult.java new file mode 100644 index 0000000..14047b1 --- /dev/null +++ b/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/ConfigurationPropertiesValidationResult.java @@ -0,0 +1,41 @@ +/* + * 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.runtimecatalog; + +/** + * Details result of validating configuration properties (eg application.properties for camel-main). + */ +public class ConfigurationPropertiesValidationResult extends EndpointValidationResult { + + // TODO: Move stuff to base class for EndpointValidationResult so they can share code + + private String key; + private String value; + + public ConfigurationPropertiesValidationResult(String key, String value) { + this.key = key; + this.value = value; + } + + public String getKey() { + return key; + } + + public String getValue() { + return value; + } +} diff --git a/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/EndpointValidationResult.java b/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/EndpointValidationResult.java index 091820a..6070f1d 100644 --- a/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/EndpointValidationResult.java +++ b/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/EndpointValidationResult.java @@ -51,6 +51,8 @@ public class EndpointValidationResult implements Serializable { private Map<String, String> invalidEnum; private Map<String, String[]> invalidEnumChoices; private Map<String, String[]> invalidEnumSuggestions; + private Map<String, String> invalidMap; + private Map<String, String> invalidArray; private Map<String, String> invalidReference; private Map<String, String> invalidBoolean; private Map<String, String> invalidInteger; @@ -94,6 +96,9 @@ public class EndpointValidationResult implements Serializable { ok = invalidEnum == null && invalidEnumChoices == null && invalidReference == null && invalidBoolean == null && invalidInteger == null && invalidNumber == null; } + if (ok) { + ok = invalidMap == null && invalidArray == null; + } return ok; } @@ -191,6 +196,26 @@ public class EndpointValidationResult implements Serializable { } } + public void addInvalidMap(String name, String value) { + if (invalidMap == null) { + invalidMap = new LinkedHashMap<>(); + } + if (!invalidMap.containsKey(name)) { + invalidMap.put(name, value); + errors++; + } + } + + public void addInvalidArray(String name, String value) { + if (invalidArray == null) { + invalidArray = new LinkedHashMap<>(); + } + if (!invalidArray.containsKey(name)) { + invalidArray.put(name, value); + errors++; + } + } + public void addInvalidBoolean(String name, String value) { if (invalidBoolean == null) { invalidBoolean = new LinkedHashMap<>(); @@ -303,6 +328,14 @@ public class EndpointValidationResult implements Serializable { return invalidReference; } + public Map<String, String> getInvalidMap() { + return invalidMap; + } + + public Map<String, String> getInvalidArray() { + return invalidArray; + } + public Map<String, String> getInvalidBoolean() { return invalidBoolean; } diff --git a/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/JSonSchemaResolver.java b/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/JSonSchemaResolver.java index a6a8fc5..a849b0b 100644 --- a/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/JSonSchemaResolver.java +++ b/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/JSonSchemaResolver.java @@ -61,4 +61,11 @@ public interface JSonSchemaResolver { */ String getModelJSonSchema(String name); + /** + * Returns the camel-main json schema + * + * @return the camel-main json schema + */ + String getMainJsonSchema(); + } diff --git a/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/RuntimeCamelCatalog.java b/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/RuntimeCamelCatalog.java index 57bcda7..8228595 100644 --- a/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/RuntimeCamelCatalog.java +++ b/core/camel-api/src/main/java/org/apache/camel/runtimecatalog/RuntimeCamelCatalog.java @@ -108,7 +108,7 @@ public interface RuntimeCamelCatalog extends StaticService { EndpointValidationResult validateProperties(String scheme, Map<String, String> properties); /** - * Parses and validates the endpoint uri and constructs a key/value properties of each option. + * Parses and validates the endpoint uri * * @param uri the endpoint uri * @return validation result @@ -116,7 +116,7 @@ public interface RuntimeCamelCatalog extends StaticService { EndpointValidationResult validateEndpointProperties(String uri); /** - * Parses and validates the endpoint uri and constructs a key/value properties of each option. + * Parses and validates the endpoint uri * <p/> * The option ignoreLenientProperties can be used to ignore components that uses lenient properties. * When this is true, then the uri validation is stricter but would fail on properties that are not part of the component @@ -130,7 +130,7 @@ public interface RuntimeCamelCatalog extends StaticService { EndpointValidationResult validateEndpointProperties(String uri, boolean ignoreLenientProperties); /** - * Parses and validates the endpoint uri and constructs a key/value properties of each option. + * Parses and validates the endpoint uri * <p/> * The option ignoreLenientProperties can be used to ignore components that uses lenient properties. * When this is true, then the uri validation is stricter but would fail on properties that are not part of the component @@ -170,6 +170,14 @@ public interface RuntimeCamelCatalog extends StaticService { LanguageValidationResult validateLanguageExpression(ClassLoader classLoader, String language, String text); /** + * Parses and validates the configuration property + * + * @param text the configuration text + * @return validation result + */ + ConfigurationPropertiesValidationResult validateConfigurationProperty(String text); + + /** * Returns the component name from the given endpoint uri * * @param uri the endpoint uri diff --git a/core/camel-base/src/main/java/org/apache/camel/runtimecatalog/impl/AbstractCamelCatalog.java b/core/camel-base/src/main/java/org/apache/camel/runtimecatalog/impl/AbstractCamelCatalog.java index fe32e5b..2e06468 100644 --- a/core/camel-base/src/main/java/org/apache/camel/runtimecatalog/impl/AbstractCamelCatalog.java +++ b/core/camel-base/src/main/java/org/apache/camel/runtimecatalog/impl/AbstractCamelCatalog.java @@ -26,6 +26,7 @@ import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -34,36 +35,18 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import org.apache.camel.runtimecatalog.ConfigurationPropertiesValidationResult; import org.apache.camel.runtimecatalog.EndpointValidationResult; import org.apache.camel.runtimecatalog.JSonSchemaResolver; import org.apache.camel.runtimecatalog.LanguageValidationResult; import static org.apache.camel.runtimecatalog.impl.CatalogHelper.after; +import static org.apache.camel.runtimecatalog.impl.CatalogHelper.before; import static org.apache.camel.runtimecatalog.impl.URISupport.createQueryString; import static org.apache.camel.runtimecatalog.impl.URISupport.isEmpty; import static org.apache.camel.runtimecatalog.impl.URISupport.normalizeUri; import static org.apache.camel.runtimecatalog.impl.URISupport.stripQuery; -import static org.apache.camel.support.JSonSchemaHelper.getNames; -import static org.apache.camel.support.JSonSchemaHelper.getPropertyDefaultValue; -import static org.apache.camel.support.JSonSchemaHelper.getPropertyEnum; -import static org.apache.camel.support.JSonSchemaHelper.getPropertyKind; -import static org.apache.camel.support.JSonSchemaHelper.getPropertyNameFromNameWithPrefix; -import static org.apache.camel.support.JSonSchemaHelper.getPropertyPrefix; -import static org.apache.camel.support.JSonSchemaHelper.getRow; -import static org.apache.camel.support.JSonSchemaHelper.isComponentConsumerOnly; -import static org.apache.camel.support.JSonSchemaHelper.isComponentLenientProperties; -import static org.apache.camel.support.JSonSchemaHelper.isComponentProducerOnly; -import static org.apache.camel.support.JSonSchemaHelper.isPropertyBoolean; -import static org.apache.camel.support.JSonSchemaHelper.isPropertyConsumerOnly; -import static org.apache.camel.support.JSonSchemaHelper.isPropertyDeprecated; -import static org.apache.camel.support.JSonSchemaHelper.isPropertyInteger; -import static org.apache.camel.support.JSonSchemaHelper.isPropertyMultiValue; -import static org.apache.camel.support.JSonSchemaHelper.isPropertyNumber; -import static org.apache.camel.support.JSonSchemaHelper.isPropertyObject; -import static org.apache.camel.support.JSonSchemaHelper.isPropertyProducerOnly; -import static org.apache.camel.support.JSonSchemaHelper.isPropertyRequired; -import static org.apache.camel.support.JSonSchemaHelper.parseJsonSchema; -import static org.apache.camel.support.JSonSchemaHelper.stripOptionalPrefixFromName; +import static org.apache.camel.support.JSonSchemaHelper.*; /** * Base class for both the runtime RuntimeCamelCatalog from camel-core and the complete CamelCatalog from camel-catalog. @@ -1036,6 +1019,228 @@ public abstract class AbstractCamelCatalog { return tokens.toArray(new String[tokens.size()]); } + public ConfigurationPropertiesValidationResult validateConfigurationProperty(String text) { + String longKey = before(text, "="); + String key = longKey; + String value = after(text, "="); + + ConfigurationPropertiesValidationResult result = new ConfigurationPropertiesValidationResult(key, value); + boolean accept = acceptConfigurationPropertyKey(key); + if (!accept) { + result.addUnknown(key); + return result; + } + + boolean component = key.startsWith("camel.component."); + boolean dataformat = key.startsWith("camel.dataformat."); + boolean language = key.startsWith("camel.language."); + boolean main = key.startsWith("camel.main.") || key.startsWith("camel.hystrix.") | key.startsWith("camel.resilience4j.") || key.startsWith("camel.rest."); + if (component || dataformat || language) { + String group; + if (component) { + key = key.substring(16); + group = "componentProperties"; + } else if (dataformat) { + key = key.substring(17); + group = "properties"; + } else { + key = key.substring(15); + group = "properties"; + } + if (!key.contains(".")) { + result.addIncapable(key); + return result; + } + String name = before(key, "."); + String option = after(key, "."); + if (name != null && option != null && value != null) { + String json; + if (component) { + json = jsonSchemaResolver.getComponentJSonSchema(name); + } else if (dataformat) { + json = jsonSchemaResolver.getDataFormatJSonSchema(name); + } else { + json = jsonSchemaResolver.getLanguageJSonSchema(name); + } + if (json == null) { + result.addUnknownComponent(name); + return result; + } + List<Map<String, String>> rows = parseJsonSchema(group, json, true); + + // lower case option and remove dash + String nOption = option.replaceAll("-", "").toLowerCase(Locale.US); + String suffix = null; + int posDot = nOption.indexOf('.'); + int posBracket = nOption.indexOf('['); + if (posDot > 0 && posBracket > 0) { + int first = Math.min(posDot, posBracket); + suffix = nOption.substring(first); + nOption = nOption.substring(0, first); + } else if (posDot > 0) { + suffix = nOption.substring(posDot); + nOption = nOption.substring(0, posDot); + } else if (posBracket > 0) { + suffix = nOption.substring(posBracket); + nOption = nOption.substring(0, posBracket); + } + doValidateConfigurationProperty(result, rows, name, value, longKey, nOption, suffix); + } + } else if (main) { + // skip camel. + key = key.substring(6); + String name = before(key, "."); + String option = after(key, "."); + if (name != null && option != null && value != null) { + String json = jsonSchemaResolver.getMainJsonSchema(); + if (json == null) { + result.addIncapable("camel-main not detected on classpath"); + return result; + } + List<Map<String, String>> rows = parseMainJsonSchema(json); + + // lower case option and remove dash + String lookupKey = longKey.replaceAll("-", "").toLowerCase(Locale.US); + String suffix = null; + int pos = option.indexOf('.'); + // TODO: add support for [] maps for main + if (pos > 0 && option.length() > pos) { + suffix = option.substring(pos + 1); + // remove .suffix from lookup key + int len = lookupKey.length() - suffix.length() - 1; + lookupKey = lookupKey.substring(0, len); + } + doValidateConfigurationProperty(result, rows, name, value, longKey, lookupKey, suffix); + } + } + + return result; + } + + private void doValidateConfigurationProperty(ConfigurationPropertiesValidationResult result, List<Map<String, String>> rows, + String name, String value, String longKey, + String lookupKey, String suffix) { + + // find option + String rowKey = rows.stream() + .map(e -> e.get("name")) + .filter(n -> n.toLowerCase(Locale.US).equals(lookupKey)).findFirst().orElse(null); + if (rowKey == null) { + // unknown option + result.addUnknown(longKey); + if (suggestionStrategy != null) { + String[] suggestions = suggestionStrategy.suggestEndpointOptions(getNames(rows), name); + if (suggestions != null) { + result.addUnknownSuggestions(name, suggestions); + } + } + } else { + boolean optionPlaceholder = value.startsWith("{{") || value.startsWith("${") || value.startsWith("$simple{"); + boolean lookup = value.startsWith("#") && value.length() > 1; + + // deprecated + if (!optionPlaceholder && !lookup && isPropertyDeprecated(rows, rowKey)) { + result.addDeprecated(longKey); + } + + // is boolean + if (!optionPlaceholder && !lookup && isPropertyBoolean(rows, rowKey)) { + // value must be a boolean + boolean bool = "true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value); + if (!bool) { + result.addInvalidBoolean(longKey, value); + } + } + + // is integer + if (!optionPlaceholder && !lookup && isPropertyInteger(rows, rowKey)) { + // value must be an integer + boolean valid = validateInteger(value); + if (!valid) { + result.addInvalidInteger(longKey, value); + } + } + + // is number + if (!optionPlaceholder && !lookup && isPropertyNumber(rows, rowKey)) { + // value must be an number + boolean valid = false; + try { + valid = !Double.valueOf(value).isNaN() || !Float.valueOf(value).isNaN(); + } catch (Exception e) { + // ignore + } + if (!valid) { + result.addInvalidNumber(longKey, value); + } + } + + // is enum + String enums = getPropertyEnum(rows, rowKey); + if (!optionPlaceholder && !lookup && enums != null) { + String[] choices = enums.split(","); + boolean found = false; + for (String s : choices) { + if (value.equalsIgnoreCase(s)) { + found = true; + break; + } + } + if (!found) { + result.addInvalidEnum(longKey, value); + result.addInvalidEnumChoices(longKey, choices); + if (suggestionStrategy != null) { + Set<String> names = new LinkedHashSet<>(); + names.addAll(Arrays.asList(choices)); + String[] suggestions = suggestionStrategy.suggestEndpointOptions(names, value); + if (suggestions != null) { + result.addInvalidEnumSuggestions(longKey, suggestions); + } + } + } + } + + String javaType = getPropertyJavaType(rows, rowKey); + if (!optionPlaceholder && !lookup && javaType != null + && (javaType.startsWith("java.util.Map") || javaType.startsWith("java.util.Properties"))) { + // there must be a valid suffix + if (suffix == null || suffix.isEmpty() || suffix.equals(".")) { + result.addInvalidMap(longKey, value); + } else if (suffix.startsWith("[") && !suffix.contains("]")) { + result.addInvalidMap(longKey, value); + } + } + if (!optionPlaceholder && !lookup && javaType != null && isPropertyArray(rows, rowKey)) { + // there must be a suffix and it must be using [] style + if (suffix == null || suffix.isEmpty() || suffix.equals(".")) { + result.addInvalidArray(longKey, value); + } else if (!suffix.startsWith("[") && !suffix.contains("]")) { + result.addInvalidArray(longKey, value); + } else { + String index = before(suffix.substring(1), "]"); + // value must be an integer + boolean valid = validateInteger(index); + if (!valid) { + result.addInvalidInteger(longKey, index); + } + } + } + } + } + + private static boolean acceptConfigurationPropertyKey(String key) { + if (key == null) { + return false; + } + return key.startsWith("camel.component.") + || key.startsWith("camel.dataformat.") + || key.startsWith("camel.language.") + || key.startsWith("camel.main.") + || key.startsWith("camel.hystrix.") + || key.startsWith("camel.resilience4j.") + || key.startsWith("camel.rest."); + } + private LanguageValidationResult doValidateSimple(ClassLoader classLoader, String simple, boolean predicate) { if (classLoader == null) { classLoader = getClass().getClassLoader(); @@ -1225,7 +1430,8 @@ public abstract class AbstractCamelCatalog { private static boolean validateInteger(String value) { boolean valid = false; try { - valid = Integer.valueOf(value) != null; + Integer.parseInt(value); + valid = true; } catch (Exception e) { // ignore } diff --git a/core/camel-base/src/main/java/org/apache/camel/runtimecatalog/impl/CamelContextJSonSchemaResolver.java b/core/camel-base/src/main/java/org/apache/camel/runtimecatalog/impl/CamelContextJSonSchemaResolver.java index ac8f092..9e92aa5 100644 --- a/core/camel-base/src/main/java/org/apache/camel/runtimecatalog/impl/CamelContextJSonSchemaResolver.java +++ b/core/camel-base/src/main/java/org/apache/camel/runtimecatalog/impl/CamelContextJSonSchemaResolver.java @@ -17,10 +17,13 @@ package org.apache.camel.runtimecatalog.impl; import java.io.IOException; +import java.io.InputStream; import org.apache.camel.CamelContext; import org.apache.camel.CatalogCamelContext; import org.apache.camel.runtimecatalog.JSonSchemaResolver; +import org.apache.camel.spi.ClassResolver; +import org.apache.camel.util.IOHelper; /** * Uses runtime {@link CamelContext} to resolve the JSon schema files. @@ -79,4 +82,20 @@ public class CamelContextJSonSchemaResolver implements JSonSchemaResolver { return null; } + @Override + public String getMainJsonSchema() { + String path = "META-INF/camel-main-configuration-metadata.json"; + ClassResolver resolver = camelContext.getClassResolver(); + InputStream inputStream = resolver.loadResourceAsStream(path); + if (inputStream != null) { + try { + return IOHelper.loadText(inputStream); + } catch (IOException e) { + // ignore + } finally { + IOHelper.close(inputStream); + } + } + return null; + } } diff --git a/core/camel-core/src/test/java/org/apache/camel/runtimecatalog/impl/RuntimeCamelCatalogTest.java b/core/camel-core/src/test/java/org/apache/camel/runtimecatalog/impl/RuntimeCamelCatalogTest.java index 37d79a8..93693f3 100644 --- a/core/camel-core/src/test/java/org/apache/camel/runtimecatalog/impl/RuntimeCamelCatalogTest.java +++ b/core/camel-core/src/test/java/org/apache/camel/runtimecatalog/impl/RuntimeCamelCatalogTest.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.Map; import org.apache.camel.impl.DefaultCamelContext; +import org.apache.camel.runtimecatalog.ConfigurationPropertiesValidationResult; import org.apache.camel.runtimecatalog.EndpointValidationResult; import org.apache.camel.runtimecatalog.LanguageValidationResult; import org.apache.camel.runtimecatalog.RuntimeCamelCatalog; @@ -28,10 +29,8 @@ import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; + +import static org.junit.Assert.*; public class RuntimeCamelCatalogTest { @@ -376,4 +375,50 @@ public class RuntimeCamelCatalogTest { assertEquals("delete", result.getNotProducerOnly().iterator().next()); } + @Test + public void testValidateConfigurationPropertyComponent() throws Exception { + String text = "camel.component.seda.queueSize=1234"; + ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.seda.queue-size=1234"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.seda.queuesize=1234"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.component.seda.queueSize=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertEquals("abc", result.getInvalidInteger().get("camel.component.seda.queueSize")); + + text = "camel.component.seda.foo=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertTrue(result.getUnknown().contains("camel.component.seda.foo")); + } + + @Test + public void testValidateConfigurationPropertyLanguage() throws Exception { + String text = "camel.language.tokenize.token=;"; + ConfigurationPropertiesValidationResult result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.language.tokenize.regex=true"; + result = catalog.validateConfigurationProperty(text); + assertTrue(result.isSuccess()); + + text = "camel.language.tokenize.regex=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertEquals("abc", result.getInvalidBoolean().get("camel.language.tokenize.regex")); + + text = "camel.language.tokenize.foo=abc"; + result = catalog.validateConfigurationProperty(text); + assertFalse(result.isSuccess()); + assertTrue(result.getUnknown().contains("camel.language.tokenize.foo")); + } + } diff --git a/core/camel-support/src/main/java/org/apache/camel/support/JSonSchemaHelper.java b/core/camel-support/src/main/java/org/apache/camel/support/JSonSchemaHelper.java index 6c77d3f..fc90ac9 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/JSonSchemaHelper.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/JSonSchemaHelper.java @@ -24,6 +24,7 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import org.apache.camel.util.json.JsonArray; import org.apache.camel.util.json.JsonObject; import org.apache.camel.util.json.Jsoner; @@ -36,6 +37,69 @@ public final class JSonSchemaHelper { } /** + * Parses the camel-main json schema to split it into a list or rows, where each row contains key value pairs with the metadata + * + * @param json the main configuration json + * @return a list of all the rows, where each row is a set of key value pairs with metadata + * @throws RuntimeException is thrown if error parsing the json data + */ + @SuppressWarnings("unchecked") + public static List<Map<String, String>> parseMainJsonSchema(String json) { + List<Map<String, String>> answer = new ArrayList<>(); + if (json == null) { + return answer; + } + + // convert into a List<Map<String, String>> structure which is expected as output from this parser + try { + JsonObject output = (JsonObject) Jsoner.deserialize(json); + for (String key : output.keySet()) { + JsonArray array = (JsonArray) output.get(key); + if (key.equals("properties")) { + // flattern each entry in the row with name as they key, and its value as the content (its a map also) + for (Object obj : array) { + Map entry = (Map) obj; + Map<String, String> newRow = new LinkedHashMap(); + newRow.putAll(entry); + answer.add(newRow); + String name = ((Map) obj).get("name").toString(); + // use naming style with camel case + String lookupKey = dashToCamelCase(name); + newRow.put("name", lookupKey); + // its the java type + String type = newRow.get("type"); + newRow.put("javaType", type); + newRow.put("type", fromMainToType(type)); + } + } + } + } catch (Exception e) { + // wrap parsing exceptions as runtime + throw new RuntimeException("Cannot parse json", e); + } + + return answer; + } + + private static String fromMainToType(String type) { + if ("boolean".equals(type) || "java.lang.Boolean".equals(type)) { + return "boolean"; + } else if ("int".equals(type) || "java.lang.Integer".equals(type)) { + return "integer"; + } else if ("long".equals(type) || "java.lang.Long".equals(type)) { + return "integer"; + } else if ("float".equals(type) || "java.lang.Float".equals(type)) { + return "number"; + } else if ("double".equals(type) || "java.lang.Double".equals(type)) { + return "number"; + } else if ("string".equals(type) || "java.lang.String".equals(type)) { + return "string"; + } else { + return "object"; + } + } + + /** * Parses the json schema to split it into a list or rows, where each row contains key value pairs with the metadata * * @param group the group to parse from such as <tt>component</tt>, <tt>componentProperties</tt>, or <tt>properties</tt>. @@ -150,7 +214,7 @@ public final class JSonSchemaHelper { String labels = null; boolean found = false; if (row.containsKey("name")) { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } if (row.containsKey("label")) { labels = row.get("label"); @@ -167,7 +231,7 @@ public final class JSonSchemaHelper { String labels = null; boolean found = false; if (row.containsKey("name")) { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } if (row.containsKey("label")) { labels = row.get("label"); @@ -184,7 +248,7 @@ public final class JSonSchemaHelper { boolean required = false; boolean found = false; if (row.containsKey("name")) { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } if (row.containsKey("required")) { required = "true".equals(row.get("required")); @@ -201,7 +265,7 @@ public final class JSonSchemaHelper { boolean deprecated = false; boolean found = false; if (row.containsKey("name")) { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } if (row.containsKey("deprecated")) { deprecated = "true".equals(row.get("deprecated")); @@ -218,7 +282,7 @@ public final class JSonSchemaHelper { String kind = null; boolean found = false; if (row.containsKey("name")) { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } if (row.containsKey("kind")) { kind = row.get("kind"); @@ -230,12 +294,29 @@ public final class JSonSchemaHelper { return null; } + public static String getPropertyJavaType(List<Map<String, String>> rows, String name) { + for (Map<String, String> row : rows) { + String javaType = null; + boolean found = false; + if (row.containsKey("name")) { + found = name.equalsIgnoreCase(row.get("name")); + } + if (row.containsKey("javaType")) { + javaType = row.get("javaType"); + } + if (found) { + return javaType; + } + } + return null; + } + public static boolean isPropertyBoolean(List<Map<String, String>> rows, String name) { for (Map<String, String> row : rows) { String type = null; boolean found = false; if (row.containsKey("name")) { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } if (row.containsKey("type")) { type = row.get("type"); @@ -252,7 +333,7 @@ public final class JSonSchemaHelper { String type = null; boolean found = false; if (row.containsKey("name")) { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } if (row.containsKey("type")) { type = row.get("type"); @@ -264,12 +345,29 @@ public final class JSonSchemaHelper { return false; } + public static boolean isPropertyArray(List<Map<String, String>> rows, String name) { + for (Map<String, String> row : rows) { + String type = null; + boolean found = false; + if (row.containsKey("name")) { + found = name.equalsIgnoreCase(row.get("name")); + } + if (row.containsKey("type")) { + type = row.get("type"); + } + if (found) { + return "array".equals(type); + } + } + return false; + } + public static boolean isPropertyNumber(List<Map<String, String>> rows, String name) { for (Map<String, String> row : rows) { String type = null; boolean found = false; if (row.containsKey("name")) { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } if (row.containsKey("type")) { type = row.get("type"); @@ -286,7 +384,7 @@ public final class JSonSchemaHelper { String type = null; boolean found = false; if (row.containsKey("name")) { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } if (row.containsKey("type")) { type = row.get("type"); @@ -303,7 +401,7 @@ public final class JSonSchemaHelper { String defaultValue = null; boolean found = false; if (row.containsKey("name")) { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } if (row.containsKey("defaultValue")) { defaultValue = row.get("defaultValue"); @@ -328,7 +426,7 @@ public final class JSonSchemaHelper { // try again return stripOptionalPrefixFromName(rows, name); } else { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } } if (found) { @@ -343,7 +441,7 @@ public final class JSonSchemaHelper { String enums = null; boolean found = false; if (row.containsKey("name")) { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } if (row.containsKey("enum")) { enums = row.get("enum"); @@ -360,7 +458,7 @@ public final class JSonSchemaHelper { String prefix = null; boolean found = false; if (row.containsKey("name")) { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } if (row.containsKey("prefix")) { prefix = row.get("prefix"); @@ -377,7 +475,7 @@ public final class JSonSchemaHelper { boolean multiValue = false; boolean found = false; if (row.containsKey("name")) { - found = name.equals(row.get("name")); + found = name.equalsIgnoreCase(row.get("name")); } if (row.containsKey("multiValue")) { multiValue = "true".equals(row.get("multiValue")); @@ -426,4 +524,35 @@ public final class JSonSchemaHelper { return answer; } + /** + * Converts the string from dash format into camel case (hello-great-world -> helloGreatWorld) + * + * @param text the string + * @return the string camel cased + */ + private static String dashToCamelCase(String text) { + if (text == null) { + return null; + } + int length = text.length(); + if (length == 0) { + return text; + } + if (text.indexOf('-') == -1) { + return text; + } + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + if (c == '-') { + i++; + sb.append(Character.toUpperCase(text.charAt(i))); + } else { + sb.append(c); + } + } + return sb.toString(); + } } diff --git a/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java b/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java index 25d5bd8..1a09199 100644 --- a/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java +++ b/core/camel-support/src/main/java/org/apache/camel/support/PropertyBindingSupport.java @@ -713,7 +713,7 @@ public final class PropertyBindingSupport { } else if (answer instanceof List) { List list = (List) answer; if (isNotEmpty(lookupKey)) { - int idx = Integer.valueOf(lookupKey); + int idx = Integer.parseInt(lookupKey); answer = list.get(idx); } else { if (list.isEmpty()) {