001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.runtimecatalog;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.net.URI;
022import java.net.URISyntaxException;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.HashMap;
026import java.util.Iterator;
027import java.util.LinkedHashMap;
028import java.util.LinkedHashSet;
029import java.util.List;
030import java.util.Map;
031import java.util.Objects;
032import java.util.Set;
033import java.util.TreeMap;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036
037import static org.apache.camel.runtimecatalog.CatalogHelper.after;
038import static org.apache.camel.runtimecatalog.JSonSchemaHelper.getNames;
039import static org.apache.camel.runtimecatalog.JSonSchemaHelper.getPropertyDefaultValue;
040import static org.apache.camel.runtimecatalog.JSonSchemaHelper.getPropertyEnum;
041import static org.apache.camel.runtimecatalog.JSonSchemaHelper.getPropertyKind;
042import static org.apache.camel.runtimecatalog.JSonSchemaHelper.getPropertyNameFromNameWithPrefix;
043import static org.apache.camel.runtimecatalog.JSonSchemaHelper.getPropertyPrefix;
044import static org.apache.camel.runtimecatalog.JSonSchemaHelper.getRow;
045import static org.apache.camel.runtimecatalog.JSonSchemaHelper.isComponentConsumerOnly;
046import static org.apache.camel.runtimecatalog.JSonSchemaHelper.isComponentLenientProperties;
047import static org.apache.camel.runtimecatalog.JSonSchemaHelper.isComponentProducerOnly;
048import static org.apache.camel.runtimecatalog.JSonSchemaHelper.isPropertyBoolean;
049import static org.apache.camel.runtimecatalog.JSonSchemaHelper.isPropertyConsumerOnly;
050import static org.apache.camel.runtimecatalog.JSonSchemaHelper.isPropertyDeprecated;
051import static org.apache.camel.runtimecatalog.JSonSchemaHelper.isPropertyInteger;
052import static org.apache.camel.runtimecatalog.JSonSchemaHelper.isPropertyMultiValue;
053import static org.apache.camel.runtimecatalog.JSonSchemaHelper.isPropertyNumber;
054import static org.apache.camel.runtimecatalog.JSonSchemaHelper.isPropertyObject;
055import static org.apache.camel.runtimecatalog.JSonSchemaHelper.isPropertyProducerOnly;
056import static org.apache.camel.runtimecatalog.JSonSchemaHelper.isPropertyRequired;
057import static org.apache.camel.runtimecatalog.JSonSchemaHelper.stripOptionalPrefixFromName;
058import static org.apache.camel.runtimecatalog.URISupport.createQueryString;
059import static org.apache.camel.runtimecatalog.URISupport.isEmpty;
060import static org.apache.camel.runtimecatalog.URISupport.normalizeUri;
061import static org.apache.camel.runtimecatalog.URISupport.stripQuery;
062
063/**
064 * Base class for both the runtime RuntimeCamelCatalog from camel-core and the complete CamelCatalog from camel-catalog.
065 */
066public abstract class AbstractCamelCatalog {
067
068    // CHECKSTYLE:OFF
069
070    private static final Pattern SYNTAX_PATTERN = Pattern.compile("([\\w.]+)");
071    private static final Pattern COMPONENT_SYNTAX_PARSER = Pattern.compile("([^\\w-]*)([\\w-]+)");
072
073    private SuggestionStrategy suggestionStrategy;
074    private JSonSchemaResolver jsonSchemaResolver;
075
076    public SuggestionStrategy getSuggestionStrategy() {
077        return suggestionStrategy;
078    }
079
080    public void setSuggestionStrategy(SuggestionStrategy suggestionStrategy) {
081        this.suggestionStrategy = suggestionStrategy;
082    }
083
084    public JSonSchemaResolver getJSonSchemaResolver() {
085        return jsonSchemaResolver;
086    }
087
088    public void setJSonSchemaResolver(JSonSchemaResolver resolver) {
089        this.jsonSchemaResolver = resolver;
090    }
091
092    public boolean validateTimePattern(String pattern) {
093        return validateInteger(pattern);
094    }
095
096    public EndpointValidationResult validateEndpointProperties(String uri) {
097        return validateEndpointProperties(uri, false, false, false);
098    }
099
100    public EndpointValidationResult validateEndpointProperties(String uri, boolean ignoreLenientProperties) {
101        return validateEndpointProperties(uri, ignoreLenientProperties, false, false);
102    }
103
104    public EndpointValidationResult validateProperties(String scheme, Map<String, String> properties) {
105        EndpointValidationResult result = new EndpointValidationResult(scheme);
106
107        String json = jsonSchemaResolver.getComponentJSonSchema(scheme);
108        List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("properties", json, true);
109        List<Map<String, String>> componentProps = JSonSchemaHelper.parseJsonSchema("componentProperties", json, true);
110
111        // endpoint options have higher priority so remove those from component
112        // that may clash
113        componentProps.stream()
114            .filter(c -> rows.stream().noneMatch(e -> Objects.equals(e.get("name"), c.get("name"))))
115            .forEach(rows::add);
116
117        boolean lenient = Boolean.getBoolean(properties.getOrDefault("lenient", "false"));
118
119        // the dataformat component refers to a data format so lets add the properties for the selected
120        // data format to the list of rows
121        if ("dataformat".equals(scheme)) {
122            String dfName = properties.get("name");
123            if (dfName != null) {
124                String dfJson = jsonSchemaResolver.getDataFormatJSonSchema(dfName);
125                List<Map<String, String>> dfRows = JSonSchemaHelper.parseJsonSchema("properties", dfJson, true);
126                if (dfRows != null && !dfRows.isEmpty()) {
127                    rows.addAll(dfRows);
128                }
129            }
130        }
131
132        for (Map.Entry<String, String> property : properties.entrySet()) {
133            String value = property.getValue();
134            String originalName = property.getKey();
135            String name = property.getKey();
136            // the name may be using an optional prefix, so lets strip that because the options
137            // in the schema are listed without the prefix
138            name = stripOptionalPrefixFromName(rows, name);
139            // the name may be using a prefix, so lets see if we can find the real property name
140            String propertyName = getPropertyNameFromNameWithPrefix(rows, name);
141            if (propertyName != null) {
142                name = propertyName;
143            }
144
145            String prefix = getPropertyPrefix(rows, name);
146            String kind = getPropertyKind(rows, name);
147            boolean namePlaceholder = name.startsWith("{{") && name.endsWith("}}");
148            boolean valuePlaceholder = value.startsWith("{{") || value.startsWith("${") || value.startsWith("$simple{");
149            boolean lookup = value.startsWith("#") && value.length() > 1;
150            // we cannot evaluate multi values as strict as the others, as we don't know their expected types
151            boolean multiValue = prefix != null && originalName.startsWith(prefix) && isPropertyMultiValue(rows, name);
152
153            Map<String, String> row = getRow(rows, name);
154            if (row == null) {
155                // unknown option
156
157                // only add as error if the component is not lenient properties, or not stub component
158                // and the name is not a property placeholder for one or more values
159                if (!namePlaceholder && !"stub".equals(scheme)) {
160                    if (lenient) {
161                        // as if we are lenient then the option is a dynamic extra option which we cannot validate
162                        result.addLenient(name);
163                    } else {
164                        // its unknown
165                        result.addUnknown(name);
166                        if (suggestionStrategy != null) {
167                            String[] suggestions = suggestionStrategy.suggestEndpointOptions(getNames(rows), name);
168                            if (suggestions != null) {
169                                result.addUnknownSuggestions(name, suggestions);
170                            }
171                        }
172                    }
173                }
174            } else {
175                /* TODO: we may need to add something in the properties to know if they are related to a producer or consumer
176                if ("parameter".equals(kind)) {
177                    // consumer only or producer only mode for parameters
178                    if (consumerOnly) {
179                        boolean producer = isPropertyProducerOnly(rows, name);
180                        if (producer) {
181                            // the option is only for producer so you cannot use it in consumer mode
182                            result.addNotConsumerOnly(name);
183                        }
184                    } else if (producerOnly) {
185                        boolean consumer = isPropertyConsumerOnly(rows, name);
186                        if (consumer) {
187                            // the option is only for consumer so you cannot use it in producer mode
188                            result.addNotProducerOnly(name);
189                        }
190                    }
191                }
192                */
193
194                // default value
195                String defaultValue = getPropertyDefaultValue(rows, name);
196                if (defaultValue != null) {
197                    result.addDefaultValue(name, defaultValue);
198                }
199
200                // is required but the value is empty
201                boolean required = isPropertyRequired(rows, name);
202                if (required && isEmpty(value)) {
203                    result.addRequired(name);
204                }
205
206                // is the option deprecated
207                boolean deprecated = isPropertyDeprecated(rows, name);
208                if (deprecated) {
209                    result.addDeprecated(name);
210                }
211
212                // is enum but the value is not within the enum range
213                // but we can only check if the value is not a placeholder
214                String enums = getPropertyEnum(rows, name);
215                if (!multiValue && !valuePlaceholder && !lookup && enums != null) {
216                    String[] choices = enums.split(",");
217                    boolean found = false;
218                    for (String s : choices) {
219                        if (value.equalsIgnoreCase(s)) {
220                            found = true;
221                            break;
222                        }
223                    }
224                    if (!found) {
225                        result.addInvalidEnum(name, value);
226                        result.addInvalidEnumChoices(name, choices);
227                        if (suggestionStrategy != null) {
228                            Set<String> names = new LinkedHashSet<>();
229                            names.addAll(Arrays.asList(choices));
230                            String[] suggestions = suggestionStrategy.suggestEndpointOptions(names, value);
231                            if (suggestions != null) {
232                                result.addInvalidEnumSuggestions(name, suggestions);
233                            }
234                        }
235
236                    }
237                }
238
239                // is reference lookup of bean (not applicable for @UriPath, enums, or multi-valued)
240                if (!multiValue && enums == null && !"path".equals(kind) && isPropertyObject(rows, name)) {
241                    // must start with # and be at least 2 characters
242                    if (!value.startsWith("#") || value.length() <= 1) {
243                        result.addInvalidReference(name, value);
244                    }
245                }
246
247                // is boolean
248                if (!multiValue && !valuePlaceholder && !lookup && isPropertyBoolean(rows, name)) {
249                    // value must be a boolean
250                    boolean bool = "true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value);
251                    if (!bool) {
252                        result.addInvalidBoolean(name, value);
253                    }
254                }
255
256                // is integer
257                if (!multiValue && !valuePlaceholder && !lookup && isPropertyInteger(rows, name)) {
258                    // value must be an integer
259                    boolean valid = validateInteger(value);
260                    if (!valid) {
261                        result.addInvalidInteger(name, value);
262                    }
263                }
264
265                // is number
266                if (!multiValue && !valuePlaceholder && !lookup && isPropertyNumber(rows, name)) {
267                    // value must be an number
268                    boolean valid = false;
269                    try {
270                        valid = !Double.valueOf(value).isNaN() || !Float.valueOf(value).isNaN();
271                    } catch (Exception e) {
272                        // ignore
273                    }
274                    if (!valid) {
275                        result.addInvalidNumber(name, value);
276                    }
277                }
278            }
279        }
280
281        // now check if all required values are there, and that a default value does not exists
282        for (Map<String, String> row : rows) {
283            String name = row.get("name");
284            boolean required = isPropertyRequired(rows, name);
285            if (required) {
286                String value = properties.get(name);
287                if (isEmpty(value)) {
288                    value = getPropertyDefaultValue(rows, name);
289                }
290                if (isEmpty(value)) {
291                    result.addRequired(name);
292                }
293            }
294        }
295
296        return result;
297    }
298
299    public EndpointValidationResult validateEndpointProperties(String uri, boolean ignoreLenientProperties, boolean consumerOnly, boolean producerOnly) {
300        EndpointValidationResult result = new EndpointValidationResult(uri);
301
302        Map<String, String> properties;
303        List<Map<String, String>> rows;
304        boolean lenientProperties;
305        String scheme;
306
307        try {
308            String json = null;
309
310            // parse the uri
311            URI u = normalizeUri(uri);
312            scheme = u.getScheme();
313
314            if (scheme != null) {
315                json = jsonSchemaResolver.getComponentJSonSchema(scheme);
316            }
317            if (json == null) {
318                // 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
319                if (uri.startsWith("{{")) {
320                    result.addIncapable(uri);
321                } else if (scheme != null) {
322                    result.addUnknownComponent(scheme);
323                } else {
324                    result.addUnknownComponent(uri);
325                }
326                return result;
327            }
328
329            rows = JSonSchemaHelper.parseJsonSchema("component", json, false);
330
331            // is the component capable of both consumer and producer?
332            boolean canConsumeAndProduce = false;
333            if (!isComponentConsumerOnly(rows) && !isComponentProducerOnly(rows)) {
334                canConsumeAndProduce = true;
335            }
336
337            if (canConsumeAndProduce && consumerOnly) {
338                // lenient properties is not support in consumer only mode if the component can do both of them
339                lenientProperties = false;
340            } else {
341                // only enable lenient properties if we should not ignore
342                lenientProperties = !ignoreLenientProperties && isComponentLenientProperties(rows);
343            }
344            rows = JSonSchemaHelper.parseJsonSchema("properties", json, true);
345            properties = endpointProperties(uri);
346        } catch (URISyntaxException e) {
347            if (uri.startsWith("{{")) {
348                // 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
349                result.addIncapable(uri);
350            } else {
351                result.addSyntaxError(e.getMessage());
352            }
353
354            return result;
355        }
356
357        // the dataformat component refers to a data format so lets add the properties for the selected
358        // data format to the list of rows
359        if ("dataformat".equals(scheme)) {
360            String dfName = properties.get("name");
361            if (dfName != null) {
362                String dfJson = jsonSchemaResolver.getDataFormatJSonSchema(dfName);
363                List<Map<String, String>> dfRows = JSonSchemaHelper.parseJsonSchema("properties", dfJson, true);
364                if (dfRows != null && !dfRows.isEmpty()) {
365                    rows.addAll(dfRows);
366                }
367            }
368        }
369
370        for (Map.Entry<String, String> property : properties.entrySet()) {
371            String value = property.getValue();
372            String originalName = property.getKey();
373            String name = property.getKey();
374            // the name may be using an optional prefix, so lets strip that because the options
375            // in the schema are listed without the prefix
376            name = stripOptionalPrefixFromName(rows, name);
377            // the name may be using a prefix, so lets see if we can find the real property name
378            String propertyName = getPropertyNameFromNameWithPrefix(rows, name);
379            if (propertyName != null) {
380                name = propertyName;
381            }
382
383            String prefix = getPropertyPrefix(rows, name);
384            String kind = getPropertyKind(rows, name);
385            boolean namePlaceholder = name.startsWith("{{") && name.endsWith("}}");
386            boolean valuePlaceholder = value.startsWith("{{") || value.startsWith("${") || value.startsWith("$simple{");
387            boolean lookup = value.startsWith("#") && value.length() > 1;
388            // we cannot evaluate multi values as strict as the others, as we don't know their expected types
389            boolean mulitValue = prefix != null && originalName.startsWith(prefix) && isPropertyMultiValue(rows, name);
390
391            Map<String, String> row = getRow(rows, name);
392            if (row == null) {
393                // unknown option
394
395                // only add as error if the component is not lenient properties, or not stub component
396                // and the name is not a property placeholder for one or more values
397                if (!namePlaceholder && !"stub".equals(scheme)) {
398                    if (lenientProperties) {
399                        // as if we are lenient then the option is a dynamic extra option which we cannot validate
400                        result.addLenient(name);
401                    } else {
402                        // its unknown
403                        result.addUnknown(name);
404                        if (suggestionStrategy != null) {
405                            String[] suggestions = suggestionStrategy.suggestEndpointOptions(getNames(rows), name);
406                            if (suggestions != null) {
407                                result.addUnknownSuggestions(name, suggestions);
408                            }
409                        }
410                    }
411                }
412            } else {
413                if ("parameter".equals(kind)) {
414                    // consumer only or producer only mode for parameters
415                    if (consumerOnly) {
416                        boolean producer = isPropertyProducerOnly(rows, name);
417                        if (producer) {
418                            // the option is only for producer so you cannot use it in consumer mode
419                            result.addNotConsumerOnly(name);
420                        }
421                    } else if (producerOnly) {
422                        boolean consumer = isPropertyConsumerOnly(rows, name);
423                        if (consumer) {
424                            // the option is only for consumer so you cannot use it in producer mode
425                            result.addNotProducerOnly(name);
426                        }
427                    }
428                }
429
430                // default value
431                String defaultValue = getPropertyDefaultValue(rows, name);
432                if (defaultValue != null) {
433                    result.addDefaultValue(name, defaultValue);
434                }
435
436                // is required but the value is empty
437                boolean required = isPropertyRequired(rows, name);
438                if (required && isEmpty(value)) {
439                    result.addRequired(name);
440                }
441
442                // is the option deprecated
443                boolean deprecated = isPropertyDeprecated(rows, name);
444                if (deprecated) {
445                    result.addDeprecated(name);
446                }
447
448                // is enum but the value is not within the enum range
449                // but we can only check if the value is not a placeholder
450                String enums = getPropertyEnum(rows, name);
451                if (!mulitValue && !valuePlaceholder && !lookup && enums != null) {
452                    String[] choices = enums.split(",");
453                    boolean found = false;
454                    for (String s : choices) {
455                        if (value.equalsIgnoreCase(s)) {
456                            found = true;
457                            break;
458                        }
459                    }
460                    if (!found) {
461                        result.addInvalidEnum(name, value);
462                        result.addInvalidEnumChoices(name, choices);
463                        if (suggestionStrategy != null) {
464                            Set<String> names = new LinkedHashSet<>();
465                            names.addAll(Arrays.asList(choices));
466                            String[] suggestions = suggestionStrategy.suggestEndpointOptions(names, value);
467                            if (suggestions != null) {
468                                result.addInvalidEnumSuggestions(name, suggestions);
469                            }
470                        }
471
472                    }
473                }
474
475                // is reference lookup of bean (not applicable for @UriPath, enums, or multi-valued)
476                if (!mulitValue && enums == null && !"path".equals(kind) && isPropertyObject(rows, name)) {
477                    // must start with # and be at least 2 characters
478                    if (!value.startsWith("#") || value.length() <= 1) {
479                        result.addInvalidReference(name, value);
480                    }
481                }
482
483                // is boolean
484                if (!mulitValue && !valuePlaceholder && !lookup && isPropertyBoolean(rows, name)) {
485                    // value must be a boolean
486                    boolean bool = "true".equalsIgnoreCase(value) || "false".equalsIgnoreCase(value);
487                    if (!bool) {
488                        result.addInvalidBoolean(name, value);
489                    }
490                }
491
492                // is integer
493                if (!mulitValue && !valuePlaceholder && !lookup && isPropertyInteger(rows, name)) {
494                    // value must be an integer
495                    boolean valid = validateInteger(value);
496                    if (!valid) {
497                        result.addInvalidInteger(name, value);
498                    }
499                }
500
501                // is number
502                if (!mulitValue && !valuePlaceholder && !lookup && isPropertyNumber(rows, name)) {
503                    // value must be an number
504                    boolean valid = false;
505                    try {
506                        valid = !Double.valueOf(value).isNaN() || !Float.valueOf(value).isNaN();
507                    } catch (Exception e) {
508                        // ignore
509                    }
510                    if (!valid) {
511                        result.addInvalidNumber(name, value);
512                    }
513                }
514            }
515        }
516
517        // now check if all required values are there, and that a default value does not exists
518        for (Map<String, String> row : rows) {
519            String name = row.get("name");
520            boolean required = isPropertyRequired(rows, name);
521            if (required) {
522                String value = properties.get(name);
523                if (isEmpty(value)) {
524                    value = getPropertyDefaultValue(rows, name);
525                }
526                if (isEmpty(value)) {
527                    result.addRequired(name);
528                }
529            }
530        }
531
532        return result;
533    }
534
535    public Map<String, String> endpointProperties(String uri) throws URISyntaxException {
536        // need to normalize uri first
537        URI u = normalizeUri(uri);
538        String scheme = u.getScheme();
539
540        String json = jsonSchemaResolver.getComponentJSonSchema(scheme);
541        if (json == null) {
542            throw new IllegalArgumentException("Cannot find endpoint with scheme " + scheme);
543        }
544
545        // grab the syntax
546        String syntax = null;
547        String alternativeSyntax = null;
548        List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("component", json, false);
549        for (Map<String, String> row : rows) {
550            if (row.containsKey("syntax")) {
551                syntax = row.get("syntax");
552            }
553            if (row.containsKey("alternativeSyntax")) {
554                alternativeSyntax = row.get("alternativeSyntax");
555            }
556        }
557        if (syntax == null) {
558            throw new IllegalArgumentException("Endpoint with scheme " + scheme + " has no syntax defined in the json schema");
559        }
560
561        // only if we support alternative syntax, and the uri contains the username and password in the authority
562        // part of the uri, then we would need some special logic to capture that information and strip those
563        // details from the uri, so we can continue parsing the uri using the normal syntax
564        Map<String, String> userInfoOptions = new LinkedHashMap<>();
565        if (alternativeSyntax != null && alternativeSyntax.contains("@")) {
566            // clip the scheme from the syntax
567            alternativeSyntax = after(alternativeSyntax, ":");
568            // trim so only userinfo
569            int idx = alternativeSyntax.indexOf("@");
570            String fields = alternativeSyntax.substring(0, idx);
571            String[] names = fields.split(":");
572
573            // grab authority part and grab username and/or password
574            String authority = u.getAuthority();
575            if (authority != null && authority.contains("@")) {
576                String username = null;
577                String password = null;
578
579                // grab unserinfo part before @
580                String userInfo = authority.substring(0, authority.indexOf("@"));
581                String[] parts = userInfo.split(":");
582                if (parts.length == 2) {
583                    username = parts[0];
584                    password = parts[1];
585                } else {
586                    // only username
587                    username = userInfo;
588                }
589
590                // remember the username and/or password which we add later to the options
591                if (names.length == 2) {
592                    userInfoOptions.put(names[0], username);
593                    if (password != null) {
594                        // password is optional
595                        userInfoOptions.put(names[1], password);
596                    }
597                }
598            }
599        }
600
601        // clip the scheme from the syntax
602        syntax = after(syntax, ":");
603        // clip the scheme from the uri
604        uri = after(uri, ":");
605        String uriPath = stripQuery(uri);
606
607        // strip user info from uri path
608        if (!userInfoOptions.isEmpty()) {
609            int idx = uriPath.indexOf('@');
610            if (idx > -1) {
611                uriPath = uriPath.substring(idx + 1);
612            }
613        }
614
615        // strip double slash in the start
616        if (uriPath != null && uriPath.startsWith("//")) {
617            uriPath = uriPath.substring(2);
618        }
619
620        // parse the syntax and find the names of each option
621        Matcher matcher = SYNTAX_PATTERN.matcher(syntax);
622        List<String> word = new ArrayList<>();
623        while (matcher.find()) {
624            String s = matcher.group(1);
625            if (!scheme.equals(s)) {
626                word.add(s);
627            }
628        }
629        // parse the syntax and find each token between each option
630        String[] tokens = SYNTAX_PATTERN.split(syntax);
631
632        // find the position where each option start/end
633        List<String> word2 = new ArrayList<>();
634        int prev = 0;
635        int prevPath = 0;
636
637        // special for activemq/jms where the enum for destinationType causes a token issue as it includes a colon
638        // for 'temp:queue' and 'temp:topic' values
639        if ("activemq".equals(scheme) || "jms".equals(scheme)) {
640            if (uriPath.startsWith("temp:")) {
641                prevPath = 5;
642            }
643        }
644
645        for (String token : tokens) {
646            if (token.isEmpty()) {
647                continue;
648            }
649
650            // special for some tokens where :// can be used also, eg http://foo
651            int idx = -1;
652            int len = 0;
653            if (":".equals(token)) {
654                idx = uriPath.indexOf("://", prevPath);
655                len = 3;
656            }
657            if (idx == -1) {
658                idx = uriPath.indexOf(token, prevPath);
659                len = token.length();
660            }
661
662            if (idx > 0) {
663                String option = uriPath.substring(prev, idx);
664                word2.add(option);
665                prev = idx + len;
666                prevPath = prev;
667            }
668        }
669        // special for last or if we did not add anyone
670        if (prev > 0 || word2.isEmpty()) {
671            String option = uriPath.substring(prev);
672            word2.add(option);
673        }
674
675        rows = JSonSchemaHelper.parseJsonSchema("properties", json, true);
676
677        boolean defaultValueAdded = false;
678
679        // now parse the uri to know which part isw what
680        Map<String, String> options = new LinkedHashMap<>();
681
682        // include the username and password from the userinfo section
683        if (!userInfoOptions.isEmpty()) {
684            options.putAll(userInfoOptions);
685        }
686
687        // word contains the syntax path elements
688        Iterator<String> it = word2.iterator();
689        for (int i = 0; i < word.size(); i++) {
690            String key = word.get(i);
691
692            boolean allOptions = word.size() == word2.size();
693            boolean required = isPropertyRequired(rows, key);
694            String defaultValue = getPropertyDefaultValue(rows, key);
695
696            // we have all options so no problem
697            if (allOptions) {
698                String value = it.next();
699                options.put(key, value);
700            } else {
701                // we have a little problem as we do not not have all options
702                if (!required) {
703                    String value = null;
704
705                    boolean last = i == word.size() - 1;
706                    if (last) {
707                        // if its the last value then use it instead of the default value
708                        value = it.hasNext() ? it.next() : null;
709                        if (value != null) {
710                            options.put(key, value);
711                        } else {
712                            value = defaultValue;
713                        }
714                    }
715                    if (value != null) {
716                        options.put(key, value);
717                        defaultValueAdded = true;
718                    }
719                } else {
720                    String value = it.hasNext() ? it.next() : null;
721                    if (value != null) {
722                        options.put(key, value);
723                    }
724                }
725            }
726        }
727
728        Map<String, String> answer = new LinkedHashMap<>();
729
730        // remove all options which are using default values and are not required
731        for (Map.Entry<String, String> entry : options.entrySet()) {
732            String key = entry.getKey();
733            String value = entry.getValue();
734
735            if (defaultValueAdded) {
736                boolean required = isPropertyRequired(rows, key);
737                String defaultValue = getPropertyDefaultValue(rows, key);
738
739                if (!required && defaultValue != null) {
740                    if (defaultValue.equals(value)) {
741                        continue;
742                    }
743                }
744            }
745
746            // we should keep this in the answer
747            answer.put(key, value);
748        }
749
750        // now parse the uri parameters
751        Map<String, Object> parameters = URISupport.parseParameters(u);
752
753        // and covert the values to String so its JMX friendly
754        while (!parameters.isEmpty()) {
755            Map.Entry<String, Object> entry = parameters.entrySet().iterator().next();
756            String key = entry.getKey();
757            String value = entry.getValue() != null ? entry.getValue().toString() : "";
758
759            boolean multiValued = isPropertyMultiValue(rows, key);
760            if (multiValued) {
761                String prefix = getPropertyPrefix(rows, key);
762                // extra all the multi valued options
763                Map<String, Object> values = URISupport.extractProperties(parameters, prefix);
764                // build a string with the extra multi valued options with the prefix and & as separator
765                CollectionStringBuffer csb = new CollectionStringBuffer("&");
766                for (Map.Entry<String, Object> multi : values.entrySet()) {
767                    String line = prefix + multi.getKey() + "=" + (multi.getValue() != null ? multi.getValue().toString() : "");
768                    csb.append(line);
769                }
770                // append the extra multi-values to the existing (which contains the first multi value)
771                if (!csb.isEmpty()) {
772                    value = value + "&" + csb.toString();
773                }
774            }
775
776            answer.put(key, value);
777            // remove the parameter as we run in a while loop until no more parameters
778            parameters.remove(key);
779        }
780
781        return answer;
782    }
783
784    public Map<String, String> endpointLenientProperties(String uri) throws URISyntaxException {
785        // need to normalize uri first
786
787        // parse the uri
788        URI u = normalizeUri(uri);
789        String scheme = u.getScheme();
790
791        String json = jsonSchemaResolver.getComponentJSonSchema(scheme);
792        if (json == null) {
793            throw new IllegalArgumentException("Cannot find endpoint with scheme " + scheme);
794        }
795
796        List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("properties", json, true);
797
798        // now parse the uri parameters
799        Map<String, Object> parameters = URISupport.parseParameters(u);
800
801        // all the known options
802        Set<String> names = getNames(rows);
803
804        Map<String, String> answer = new LinkedHashMap<>();
805
806        // and covert the values to String so its JMX friendly
807        parameters.forEach((k, v) -> {
808            String key = k;
809            String value = v != null ? v.toString() : "";
810
811            // is the key a prefix property
812            int dot = key.indexOf('.');
813            if (dot != -1) {
814                String prefix = key.substring(0, dot + 1); // include dot in prefix
815                String option = getPropertyNameFromNameWithPrefix(rows, prefix);
816                if (option == null || !isPropertyMultiValue(rows, option)) {
817                    answer.put(key, value);
818                }
819            } else if (!names.contains(key)) {
820                answer.put(key, value);
821            }
822        });
823
824        return answer;
825    }
826
827    public String endpointComponentName(String uri) {
828        if (uri != null) {
829            int idx = uri.indexOf(":");
830            if (idx > 0) {
831                return uri.substring(0, idx);
832            }
833        }
834        return null;
835    }
836
837    public String asEndpointUri(String scheme, String json, boolean encode) throws URISyntaxException {
838        return doAsEndpointUri(scheme, json, "&", encode);
839    }
840
841    public String asEndpointUriXml(String scheme, String json, boolean encode) throws URISyntaxException {
842        return doAsEndpointUri(scheme, json, "&amp;", encode);
843    }
844
845    private String doAsEndpointUri(String scheme, String json, String ampersand, boolean encode) throws URISyntaxException {
846        List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("properties", json, true);
847
848        Map<String, String> copy = new HashMap<>();
849        for (Map<String, String> row : rows) {
850            String name = row.get("name");
851            String required = row.get("required");
852            String value = row.get("value");
853            String defaultValue = row.get("defaultValue");
854
855            // only add if either required, or the value is != default value
856            String valueToAdd = null;
857            if ("true".equals(required)) {
858                valueToAdd = value != null ? value : defaultValue;
859                if (valueToAdd == null) {
860                    valueToAdd = "";
861                }
862            } else {
863                // if we have a value and no default then add it
864                if (value != null && defaultValue == null) {
865                    valueToAdd = value;
866                }
867                // otherwise only add if the value is != default value
868                if (value != null && defaultValue != null && !value.equals(defaultValue)) {
869                    valueToAdd = value;
870                }
871            }
872
873            if (valueToAdd != null) {
874                copy.put(name, valueToAdd);
875            }
876        }
877
878        return doAsEndpointUri(scheme, copy, ampersand, encode);
879    }
880
881    public String asEndpointUri(String scheme, Map<String, String> properties, boolean encode) throws URISyntaxException {
882        return doAsEndpointUri(scheme, properties, "&", encode);
883    }
884
885    public String asEndpointUriXml(String scheme, Map<String, String> properties, boolean encode) throws URISyntaxException {
886        return doAsEndpointUri(scheme, properties, "&amp;", encode);
887    }
888
889    String doAsEndpointUri(String scheme, Map<String, String> properties, String ampersand, boolean encode) throws URISyntaxException {
890        String json = jsonSchemaResolver.getComponentJSonSchema(scheme);
891        if (json == null) {
892            throw new IllegalArgumentException("Cannot find endpoint with scheme " + scheme);
893        }
894
895        // grab the syntax
896        String originalSyntax = null;
897        List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("component", json, false);
898        for (Map<String, String> row : rows) {
899            if (row.containsKey("syntax")) {
900                originalSyntax = row.get("syntax");
901                break;
902            }
903        }
904        if (originalSyntax == null) {
905            throw new IllegalArgumentException("Endpoint with scheme " + scheme + " has no syntax defined in the json schema");
906        }
907
908        // do any properties filtering which can be needed for some special components
909        properties = filterProperties(scheme, properties);
910
911        rows = JSonSchemaHelper.parseJsonSchema("properties", json, true);
912
913        // clip the scheme from the syntax
914        String syntax = "";
915        if (originalSyntax.contains(":")) {
916            originalSyntax = after(originalSyntax, ":");
917        }
918
919        // build at first according to syntax (use a tree map as we want the uri options sorted)
920        Map<String, String> copy = new TreeMap<>(properties);
921        Matcher syntaxMatcher = COMPONENT_SYNTAX_PARSER.matcher(originalSyntax);
922        while (syntaxMatcher.find()) {
923            syntax += syntaxMatcher.group(1);
924            String propertyName = syntaxMatcher.group(2);
925            String propertyValue = copy.remove(propertyName);
926            syntax += propertyValue != null ? propertyValue : propertyName;
927        }
928
929        // do we have all the options the original syntax needs (easy way)
930        String[] keys = syntaxKeys(originalSyntax);
931        boolean hasAllKeys = properties.keySet().containsAll(Arrays.asList(keys));
932
933        // build endpoint uri
934        StringBuilder sb = new StringBuilder();
935        // add scheme later as we need to take care if there is any context-path or query parameters which
936        // affect how the URI should be constructed
937
938        if (hasAllKeys) {
939            // we have all the keys for the syntax so we can build the uri the easy way
940            sb.append(syntax);
941
942            if (!copy.isEmpty()) {
943                boolean hasQuestionmark = sb.toString().contains("?");
944                // the last option may already contain a ? char, if so we should use & instead of ?
945                sb.append(hasQuestionmark ? ampersand : '?');
946                String query = createQueryString(copy, ampersand, encode);
947                sb.append(query);
948            }
949        } else {
950            // TODO: revisit this and see if we can do this in another way
951            // oh darn some options is missing, so we need a complex way of building the uri
952
953            // the tokens between the options in the path
954            String[] tokens = syntax.split("[\\w.]+");
955
956            // parse the syntax into each options
957            Matcher matcher = SYNTAX_PATTERN.matcher(originalSyntax);
958            List<String> options = new ArrayList<>();
959            while (matcher.find()) {
960                String s = matcher.group(1);
961                options.add(s);
962            }
963
964            // need to preserve {{ and }} from the syntax
965            // (we need to use words only as its provisional placeholders)
966            syntax = syntax.replaceAll("\\{\\{", "BEGINCAMELPLACEHOLDER");
967            syntax = syntax.replaceAll("\\}\\}", "ENDCAMELPLACEHOLDER");
968
969            // parse the syntax into each options
970            Matcher matcher2 = SYNTAX_PATTERN.matcher(syntax);
971            List<String> options2 = new ArrayList<>();
972            while (matcher2.find()) {
973                String s = matcher2.group(1);
974                s = s.replaceAll("BEGINCAMELPLACEHOLDER", "\\{\\{");
975                s = s.replaceAll("ENDCAMELPLACEHOLDER", "\\}\\}");
976                options2.add(s);
977            }
978
979            // build the endpoint
980            int range = 0;
981            boolean first = true;
982            boolean hasQuestionmark = false;
983            for (int i = 0; i < options.size(); i++) {
984                String key = options.get(i);
985                String key2 = options2.get(i);
986                String token = null;
987                if (tokens.length > i) {
988                    token = tokens[i];
989                }
990
991                boolean contains = properties.containsKey(key);
992                if (!contains) {
993                    // if the key are similar we have no explicit value and can try to find a default value if the option is required
994                    if (isPropertyRequired(rows, key)) {
995                        String value = getPropertyDefaultValue(rows, key);
996                        if (value != null) {
997                            properties.put(key, value);
998                            key2 = value;
999                        }
1000                    }
1001                }
1002
1003                // was the option provided?
1004                if (properties.containsKey(key)) {
1005                    if (!first && token != null) {
1006                        sb.append(token);
1007                    }
1008                    hasQuestionmark |= key.contains("?") || (token != null && token.contains("?"));
1009                    sb.append(key2);
1010                    first = false;
1011                }
1012                range++;
1013            }
1014            // append any extra options that was in surplus for the last
1015            while (range < options2.size()) {
1016                String token = null;
1017                if (tokens.length > range) {
1018                    token = tokens[range];
1019                }
1020                String key2 = options2.get(range);
1021                sb.append(token);
1022                sb.append(key2);
1023                hasQuestionmark |= key2.contains("?") || (token != null && token.contains("?"));
1024                range++;
1025            }
1026
1027
1028            if (!copy.isEmpty()) {
1029                // the last option may already contain a ? char, if so we should use & instead of ?
1030                sb.append(hasQuestionmark ? ampersand : '?');
1031                String query = createQueryString(copy, ampersand, encode);
1032                sb.append(query);
1033            }
1034        }
1035
1036        String remainder = sb.toString();
1037        boolean queryOnly = remainder.startsWith("?");
1038        if (queryOnly) {
1039            // it has only query parameters
1040            return scheme + remainder;
1041        } else if (!remainder.isEmpty()) {
1042            // it has context path and possible query parameters
1043            return scheme + ":" + remainder;
1044        } else {
1045            // its empty without anything
1046            return scheme;
1047        }
1048    }
1049
1050    @Deprecated
1051    private static String[] syntaxTokens(String syntax) {
1052        // build tokens between the words
1053        List<String> tokens = new ArrayList<>();
1054        // preserve backwards behavior which had an empty token first
1055        tokens.add("");
1056
1057        String current = "";
1058        for (int i = 0; i < syntax.length(); i++) {
1059            char ch = syntax.charAt(i);
1060            if (Character.isLetterOrDigit(ch)) {
1061                // reset for new current tokens
1062                if (current.length() > 0) {
1063                    tokens.add(current);
1064                    current = "";
1065                }
1066            } else {
1067                current += ch;
1068            }
1069        }
1070        // anything left over?
1071        if (current.length() > 0) {
1072            tokens.add(current);
1073        }
1074
1075        return tokens.toArray(new String[tokens.size()]);
1076    }
1077
1078    private static String[] syntaxKeys(String syntax) {
1079        // build tokens between the separators
1080        List<String> tokens = new ArrayList<>();
1081
1082        if (syntax != null) {
1083            String current = "";
1084            for (int i = 0; i < syntax.length(); i++) {
1085                char ch = syntax.charAt(i);
1086                if (Character.isLetterOrDigit(ch)) {
1087                    current += ch;
1088                } else {
1089                    // reset for new current tokens
1090                    if (current.length() > 0) {
1091                        tokens.add(current);
1092                        current = "";
1093                    }
1094                }
1095            }
1096            // anything left over?
1097            if (current.length() > 0) {
1098                tokens.add(current);
1099            }
1100        }
1101
1102        return tokens.toArray(new String[tokens.size()]);
1103    }
1104
1105    public SimpleValidationResult validateSimpleExpression(String simple) {
1106        return doValidateSimple(null, simple, false);
1107    }
1108
1109    public SimpleValidationResult validateSimpleExpression(ClassLoader classLoader, String simple) {
1110        return doValidateSimple(classLoader, simple, false);
1111    }
1112
1113    public SimpleValidationResult validateSimplePredicate(String simple) {
1114        return doValidateSimple(null, simple, true);
1115    }
1116
1117    public SimpleValidationResult validateSimplePredicate(ClassLoader classLoader, String simple) {
1118        return doValidateSimple(classLoader, simple, true);
1119    }
1120
1121    private SimpleValidationResult doValidateSimple(ClassLoader classLoader, String simple, boolean predicate) {
1122        if (classLoader == null) {
1123            classLoader = getClass().getClassLoader();
1124        }
1125
1126        // if there are {{ }}} property placeholders then we need to resolve them to something else
1127        // as the simple parse cannot resolve them before parsing as we dont run the actual Camel application
1128        // with property placeholders setup so we need to dummy this by replace the {{ }} to something else
1129        // therefore we use an more unlikely character: {{XXX}} to ~^XXX^~
1130        String resolved = simple.replaceAll("\\{\\{(.+)\\}\\}", "~^$1^~");
1131
1132        SimpleValidationResult answer = new SimpleValidationResult(simple);
1133
1134        Object instance = null;
1135        Class clazz = null;
1136        try {
1137            clazz = classLoader.loadClass("org.apache.camel.language.simple.SimpleLanguage");
1138            instance = clazz.newInstance();
1139        } catch (Exception e) {
1140            // ignore
1141        }
1142
1143        if (clazz != null && instance != null) {
1144            Throwable cause = null;
1145            try {
1146                if (predicate) {
1147                    instance.getClass().getMethod("createPredicate", String.class).invoke(instance, resolved);
1148                } else {
1149                    instance.getClass().getMethod("createExpression", String.class).invoke(instance, resolved);
1150                }
1151            } catch (InvocationTargetException e) {
1152                cause = e.getTargetException();
1153            } catch (Exception e) {
1154                cause = e;
1155            }
1156
1157            if (cause != null) {
1158
1159                // reverse ~^XXX^~ back to {{XXX}}
1160                String errMsg = cause.getMessage();
1161                errMsg = errMsg.replaceAll("\\~\\^(.+)\\^\\~", "{{$1}}");
1162
1163                answer.setError(errMsg);
1164
1165                // is it simple parser exception then we can grab the index where the problem is
1166                if (cause.getClass().getName().equals("org.apache.camel.language.simple.types.SimpleIllegalSyntaxException")
1167                    || cause.getClass().getName().equals("org.apache.camel.language.simple.types.SimpleParserException")) {
1168                    try {
1169                        // we need to grab the index field from those simple parser exceptions
1170                        Method method = cause.getClass().getMethod("getIndex");
1171                        Object result = method.invoke(cause);
1172                        if (result != null) {
1173                            int index = (int) result;
1174                            answer.setIndex(index);
1175                        }
1176                    } catch (Throwable i) {
1177                        // ignore
1178                    }
1179                }
1180
1181                // we need to grab the short message field from this simple syntax exception
1182                if (cause.getClass().getName().equals("org.apache.camel.language.simple.types.SimpleIllegalSyntaxException")) {
1183                    try {
1184                        Method method = cause.getClass().getMethod("getShortMessage");
1185                        Object result = method.invoke(cause);
1186                        if (result != null) {
1187                            String msg = (String) result;
1188                            answer.setShortError(msg);
1189                        }
1190                    } catch (Throwable i) {
1191                        // ignore
1192                    }
1193
1194                    if (answer.getShortError() == null) {
1195                        // fallback and try to make existing message short instead
1196                        String msg = answer.getError();
1197                        // grab everything before " at location " which would be regarded as the short message
1198                        int idx = msg.indexOf(" at location ");
1199                        if (idx > 0) {
1200                            msg = msg.substring(0, idx);
1201                            answer.setShortError(msg);
1202                        }
1203                    }
1204                }
1205            }
1206        }
1207
1208        return answer;
1209    }
1210
1211    public LanguageValidationResult validateLanguagePredicate(ClassLoader classLoader, String language, String text) {
1212        return doValidateLanguage(classLoader, language, text, true);
1213    }
1214
1215    public LanguageValidationResult validateLanguageExpression(ClassLoader classLoader, String language, String text) {
1216        return doValidateLanguage(classLoader, language, text, false);
1217    }
1218
1219    private LanguageValidationResult doValidateLanguage(ClassLoader classLoader, String language, String text, boolean predicate) {
1220        if (classLoader == null) {
1221            classLoader = getClass().getClassLoader();
1222        }
1223
1224        SimpleValidationResult answer = new SimpleValidationResult(text);
1225
1226        String json = jsonSchemaResolver.getLanguageJSonSchema(language);
1227        if (json == null) {
1228            answer.setError("Unknown language " + language);
1229            return answer;
1230        }
1231
1232        List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("language", json, false);
1233        String className = null;
1234        for (Map<String, String> row : rows) {
1235            if (row.containsKey("javaType")) {
1236                className = row.get("javaType");
1237            }
1238        }
1239
1240        if (className == null) {
1241            answer.setError("Cannot find javaType for language " + language);
1242            return answer;
1243        }
1244
1245        Object instance = null;
1246        Class clazz = null;
1247        try {
1248            clazz = classLoader.loadClass(className);
1249            instance = clazz.newInstance();
1250        } catch (Exception e) {
1251            // ignore
1252        }
1253
1254        if (clazz != null && instance != null) {
1255            Throwable cause = null;
1256            try {
1257                if (predicate) {
1258                    instance.getClass().getMethod("createPredicate", String.class).invoke(instance, text);
1259                } else {
1260                    instance.getClass().getMethod("createExpression", String.class).invoke(instance, text);
1261                }
1262            } catch (InvocationTargetException e) {
1263                cause = e.getTargetException();
1264            } catch (Exception e) {
1265                cause = e;
1266            }
1267
1268            if (cause != null) {
1269                answer.setError(cause.getMessage());
1270            }
1271        }
1272
1273        return answer;
1274    }
1275
1276    /**
1277     * Special logic for log endpoints to deal when showAll=true
1278     */
1279    private Map<String, String> filterProperties(String scheme, Map<String, String> options) {
1280        if ("log".equals(scheme)) {
1281            String showAll = options.get("showAll");
1282            if ("true".equals(showAll)) {
1283                Map<String, String> filtered = new LinkedHashMap<>();
1284                // remove all the other showXXX options when showAll=true
1285                for (Map.Entry<String, String> entry : options.entrySet()) {
1286                    String key = entry.getKey();
1287                    boolean skip = key.startsWith("show") && !key.equals("showAll");
1288                    if (!skip) {
1289                        filtered.put(key, entry.getValue());
1290                    }
1291                }
1292                return filtered;
1293            }
1294        }
1295        // use as-is
1296        return options;
1297    }
1298
1299    private static boolean validateInteger(String value) {
1300        boolean valid = false;
1301        try {
1302            valid = Integer.valueOf(value) != null;
1303        } catch (Exception e) {
1304            // ignore
1305        }
1306        if (!valid) {
1307            // it may be a time pattern, such as 5s for 5 seconds = 5000
1308            try {
1309                TimePatternConverter.toMilliSeconds(value);
1310                valid = true;
1311            } catch (Exception e) {
1312                // ignore
1313            }
1314        }
1315        return valid;
1316    }
1317
1318    // CHECKSTYLE:ON
1319
1320}