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