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