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