CAMEL-10959: add RuntimeCamelCatalog to camel-core so we can reuse more catalog logic at runtime and have camel-catalog for tooling that has the complete catalog content. We need the runtime for component health check and more in the future.
Project: http://git-wip-us.apache.org/repos/asf/camel/repo Commit: http://git-wip-us.apache.org/repos/asf/camel/commit/f5848e39 Tree: http://git-wip-us.apache.org/repos/asf/camel/tree/f5848e39 Diff: http://git-wip-us.apache.org/repos/asf/camel/diff/f5848e39 Branch: refs/heads/master Commit: f5848e39c08b0abd99a5f4e9eba73c7740c60c05 Parents: 6e9c513 Author: Claus Ibsen <davscl...@apache.org> Authored: Wed Mar 8 18:57:20 2017 +0100 Committer: Claus Ibsen <davscl...@apache.org> Committed: Wed Mar 8 18:57:20 2017 +0100 ---------------------------------------------------------------------- camel-core/pom.xml | 29 + .../camel/catalog/AbstractCamelCatalog.java | 1029 +++++++++++++++++ .../catalog/CamelContextJSonSchemaResolver.java | 80 ++ .../org/apache/camel/catalog/CatalogHelper.java | 195 ++++ .../camel/catalog/CollectionStringBuffer.java | 57 + .../catalog/DefaultRuntimeCamelCatalog.java | 126 +++ .../camel/catalog/EndpointValidationResult.java | 426 +++++++ .../apache/camel/catalog/JSonSchemaHelper.java | 424 +++++++ .../camel/catalog/JSonSchemaResolver.java | 64 ++ .../camel/catalog/LanguageValidationResult.java | 65 ++ .../camel/catalog/RuntimeCamelCatalog.java | 245 ++++ .../camel/catalog/SimpleValidationResult.java | 32 + .../camel/catalog/SuggestionStrategy.java | 34 + .../camel/catalog/TimePatternConverter.java | 120 ++ .../org/apache/camel/catalog/URISupport.java | 392 +++++++ .../catalog/UnsafeUriCharactersEncoder.java | 206 ++++ .../java/org/apache/camel/catalog/package.html | 25 + .../org/apache/camel/util/EndpointHelper.java | 286 +---- .../camel/catalog/RuntimeCamelCatalogTest.java | 392 +++++++ platforms/camel-catalog/pom.xml | 49 +- .../camel/catalog/AbstractCamelCatalog.java | 1029 +++++++++++++++++ .../catalog/CamelCatalogJSonSchemaResolver.java | 181 +++ .../camel/catalog/DefaultCamelCatalog.java | 1053 +----------------- .../camel/catalog/JSonSchemaResolver.java | 64 ++ .../java/org/apache/camel/catalog/package.html | 25 + 25 files changed, 5301 insertions(+), 1327 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/camel/blob/f5848e39/camel-core/pom.xml ---------------------------------------------------------------------- diff --git a/camel-core/pom.xml b/camel-core/pom.xml index 1a7c30e..2b7ae8a 100644 --- a/camel-core/pom.xml +++ b/camel-core/pom.xml @@ -230,7 +230,36 @@ </plugin> </plugins> </pluginManagement> + <plugins> + + <!-- Inline the contents of camel-catalog-core into this jar. --> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-dependency-plugin</artifactId> + <version>2.10</version> + <executions> + <execution> + <id>unpack</id> + <phase>process-sources</phase> + <goals> + <goal>unpack</goal> + </goals> + <configuration> + <artifactItems> + <artifactItem> + <groupId>org.apache.camel</groupId> + <artifactId>camel-catalog-core</artifactId> + <version>${project.version}</version> + <includes>org/apache/camel/catalog/**</includes> + <outputDirectory>${project.build.directory}/classes</outputDirectory> + </artifactItem> + </artifactItems> + </configuration> + </execution> + </executions> + </plugin> + <!-- shade caffeine cache for faster Camel and spi-annotations as needed by everybody --> <plugin> <groupId>org.apache.maven.plugins</groupId> http://git-wip-us.apache.org/repos/asf/camel/blob/f5848e39/camel-core/src/main/java/org/apache/camel/catalog/AbstractCamelCatalog.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/catalog/AbstractCamelCatalog.java b/camel-core/src/main/java/org/apache/camel/catalog/AbstractCamelCatalog.java new file mode 100644 index 0000000..3295ca9 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/catalog/AbstractCamelCatalog.java @@ -0,0 +1,1029 @@ +/** + * 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.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.apache.camel.catalog.CatalogHelper.after; +import static org.apache.camel.catalog.JSonSchemaHelper.getNames; +import static org.apache.camel.catalog.JSonSchemaHelper.getPropertyDefaultValue; +import static org.apache.camel.catalog.JSonSchemaHelper.getPropertyEnum; +import static org.apache.camel.catalog.JSonSchemaHelper.getPropertyKind; +import static org.apache.camel.catalog.JSonSchemaHelper.getPropertyNameFromNameWithPrefix; +import static org.apache.camel.catalog.JSonSchemaHelper.getPropertyPrefix; +import static org.apache.camel.catalog.JSonSchemaHelper.getRow; +import static org.apache.camel.catalog.JSonSchemaHelper.isComponentConsumerOnly; +import static org.apache.camel.catalog.JSonSchemaHelper.isComponentLenientProperties; +import static org.apache.camel.catalog.JSonSchemaHelper.isComponentProducerOnly; +import static org.apache.camel.catalog.JSonSchemaHelper.isPropertyBoolean; +import static org.apache.camel.catalog.JSonSchemaHelper.isPropertyConsumerOnly; +import static org.apache.camel.catalog.JSonSchemaHelper.isPropertyInteger; +import static org.apache.camel.catalog.JSonSchemaHelper.isPropertyMultiValue; +import static org.apache.camel.catalog.JSonSchemaHelper.isPropertyNumber; +import static org.apache.camel.catalog.JSonSchemaHelper.isPropertyObject; +import static org.apache.camel.catalog.JSonSchemaHelper.isPropertyProducerOnly; +import static org.apache.camel.catalog.JSonSchemaHelper.isPropertyRequired; +import static org.apache.camel.catalog.JSonSchemaHelper.stripOptionalPrefixFromName; +import static org.apache.camel.catalog.URISupport.createQueryString; +import static org.apache.camel.catalog.URISupport.isEmpty; +import static org.apache.camel.catalog.URISupport.normalizeUri; +import static org.apache.camel.catalog.URISupport.stripQuery; + +/** + * Base class for both the runtime RuntimeCamelCatalog from camel-core and the complete CamelCatalog from camel-catalog. + */ +public abstract class AbstractCamelCatalog { + + // CHECKSTYLE:OFF + + private static final Pattern SYNTAX_PATTERN = Pattern.compile("(\\w+)"); + + private SuggestionStrategy suggestionStrategy; + private JSonSchemaResolver jsonSchemaResolver; + + public SuggestionStrategy getSuggestionStrategy() { + return suggestionStrategy; + } + + public void setSuggestionStrategy(SuggestionStrategy suggestionStrategy) { + this.suggestionStrategy = suggestionStrategy; + } + + public JSonSchemaResolver getJSonSchemaResolver() { + return jsonSchemaResolver; + } + + public void setJSonSchemaResolver(JSonSchemaResolver resolver) { + this.jsonSchemaResolver = resolver; + } + + public boolean validateTimePattern(String pattern) { + return validateInteger(pattern); + } + + public EndpointValidationResult validateEndpointProperties(String uri) { + return validateEndpointProperties(uri, false, false, false); + } + + public EndpointValidationResult validateEndpointProperties(String uri, boolean ignoreLenientProperties) { + return validateEndpointProperties(uri, ignoreLenientProperties, false, false); + } + + public EndpointValidationResult validateEndpointProperties(String uri, boolean ignoreLenientProperties, boolean consumerOnly, boolean producerOnly) { + EndpointValidationResult result = new EndpointValidationResult(uri); + + Map<String, String> properties; + List<Map<String, String>> rows; + boolean lenientProperties; + String scheme; + + try { + String json = null; + + // parse the uri + URI u = normalizeUri(uri); + scheme = u.getScheme(); + + if (scheme != null) { + json = jsonSchemaResolver.getComponentJSonSchema(scheme); + } + if (json == null) { + // if the uri starts with a placeholder then we are also incapable of parsing it as we wasn't able to resolve the component name + if (uri.startsWith("{{")) { + result.addIncapable(uri); + } else if (scheme != null) { + result.addUnknownComponent(scheme); + } else { + result.addUnknownComponent(uri); + } + return result; + } + + rows = JSonSchemaHelper.parseJsonSchema("component", json, false); + + // is the component capable of both consumer and producer? + boolean canConsumeAndProduce = false; + if (!isComponentConsumerOnly(rows) && !isComponentProducerOnly(rows)) { + canConsumeAndProduce = true; + } + + if (canConsumeAndProduce && consumerOnly) { + // lenient properties is not support in consumer only mode if the component can do both of them + lenientProperties = false; + } else { + // only enable lenient properties if we should not ignore + lenientProperties = !ignoreLenientProperties && isComponentLenientProperties(rows); + } + rows = JSonSchemaHelper.parseJsonSchema("properties", json, true); + properties = endpointProperties(uri); + } catch (URISyntaxException e) { + if (uri.startsWith("{{")) { + // if the uri starts with a placeholder then we are also incapable of parsing it as we wasn't able to resolve the component name + result.addIncapable(uri); + } else { + result.addSyntaxError(e.getMessage()); + } + + return result; + } + + // the dataformat component refers to a data format so lets add the properties for the selected + // data format to the list of rows + if ("dataformat".equals(scheme)) { + String dfName = properties.get("name"); + if (dfName != null) { + String dfJson = jsonSchemaResolver.getDataFormatJSonSchema(dfName); + List<Map<String, String>> dfRows = JSonSchemaHelper.parseJsonSchema("properties", dfJson, true); + if (dfRows != null && !dfRows.isEmpty()) { + rows.addAll(dfRows); + } + } + } + + for (Map.Entry<String, String> property : properties.entrySet()) { + String value = property.getValue(); + String originalName = property.getKey(); + String name = property.getKey(); + // the name may be using an optional prefix, so lets strip that because the options + // in the schema are listed without the prefix + name = stripOptionalPrefixFromName(rows, name); + // the name may be using a prefix, so lets see if we can find the real property name + String propertyName = getPropertyNameFromNameWithPrefix(rows, name); + if (propertyName != null) { + name = propertyName; + } + + String prefix = getPropertyPrefix(rows, name); + String kind = getPropertyKind(rows, name); + boolean namePlaceholder = name.startsWith("{{") && name.endsWith("}}"); + boolean valuePlaceholder = value.startsWith("{{") || value.startsWith("${") || value.startsWith("$simple{"); + boolean lookup = value.startsWith("#") && value.length() > 1; + // we cannot evaluate multi values as strict as the others, as we don't know their expected types + boolean mulitValue = prefix != null && originalName.startsWith(prefix) && isPropertyMultiValue(rows, name); + + Map<String, String> row = getRow(rows, name); + if (row == null) { + // unknown option + + // only add as error if the component is not lenient properties, or not stub component + // and the name is not a property placeholder for one or more values + if (!namePlaceholder && !"stub".equals(scheme)) { + if (lenientProperties) { + // as if we are lenient then the option is a dynamic extra option which we cannot validate + result.addLenient(name); + } else { + // its unknown + result.addUnknown(name); + if (suggestionStrategy != null) { + String[] suggestions = suggestionStrategy.suggestEndpointOptions(getNames(rows), name); + if (suggestions != null) { + result.addUnknownSuggestions(name, suggestions); + } + } + } + } + } else { + if ("parameter".equals(kind)) { + // consumer only or producer only mode for parameters + if (consumerOnly) { + boolean producer = isPropertyProducerOnly(rows, name); + if (producer) { + // the option is only for producer so you cannot use it in consumer mode + result.addNotConsumerOnly(name); + } + } else if (producerOnly) { + boolean consumer = isPropertyConsumerOnly(rows, name); + if (consumer) { + // the option is only for consumer so you cannot use it in producer mode + result.addNotProducerOnly(name); + } + } + } + + // default value + String defaultValue = getPropertyDefaultValue(rows, name); + if (defaultValue != null) { + result.addDefaultValue(name, defaultValue); + } + + // is required but the value is empty + boolean required = isPropertyRequired(rows, name); + if (required && isEmpty(value)) { + result.addRequired(name); + } + + // is enum but the value is not within the enum range + // but we can only check if the value is not a placeholder + String enums = getPropertyEnum(rows, name); + if (!mulitValue && !valuePlaceholder && !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(name, value); + result.addInvalidEnumChoices(name, 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(name, suggestions); + } + } + + } + } + + // is reference lookup of bean (not applicable for @UriPath, enums, or multi-valued) + if (!mulitValue && enums == null && !"path".equals(kind) && isPropertyObject(rows, name)) { + // must start with # and be at least 2 characters + if (!value.startsWith("#") || value.length() <= 1) { + result.addInvalidReference(name, value); + } + } + + // is boolean + if (!mulitValue && !valuePlaceholder && !lookup && isPropertyBoolean(rows, name)) { + // value must be a boolean + boolean bool = "true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value); + if (!bool) { + result.addInvalidBoolean(name, value); + } + } + + // is integer + if (!mulitValue && !valuePlaceholder && !lookup && isPropertyInteger(rows, name)) { + // value must be an integer + boolean valid = validateInteger(value); + if (!valid) { + result.addInvalidInteger(name, value); + } + } + + // is number + if (!mulitValue && !valuePlaceholder && !lookup && isPropertyNumber(rows, name)) { + // 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(name, value); + } + } + } + } + + // now check if all required values are there, and that a default value does not exists + for (Map<String, String> row : rows) { + String name = row.get("name"); + boolean required = isPropertyRequired(rows, name); + if (required) { + String value = properties.get(name); + if (isEmpty(value)) { + value = getPropertyDefaultValue(rows, name); + } + if (isEmpty(value)) { + result.addRequired(name); + } + } + } + + return result; + } + + public Map<String, String> endpointProperties(String uri) throws URISyntaxException { + // need to normalize uri first + URI u = normalizeUri(uri); + String scheme = u.getScheme(); + + String json = jsonSchemaResolver.getComponentJSonSchema(scheme); + if (json == null) { + throw new IllegalArgumentException("Cannot find endpoint with scheme " + scheme); + } + + // grab the syntax + String syntax = null; + String alternativeSyntax = null; + List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("component", json, false); + for (Map<String, String> row : rows) { + if (row.containsKey("syntax")) { + syntax = row.get("syntax"); + } + if (row.containsKey("alternativeSyntax")) { + alternativeSyntax = row.get("alternativeSyntax"); + } + } + if (syntax == null) { + throw new IllegalArgumentException("Endpoint with scheme " + scheme + " has no syntax defined in the json schema"); + } + + // only if we support alternative syntax, and the uri contains the username and password in the authority + // part of the uri, then we would need some special logic to capture that information and strip those + // details from the uri, so we can continue parsing the uri using the normal syntax + Map<String, String> userInfoOptions = new LinkedHashMap<String, String>(); + if (alternativeSyntax != null && alternativeSyntax.contains("@")) { + // clip the scheme from the syntax + alternativeSyntax = after(alternativeSyntax, ":"); + // trim so only userinfo + int idx = alternativeSyntax.indexOf("@"); + String fields = alternativeSyntax.substring(0, idx); + String[] names = fields.split(":"); + + // grab authority part and grab username and/or password + String authority = u.getAuthority(); + if (authority != null && authority.contains("@")) { + String username = null; + String password = null; + + // 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]; + } else { + // only username + username = userInfo; + } + + // remember the username and/or password which we add later to the options + if (names.length == 2) { + userInfoOptions.put(names[0], username); + if (password != null) { + // password is optional + userInfoOptions.put(names[1], password); + } + } + } + } + + // clip the scheme from the syntax + syntax = after(syntax, ":"); + // clip the scheme from the uri + uri = after(uri, ":"); + String uriPath = stripQuery(uri); + + // strip user info from uri path + if (!userInfoOptions.isEmpty()) { + int idx = uriPath.indexOf('@'); + if (idx > -1) { + uriPath = uriPath.substring(idx + 1); + } + } + + // strip double slash in the start + if (uriPath != null && uriPath.startsWith("//")) { + uriPath = uriPath.substring(2); + } + + // parse the syntax and find the names of each option + Matcher matcher = SYNTAX_PATTERN.matcher(syntax); + List<String> word = new ArrayList<String>(); + while (matcher.find()) { + String s = matcher.group(1); + if (!scheme.equals(s)) { + word.add(s); + } + } + // parse the syntax and find each token between each option + String[] tokens = SYNTAX_PATTERN.split(syntax); + + // find the position where each option start/end + List<String> word2 = new ArrayList<String>(); + int prev = 0; + int prevPath = 0; + + // 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 + if ("activemq".equals(scheme) || "jms".equals(scheme)) { + if (uriPath.startsWith("temp:")) { + prevPath = 5; + } + } + + for (String token : tokens) { + if (token.isEmpty()) { + continue; + } + + // special for some tokens where :// can be used also, eg http://foo + int idx = -1; + int len = 0; + if (":".equals(token)) { + idx = uriPath.indexOf("://", prevPath); + len = 3; + } + if (idx == -1) { + idx = uriPath.indexOf(token, prevPath); + len = token.length(); + } + + if (idx > 0) { + String option = uriPath.substring(prev, idx); + word2.add(option); + prev = idx + len; + prevPath = prev; + } + } + // special for last or if we did not add anyone + if (prev > 0 || word2.isEmpty()) { + String option = uriPath.substring(prev); + word2.add(option); + } + + rows = JSonSchemaHelper.parseJsonSchema("properties", json, true); + + boolean defaultValueAdded = false; + + // now parse the uri to know which part isw what + Map<String, String> options = new LinkedHashMap<String, String>(); + + // include the username and password from the userinfo section + if (!userInfoOptions.isEmpty()) { + options.putAll(userInfoOptions); + } + + // word contains the syntax path elements + Iterator<String> it = word2.iterator(); + for (int i = 0; i < word.size(); i++) { + String key = word.get(i); + + boolean allOptions = word.size() == word2.size(); + boolean required = isPropertyRequired(rows, key); + String defaultValue = getPropertyDefaultValue(rows, key); + + // we have all options so no problem + if (allOptions) { + String value = it.next(); + options.put(key, value); + } else { + // we have a little problem as we do not not have all options + if (!required) { + String value = null; + + boolean last = i == word.size() - 1; + if (last) { + // if its the last value then use it instead of the default value + value = it.hasNext() ? it.next() : null; + if (value != null) { + options.put(key, value); + } else { + value = defaultValue; + } + } + if (value != null) { + options.put(key, value); + defaultValueAdded = true; + } + } else { + String value = it.hasNext() ? it.next() : null; + if (value != null) { + options.put(key, value); + } + } + } + } + + Map<String, String> answer = new LinkedHashMap<String, String>(); + + // remove all options which are using default values and are not required + for (Map.Entry<String, String> entry : options.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + + if (defaultValueAdded) { + boolean required = isPropertyRequired(rows, key); + String defaultValue = getPropertyDefaultValue(rows, key); + + if (!required && defaultValue != null) { + if (defaultValue.equals(value)) { + continue; + } + } + } + + // we should keep this in the answer + answer.put(key, value); + } + + // now parse the uri parameters + Map<String, Object> parameters = URISupport.parseParameters(u); + + // and covert the values to String so its JMX friendly + while (!parameters.isEmpty()) { + Map.Entry<String, Object> entry = parameters.entrySet().iterator().next(); + String key = entry.getKey(); + String value = entry.getValue() != null ? entry.getValue().toString() : ""; + + boolean multiValued = isPropertyMultiValue(rows, key); + if (multiValued) { + String prefix = getPropertyPrefix(rows, key); + // extra all the multi valued options + Map<String, Object> values = URISupport.extractProperties(parameters, prefix); + // build a string with the extra multi valued options with the prefix and & as separator + CollectionStringBuffer csb = new CollectionStringBuffer("&"); + for (Map.Entry<String, Object> multi : values.entrySet()) { + String line = prefix + multi.getKey() + "=" + (multi.getValue() != null ? multi.getValue().toString() : ""); + csb.append(line); + } + // append the extra multi-values to the existing (which contains the first multi value) + if (!csb.isEmpty()) { + value = value + "&" + csb.toString(); + } + } + + answer.put(key, value); + // remove the parameter as we run in a while loop until no more parameters + parameters.remove(key); + } + + return answer; + } + + public Map<String, String> endpointLenientProperties(String uri) throws URISyntaxException { + // need to normalize uri first + + // parse the uri + URI u = normalizeUri(uri); + String scheme = u.getScheme(); + + String json = jsonSchemaResolver.getComponentJSonSchema(scheme); + if (json == null) { + throw new IllegalArgumentException("Cannot find endpoint with scheme " + scheme); + } + + List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("properties", json, true); + + // now parse the uri parameters + Map<String, Object> parameters = URISupport.parseParameters(u); + + // all the known options + Set<String> names = getNames(rows); + + Map<String, String> answer = new LinkedHashMap<>(); + + // and covert the values to String so its JMX friendly + parameters.forEach((k, v) -> { + String key = k; + String value = v != null ? v.toString() : ""; + + // is the key a prefix property + int dot = key.indexOf('.'); + if (dot != -1) { + String prefix = key.substring(0, dot + 1); // include dot in prefix + String option = getPropertyNameFromNameWithPrefix(rows, prefix); + if (option == null || !isPropertyMultiValue(rows, option)) { + answer.put(key, value); + } + } else if (!names.contains(key)) { + answer.put(key, value); + } + }); + + return answer; + } + + public String endpointComponentName(String uri) { + if (uri != null) { + int idx = uri.indexOf(":"); + if (idx > 0) { + return uri.substring(0, idx); + } + } + return null; + } + + public String asEndpointUri(String scheme, String json, boolean encode) throws URISyntaxException { + return doAsEndpointUri(scheme, json, "&", encode); + } + + public String asEndpointUriXml(String scheme, String json, boolean encode) throws URISyntaxException { + return doAsEndpointUri(scheme, json, "&", encode); + } + + private String doAsEndpointUri(String scheme, String json, String ampersand, boolean encode) throws URISyntaxException { + List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("properties", json, true); + + Map<String, String> copy = new HashMap<String, String>(); + for (Map<String, String> row : rows) { + String name = row.get("name"); + String required = row.get("required"); + String value = row.get("value"); + String defaultValue = row.get("defaultValue"); + + // only add if either required, or the value is != default value + String valueToAdd = null; + if ("true".equals(required)) { + valueToAdd = value != null ? value : defaultValue; + if (valueToAdd == null) { + valueToAdd = ""; + } + } else { + // if we have a value and no default then add it + if (value != null && defaultValue == null) { + valueToAdd = value; + } + // otherwise only add if the value is != default value + if (value != null && defaultValue != null && !value.equals(defaultValue)) { + valueToAdd = value; + } + } + + if (valueToAdd != null) { + copy.put(name, valueToAdd); + } + } + + return doAsEndpointUri(scheme, copy, ampersand, encode); + } + + public String asEndpointUri(String scheme, Map<String, String> properties, boolean encode) throws URISyntaxException { + return doAsEndpointUri(scheme, properties, "&", encode); + } + + public String asEndpointUriXml(String scheme, Map<String, String> properties, boolean encode) throws URISyntaxException { + return doAsEndpointUri(scheme, properties, "&", encode); + } + + private String doAsEndpointUri(String scheme, Map<String, String> properties, String ampersand, boolean encode) throws URISyntaxException { + String json = jsonSchemaResolver.getComponentJSonSchema(scheme); + if (json == null) { + throw new IllegalArgumentException("Cannot find endpoint with scheme " + scheme); + } + + // grab the syntax + String syntax = null; + List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("component", json, false); + for (Map<String, String> row : rows) { + if (row.containsKey("syntax")) { + syntax = row.get("syntax"); + break; + } + } + if (syntax == null) { + throw new IllegalArgumentException("Endpoint with scheme " + scheme + " has no syntax defined in the json schema"); + } + + // do any properties filtering which can be needed for some special components + properties = filterProperties(scheme, properties); + + rows = JSonSchemaHelper.parseJsonSchema("properties", json, true); + + // clip the scheme from the syntax + syntax = after(syntax, ":"); + + String originalSyntax = syntax; + + // build at first according to syntax (use a tree map as we want the uri options sorted) + Map<String, String> copy = new TreeMap<String, String>(); + for (Map.Entry<String, String> entry : properties.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue() != null ? entry.getValue() : ""; + if (syntax != null && syntax.contains(key)) { + syntax = syntax.replace(key, value); + } else { + copy.put(key, value); + } + } + + // the tokens between the options in the path + String[] tokens = syntax.split("\\w+"); + + // parse the syntax into each options + Matcher matcher = SYNTAX_PATTERN.matcher(originalSyntax); + List<String> options = new ArrayList<String>(); + 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.replaceAll("\\{\\{", "BEGINCAMELPLACEHOLDER"); + syntax = syntax.replaceAll("\\}\\}", "ENDCAMELPLACEHOLDER"); + + // parse the syntax into each options + Matcher matcher2 = SYNTAX_PATTERN.matcher(syntax); + List<String> options2 = new ArrayList<String>(); + while (matcher2.find()) { + String s = matcher2.group(1); + s = s.replaceAll("BEGINCAMELPLACEHOLDER", "\\{\\{"); + s = s.replaceAll("ENDCAMELPLACEHOLDER", "\\}\\}"); + options2.add(s); + } + + // build the endpoint + StringBuilder sb = new StringBuilder(); + sb.append(scheme); + sb.append(":"); + + int range = 0; + 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 + if (isPropertyRequired(rows, key)) { + String value = getPropertyDefaultValue(rows, key); + if (value != null) { + properties.put(key, value); + key2 = value; + } + } + } + + // was the option provided? + if (properties.containsKey(key)) { + if (!first && token != null) { + sb.append(token); + } + hasQuestionmark |= key.contains("?") || (token != null && token.contains("?")); + sb.append(key2); + 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++; + } + + if (!copy.isEmpty()) { + // the last option may already contain a ? char, if so we should use & instead of ? + sb.append(hasQuestionmark ? ampersand : '?'); + String query = createQueryString(copy, ampersand, encode); + sb.append(query); + } + + return sb.toString(); + } + + public SimpleValidationResult validateSimpleExpression(String simple) { + return doValidateSimple(null, simple, false); + } + + public SimpleValidationResult validateSimpleExpression(ClassLoader classLoader, String simple) { + return doValidateSimple(classLoader, simple, false); + } + + public SimpleValidationResult validateSimplePredicate(String simple) { + return doValidateSimple(null, simple, true); + } + + public SimpleValidationResult validateSimplePredicate(ClassLoader classLoader, String simple) { + return doValidateSimple(classLoader, simple, true); + } + + private SimpleValidationResult doValidateSimple(ClassLoader classLoader, String simple, boolean predicate) { + if (classLoader == null) { + classLoader = getClass().getClassLoader(); + } + + // if there are {{ }}} property placeholders then we need to resolve them to something else + // as the simple parse cannot resolve them before parsing as we dont run the actual Camel application + // with property placeholders setup so we need to dummy this by replace the {{ }} to something else + // therefore we use an more unlikely character: {{XXX}} to ~^XXX^~ + String resolved = simple.replaceAll("\\{\\{(.+)\\}\\}", "~^$1^~"); + + SimpleValidationResult answer = new SimpleValidationResult(simple); + + Object instance = null; + Class clazz = null; + try { + clazz = classLoader.loadClass("org.apache.camel.language.simple.SimpleLanguage"); + instance = clazz.newInstance(); + } catch (Exception e) { + // ignore + } + + if (clazz != null && instance != null) { + Throwable cause = null; + try { + if (predicate) { + instance.getClass().getMethod("createPredicate", String.class).invoke(instance, resolved); + } else { + instance.getClass().getMethod("createExpression", String.class).invoke(instance, resolved); + } + } catch (InvocationTargetException e) { + cause = e.getTargetException(); + } catch (Exception e) { + cause = e; + } + + if (cause != null) { + + // reverse ~^XXX^~ back to {{XXX}} + String errMsg = cause.getMessage(); + errMsg = errMsg.replaceAll("\\~\\^(.+)\\^\\~", "{{$1}}"); + + answer.setError(errMsg); + + // is it simple parser exception then we can grab the index where the problem is + if (cause.getClass().getName().equals("org.apache.camel.language.simple.types.SimpleIllegalSyntaxException") + || cause.getClass().getName().equals("org.apache.camel.language.simple.types.SimpleParserException")) { + try { + // we need to grab the index field from those simple parser exceptions + Method method = cause.getClass().getMethod("getIndex"); + Object result = method.invoke(cause); + if (result != null) { + int index = (int) result; + answer.setIndex(index); + } + } catch (Throwable i) { + // ignore + } + } + + // we need to grab the short message field from this simple syntax exception + if (cause.getClass().getName().equals("org.apache.camel.language.simple.types.SimpleIllegalSyntaxException")) { + try { + Method method = cause.getClass().getMethod("getShortMessage"); + Object result = method.invoke(cause); + if (result != null) { + String msg = (String) result; + answer.setShortError(msg); + } + } catch (Throwable i) { + // ignore + } + + if (answer.getShortError() == null) { + // fallback and try to make existing message short instead + String msg = answer.getError(); + // grab everything before " at location " which would be regarded as the short message + int idx = msg.indexOf(" at location "); + if (idx > 0) { + msg = msg.substring(0, idx); + answer.setShortError(msg); + } + } + } + } + } + + return answer; + } + + public LanguageValidationResult validateLanguagePredicate(ClassLoader classLoader, String language, String text) { + return doValidateLanguage(classLoader, language, text, true); + } + + public LanguageValidationResult validateLanguageExpression(ClassLoader classLoader, String language, String text) { + return doValidateLanguage(classLoader, language, text, false); + } + + private LanguageValidationResult doValidateLanguage(ClassLoader classLoader, String language, String text, boolean predicate) { + if (classLoader == null) { + classLoader = getClass().getClassLoader(); + } + + SimpleValidationResult answer = new SimpleValidationResult(text); + + String json = jsonSchemaResolver.getLanguageJSonSchema(language); + if (json == null) { + answer.setError("Unknown language " + language); + return answer; + } + + List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("language", json, false); + String className = null; + for (Map<String, String> row : rows) { + if (row.containsKey("javaType")) { + className = row.get("javaType"); + } + } + + if (className == null) { + answer.setError("Cannot find javaType for language " + language); + return answer; + } + + Object instance = null; + Class clazz = null; + try { + clazz = classLoader.loadClass(className); + instance = clazz.newInstance(); + } catch (Exception e) { + // ignore + } + + if (clazz != null && instance != null) { + Throwable cause = null; + try { + if (predicate) { + instance.getClass().getMethod("createPredicate", String.class).invoke(instance, text); + } else { + instance.getClass().getMethod("createExpression", String.class).invoke(instance, text); + } + } catch (InvocationTargetException e) { + cause = e.getTargetException(); + } catch (Exception e) { + cause = e; + } + + if (cause != null) { + answer.setError(cause.getMessage()); + } + } + + return answer; + } + + /** + * Special logic for log endpoints to deal when showAll=true + */ + private Map<String, String> filterProperties(String scheme, Map<String, String> options) { + if ("log".equals(scheme)) { + String showAll = options.get("showAll"); + if ("true".equals(showAll)) { + Map<String, String> filtered = new LinkedHashMap<String, String>(); + // remove all the other showXXX options when showAll=true + for (Map.Entry<String, String> entry : options.entrySet()) { + String key = entry.getKey(); + boolean skip = key.startsWith("show") && !key.equals("showAll"); + if (!skip) { + filtered.put(key, entry.getValue()); + } + } + return filtered; + } + } + // use as-is + return options; + } + + private static boolean validateInteger(String value) { + boolean valid = false; + try { + valid = Integer.valueOf(value) != null; + } catch (Exception e) { + // ignore + } + if (!valid) { + // it may be a time pattern, such as 5s for 5 seconds = 5000 + try { + TimePatternConverter.toMilliSeconds(value); + valid = true; + } catch (Exception e) { + // ignore + } + } + return valid; + } + + // CHECKSTYLE:ON + +} http://git-wip-us.apache.org/repos/asf/camel/blob/f5848e39/camel-core/src/main/java/org/apache/camel/catalog/CamelContextJSonSchemaResolver.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/catalog/CamelContextJSonSchemaResolver.java b/camel-core/src/main/java/org/apache/camel/catalog/CamelContextJSonSchemaResolver.java new file mode 100644 index 0000000..8d95488 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/catalog/CamelContextJSonSchemaResolver.java @@ -0,0 +1,80 @@ +/** + * 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.io.IOException; + +import org.apache.camel.CamelContext; + +/** + * Uses runtime {@link CamelContext} to resolve the JSon schema files. + */ +public class CamelContextJSonSchemaResolver implements JSonSchemaResolver { + + private final CamelContext camelContext; + + public CamelContextJSonSchemaResolver(CamelContext camelContext) { + this.camelContext = camelContext; + } + + @Override + public String getComponentJSonSchema(String name) { + try { + return camelContext.getComponentParameterJsonSchema(name); + } catch (IOException e) { + // ignore + } + return null; + } + + @Override + public String getDataFormatJSonSchema(String name) { + try { + return camelContext.getDataFormatParameterJsonSchema(name); + } catch (IOException e) { + // ignore + } + return null; + } + + @Override + public String getLanguageJSonSchema(String name) { + try { + return camelContext.getLanguageParameterJsonSchema(name); + } catch (IOException e) { + // ignore + } + return null; + } + + @Override + public String getOtherJSonSchema(String name) { + // not supported + return null; + } + + @Override + public String getModelJSonSchema(String name) { + try { + return camelContext.getEipParameterJsonSchema(name); + } catch (IOException e) { + // ignore + } + return null; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/f5848e39/camel-core/src/main/java/org/apache/camel/catalog/CatalogHelper.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/catalog/CatalogHelper.java b/camel-core/src/main/java/org/apache/camel/catalog/CatalogHelper.java new file mode 100644 index 0000000..f7c0072 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/catalog/CatalogHelper.java @@ -0,0 +1,195 @@ +/** + * 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.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.LineNumberReader; +import java.util.List; + +public final class CatalogHelper { + + private CatalogHelper() { + } + + /** + * Loads the entire stream into memory as a String and returns it. + * <p/> + * <b>Notice:</b> This implementation appends a <tt>\n</tt> as line + * terminator at the of the text. + * <p/> + * Warning, don't use for crazy big streams :) + */ + public static void loadLines(InputStream in, List<String> lines) throws IOException { + InputStreamReader isr = new InputStreamReader(in); + try { + BufferedReader reader = new LineNumberReader(isr); + while (true) { + String line = reader.readLine(); + if (line != null) { + lines.add(line); + } else { + break; + } + } + } finally { + isr.close(); + in.close(); + } + } + + /** + * Loads the entire stream into memory as a String and returns it. + * <p/> + * <b>Notice:</b> This implementation appends a <tt>\n</tt> as line + * terminator at the of the text. + * <p/> + * Warning, don't use for crazy big streams :) + */ + public static String loadText(InputStream in) throws IOException { + StringBuilder builder = new StringBuilder(); + InputStreamReader isr = new InputStreamReader(in); + try { + BufferedReader reader = new LineNumberReader(isr); + while (true) { + String line = reader.readLine(); + if (line != null) { + builder.append(line); + builder.append("\n"); + } else { + break; + } + } + return builder.toString(); + } finally { + isr.close(); + in.close(); + } + } + + /** + * Matches the name with the pattern. + * + * @param name the name + * @param pattern the pattern + * @return <tt>true</tt> if matched, or <tt>false</tt> if not + */ + public static boolean matchWildcard(String name, String pattern) { + // we have wildcard support in that hence you can match with: file* to match any file endpoints + if (pattern.endsWith("*") && name.startsWith(pattern.substring(0, pattern.length() - 1))) { + return true; + } + return false; + } + + /** + * Returns the string after the given token + * + * @param text the text + * @param after the token + * @return the text after the token, or <tt>null</tt> if text does not contain the token + */ + public static String after(String text, String after) { + if (!text.contains(after)) { + return null; + } + return text.substring(text.indexOf(after) + after.length()); + } + + /** + * Returns the string before the given token + * + * @param text the text + * @param before the token + * @return the text before the token, or <tt>null</tt> if text does not contain the token + */ + public static String before(String text, String before) { + if (!text.contains(before)) { + return null; + } + return text.substring(0, text.indexOf(before)); + } + + /** + * Returns the string between the given tokens + * + * @param text the text + * @param after the before token + * @param before the after token + * @return the text between the tokens, or <tt>null</tt> if text does not contain the tokens + */ + public static String between(String text, String after, String before) { + text = after(text, after); + if (text == null) { + return null; + } + return before(text, before); + } + + /** + * Tests whether the value is <tt>null</tt> or an empty string. + * + * @param value the value, if its a String it will be tested for text length as well + * @return true if empty + */ + public static boolean isEmpty(Object value) { + return !isNotEmpty(value); + } + + /** + * Tests whether the value is <b>not</b> <tt>null</tt> or an empty string. + * + * @param value the value, if its a String it will be tested for text length as well + * @return true if <b>not</b> empty + */ + public static boolean isNotEmpty(Object value) { + if (value == null) { + return false; + } else if (value instanceof String) { + String text = (String) value; + return text.trim().length() > 0; + } else { + return true; + } + } + + /** + * Removes all leading and ending quotes (single and double) from the string + * + * @param s the string + * @return the string without leading and ending quotes (single and double) + */ + public static String removeLeadingAndEndingQuotes(String s) { + if (isEmpty(s)) { + return s; + } + + String copy = s.trim(); + if (copy.startsWith("'") && copy.endsWith("'")) { + return copy.substring(1, copy.length() - 1); + } + if (copy.startsWith("\"") && copy.endsWith("\"")) { + return copy.substring(1, copy.length() - 1); + } + + // no quotes, so return as-is + return s; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/f5848e39/camel-core/src/main/java/org/apache/camel/catalog/CollectionStringBuffer.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/catalog/CollectionStringBuffer.java b/camel-core/src/main/java/org/apache/camel/catalog/CollectionStringBuffer.java new file mode 100644 index 0000000..2844ca9 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/catalog/CollectionStringBuffer.java @@ -0,0 +1,57 @@ +/** + * 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; + +public class CollectionStringBuffer { + private final StringBuilder buffer = new StringBuilder(); + private String separator; + private boolean first = true; + + public CollectionStringBuffer() { + this(", "); + } + + public CollectionStringBuffer(String separator) { + this.separator = separator; + } + + @Override + public String toString() { + return buffer.toString(); + } + + public void append(Object value) { + if (first) { + first = false; + } else { + buffer.append(separator); + } + buffer.append(value); + } + + public String getSeparator() { + return separator; + } + + public void setSeparator(String separator) { + this.separator = separator; + } + + public boolean isEmpty() { + return first; + } +} http://git-wip-us.apache.org/repos/asf/camel/blob/f5848e39/camel-core/src/main/java/org/apache/camel/catalog/DefaultRuntimeCamelCatalog.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/catalog/DefaultRuntimeCamelCatalog.java b/camel-core/src/main/java/org/apache/camel/catalog/DefaultRuntimeCamelCatalog.java new file mode 100644 index 0000000..8579849 --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/catalog/DefaultRuntimeCamelCatalog.java @@ -0,0 +1,126 @@ +/** + * 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.HashMap; +import java.util.Map; + +import org.apache.camel.CamelContext; + +/** + * Default {@link RuntimeCamelCatalog}. + */ +public class DefaultRuntimeCamelCatalog extends AbstractCamelCatalog implements RuntimeCamelCatalog { + + // cache of operation -> result + private final Map<String, Object> cache = new HashMap<String, Object>(); + private boolean caching; + + /** + * Creates the {@link RuntimeCamelCatalog} without caching enabled. + * + * @param camelContext the camel context + */ + public DefaultRuntimeCamelCatalog(CamelContext camelContext) { + this(camelContext, false); + } + + /** + * Creates the {@link RuntimeCamelCatalog} + * + * @param camelContext the camel context + * @param caching whether to use cache + */ + public DefaultRuntimeCamelCatalog(CamelContext camelContext, boolean caching) { + this.caching = caching; + setJSonSchemaResolver(new CamelContextJSonSchemaResolver(camelContext)); + } + + @Override + public String modelJSonSchema(String name) { + String answer = null; + if (caching) { + answer = (String) cache.get("model-" + name); + } + + if (answer == null) { + answer = getJSonSchemaResolver().getModelJSonSchema(name); + if (caching) { + cache.put("model-" + name, answer); + } + } + + return answer; + } + + @Override + public String componentJSonSchema(String name) { + String answer = null; + if (caching) { + answer = (String) cache.get("component-" + name); + } + + if (answer == null) { + answer = getJSonSchemaResolver().getComponentJSonSchema(name); + if (caching) { + cache.put("component-" + name, answer); + } + } + + return answer; + } + + @Override + public String dataFormatJSonSchema(String name) { + String answer = null; + if (caching) { + answer = (String) cache.get("dataformat-" + name); + } + + if (answer == null) { + answer = getJSonSchemaResolver().getDataFormatJSonSchema(name); + if (caching) { + cache.put("dataformat-" + name, answer); + } + } + + return answer; + } + + @Override + public String languageJSonSchema(String name) { + // if we try to look method then its in the bean.json file + if ("method".equals(name)) { + name = "bean"; + } + + String answer = null; + if (caching) { + answer = (String) cache.get("language-" + name); + } + + if (answer == null) { + answer = getJSonSchemaResolver().getLanguageJSonSchema(name); + if (caching) { + cache.put("language-" + name, answer); + } + } + + return answer; + } + +} http://git-wip-us.apache.org/repos/asf/camel/blob/f5848e39/camel-core/src/main/java/org/apache/camel/catalog/EndpointValidationResult.java ---------------------------------------------------------------------- diff --git a/camel-core/src/main/java/org/apache/camel/catalog/EndpointValidationResult.java b/camel-core/src/main/java/org/apache/camel/catalog/EndpointValidationResult.java new file mode 100644 index 0000000..11e2c5e --- /dev/null +++ b/camel-core/src/main/java/org/apache/camel/catalog/EndpointValidationResult.java @@ -0,0 +1,426 @@ +/** + * 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.io.Serializable; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import static org.apache.camel.catalog.URISupport.isEmpty; + +/** + * Details result of validating endpoint uri. + */ +public class EndpointValidationResult implements Serializable { + + private final String uri; + private int errors; + + // general + private String syntaxError; + private String unknownComponent; + private String incapable; + + // options + private Set<String> unknown; + private Map<String, String[]> unknownSuggestions; + private Set<String> lenient; + private Set<String> notConsumerOnly; + private Set<String> notProducerOnly; + private Set<String> required; + private Map<String, String> invalidEnum; + private Map<String, String[]> invalidEnumChoices; + private Map<String, String[]> invalidEnumSuggestions; + private Map<String, String> invalidReference; + private Map<String, String> invalidBoolean; + private Map<String, String> invalidInteger; + private Map<String, String> invalidNumber; + private Map<String, String> defaultValues; + + public EndpointValidationResult(String uri) { + this.uri = uri; + } + + public String getUri() { + return uri; + } + + public int getNumberOfErrors() { + return errors; + } + + public boolean isSuccess() { + boolean ok = syntaxError == null && unknownComponent == null && incapable == null + && unknown == null && required == null; + if (ok) { + ok = notConsumerOnly == null && notProducerOnly == null; + } + if (ok) { + ok = invalidEnum == null && invalidEnumChoices == null && invalidReference == null + && invalidBoolean == null && invalidInteger == null && invalidNumber == null; + } + return ok; + } + + public void addSyntaxError(String syntaxError) { + this.syntaxError = syntaxError; + errors++; + } + + public void addIncapable(String uri) { + this.incapable = uri; + errors++; + } + + public void addUnknownComponent(String name) { + this.unknownComponent = name; + errors++; + } + + public void addUnknown(String name) { + if (unknown == null) { + unknown = new LinkedHashSet<String>(); + } + if (!unknown.contains(name)) { + unknown.add(name); + errors++; + } + } + + public void addUnknownSuggestions(String name, String[] suggestions) { + if (unknownSuggestions == null) { + unknownSuggestions = new LinkedHashMap<String, String[]>(); + } + unknownSuggestions.put(name, suggestions); + } + + public void addLenient(String name) { + if (lenient == null) { + lenient = new LinkedHashSet<String>(); + } + if (!lenient.contains(name)) { + lenient.add(name); + } + } + + public void addRequired(String name) { + if (required == null) { + required = new LinkedHashSet<String>(); + } + if (!required.contains(name)) { + required.add(name); + errors++; + } + } + + public void addInvalidEnum(String name, String value) { + if (invalidEnum == null) { + invalidEnum = new LinkedHashMap<String, String>(); + } + if (!invalidEnum.containsKey(name)) { + invalidEnum.put(name, value); + errors++; + } + } + + public void addInvalidEnumChoices(String name, String[] choices) { + if (invalidEnumChoices == null) { + invalidEnumChoices = new LinkedHashMap<String, String[]>(); + } + invalidEnumChoices.put(name, choices); + } + + public void addInvalidEnumSuggestions(String name, String[] suggestions) { + if (invalidEnumSuggestions == null) { + invalidEnumSuggestions = new LinkedHashMap<String, String[]>(); + } + invalidEnumSuggestions.put(name, suggestions); + } + + public void addInvalidReference(String name, String value) { + if (invalidReference == null) { + invalidReference = new LinkedHashMap<String, String>(); + } + if (!invalidReference.containsKey(name)) { + invalidReference.put(name, value); + errors++; + } + } + + public void addInvalidBoolean(String name, String value) { + if (invalidBoolean == null) { + invalidBoolean = new LinkedHashMap<String, String>(); + } + if (!invalidBoolean.containsKey(name)) { + invalidBoolean.put(name, value); + errors++; + } + } + + public void addInvalidInteger(String name, String value) { + if (invalidInteger == null) { + invalidInteger = new LinkedHashMap<String, String>(); + } + if (!invalidInteger.containsKey(name)) { + invalidInteger.put(name, value); + errors++; + } + } + + public void addInvalidNumber(String name, String value) { + if (invalidNumber == null) { + invalidNumber = new LinkedHashMap<String, String>(); + } + if (!invalidNumber.containsKey(name)) { + invalidNumber.put(name, value); + errors++; + } + } + + public void addDefaultValue(String name, String value) { + if (defaultValues == null) { + defaultValues = new LinkedHashMap<String, String>(); + } + defaultValues.put(name, value); + } + + public void addNotConsumerOnly(String name) { + if (notConsumerOnly == null) { + notConsumerOnly = new LinkedHashSet<String>(); + } + if (!notConsumerOnly.contains(name)) { + notConsumerOnly.add(name); + errors++; + } + } + + public void addNotProducerOnly(String name) { + if (notProducerOnly == null) { + notProducerOnly = new LinkedHashSet<String>(); + } + if (!notProducerOnly.contains(name)) { + notProducerOnly.add(name); + errors++; + } + } + + public String getSyntaxError() { + return syntaxError; + } + + public String getIncapable() { + return incapable; + } + + public Set<String> getUnknown() { + return unknown; + } + + public Set<String> getLenient() { + return lenient; + } + + public Map<String, String[]> getUnknownSuggestions() { + return unknownSuggestions; + } + + public String getUnknownComponent() { + return unknownComponent; + } + + public Set<String> getRequired() { + return required; + } + + public Map<String, String> getInvalidEnum() { + return invalidEnum; + } + + public Map<String, String[]> getInvalidEnumChoices() { + return invalidEnumChoices; + } + + public Map<String, String> getInvalidReference() { + return invalidReference; + } + + public Map<String, String> getInvalidBoolean() { + return invalidBoolean; + } + + public Map<String, String> getInvalidInteger() { + return invalidInteger; + } + + public Map<String, String> getInvalidNumber() { + return invalidNumber; + } + + public Map<String, String> getDefaultValues() { + return defaultValues; + } + + public Set<String> getNotConsumerOnly() { + return notConsumerOnly; + } + + public Set<String> getNotProducerOnly() { + return notProducerOnly; + } + + /** + * A human readable summary of the validation errors. + * + * @param includeHeader whether to include a header + * @return the summary, or <tt>null</tt> if no validation errors + */ + public String summaryErrorMessage(boolean includeHeader) { + if (isSuccess()) { + return null; + } + + if (incapable != null) { + return "\tIncapable of parsing uri: " + incapable; + } else if (syntaxError != null) { + return "\tSyntax error: " + syntaxError; + } else if (unknownComponent != null) { + return "\tUnknown component: " + unknownComponent; + } + + // for each invalid option build a reason message + Map<String, String> options = new LinkedHashMap<String, String>(); + if (unknown != null) { + for (String name : unknown) { + if (unknownSuggestions != null && unknownSuggestions.containsKey(name)) { + String[] suggestions = unknownSuggestions.get(name); + if (suggestions != null && suggestions.length > 0) { + String str = Arrays.asList(suggestions).toString(); + options.put(name, "Unknown option. Did you mean: " + str); + } else { + options.put(name, "Unknown option"); + } + } else { + options.put(name, "Unknown option"); + } + } + } + if (notConsumerOnly != null) { + for (String name : notConsumerOnly) { + options.put(name, "Option not applicable in consumer only mode"); + } + } + if (notProducerOnly != null) { + for (String name : notProducerOnly) { + options.put(name, "Option not applicable in producer only mode"); + } + } + if (required != null) { + for (String name : required) { + options.put(name, "Missing required option"); + } + } + if (invalidEnum != null) { + for (Map.Entry<String, String> entry : invalidEnum.entrySet()) { + String name = entry.getKey(); + String[] choices = invalidEnumChoices.get(name); + String defaultValue = defaultValues != null ? defaultValues.get(entry.getKey()) : null; + String str = Arrays.asList(choices).toString(); + String msg = "Invalid enum value: " + entry.getValue() + ". Possible values: " + str; + if (invalidEnumSuggestions != null) { + String[] suggestions = invalidEnumSuggestions.get(name); + if (suggestions != null && suggestions.length > 0) { + str = Arrays.asList(suggestions).toString(); + msg += ". Did you mean: " + str; + } + } + if (defaultValue != null) { + msg += ". Default value: " + defaultValue; + } + + options.put(entry.getKey(), msg); + } + } + if (invalidReference != null) { + for (Map.Entry<String, String> entry : invalidReference.entrySet()) { + boolean empty = isEmpty(entry.getValue()); + if (empty) { + options.put(entry.getKey(), "Empty reference value"); + } else if (!entry.getValue().startsWith("#")) { + options.put(entry.getKey(), "Invalid reference value: " + entry.getValue() + " must start with #"); + } else { + options.put(entry.getKey(), "Invalid reference value: " + entry.getValue()); + } + } + } + if (invalidBoolean != null) { + for (Map.Entry<String, String> entry : invalidBoolean.entrySet()) { + boolean empty = isEmpty(entry.getValue()); + if (empty) { + options.put(entry.getKey(), "Empty boolean value"); + } else { + options.put(entry.getKey(), "Invalid boolean value: " + entry.getValue()); + } + } + } + if (invalidInteger != null) { + for (Map.Entry<String, String> entry : invalidInteger.entrySet()) { + boolean empty = isEmpty(entry.getValue()); + if (empty) { + options.put(entry.getKey(), "Empty integer value"); + } else { + options.put(entry.getKey(), "Invalid integer value: " + entry.getValue()); + } + } + } + if (invalidNumber != null) { + for (Map.Entry<String, String> entry : invalidNumber.entrySet()) { + boolean empty = isEmpty(entry.getValue()); + if (empty) { + options.put(entry.getKey(), "Empty number value"); + } else { + options.put(entry.getKey(), "Invalid number value: " + entry.getValue()); + } + } + } + + // build a table with the error summary nicely formatted + // lets use 24 as min length + int maxLen = 24; + for (String key : options.keySet()) { + maxLen = Math.max(maxLen, key.length()); + } + String format = "%" + maxLen + "s %s"; + + // build the human error summary + StringBuilder sb = new StringBuilder(); + if (includeHeader) { + sb.append("Endpoint validator error\n"); + sb.append("---------------------------------------------------------------------------------------------------------------------------------------\n"); + sb.append("\n"); + } + sb.append("\t").append(uri).append("\n"); + for (Map.Entry<String, String> option : options.entrySet()) { + String out = String.format(format, option.getKey(), option.getValue()); + sb.append("\n\t").append(out); + } + + return sb.toString(); + } +}