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 if (prefix != null) { 763 // extra all the multi valued options 764 Map<String, Object> values = URISupport.extractProperties(parameters, prefix); 765 // build a string with the extra multi valued options with the prefix and & as separator 766 CollectionStringBuffer csb = new CollectionStringBuffer("&"); 767 for (Map.Entry<String, Object> multi : values.entrySet()) { 768 String line = prefix + multi.getKey() + "=" + (multi.getValue() != null ? multi.getValue().toString() : ""); 769 csb.append(line); 770 } 771 // append the extra multi-values to the existing (which contains the first multi value) 772 if (!csb.isEmpty()) { 773 value = value + "&" + csb.toString(); 774 } 775 } 776 } 777 778 answer.put(key, value); 779 // remove the parameter as we run in a while loop until no more parameters 780 parameters.remove(key); 781 } 782 783 return answer; 784 } 785 786 public Map<String, String> endpointLenientProperties(String uri) throws URISyntaxException { 787 // need to normalize uri first 788 789 // parse the uri 790 URI u = normalizeUri(uri); 791 String scheme = u.getScheme(); 792 793 String json = jsonSchemaResolver.getComponentJSonSchema(scheme); 794 if (json == null) { 795 throw new IllegalArgumentException("Cannot find endpoint with scheme " + scheme); 796 } 797 798 List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("properties", json, true); 799 800 // now parse the uri parameters 801 Map<String, Object> parameters = URISupport.parseParameters(u); 802 803 // all the known options 804 Set<String> names = getNames(rows); 805 806 Map<String, String> answer = new LinkedHashMap<>(); 807 808 // and covert the values to String so its JMX friendly 809 parameters.forEach((k, v) -> { 810 String key = k; 811 String value = v != null ? v.toString() : ""; 812 813 // is the key a prefix property 814 int dot = key.indexOf('.'); 815 if (dot != -1) { 816 String prefix = key.substring(0, dot + 1); // include dot in prefix 817 String option = getPropertyNameFromNameWithPrefix(rows, prefix); 818 if (option == null || !isPropertyMultiValue(rows, option)) { 819 answer.put(key, value); 820 } 821 } else if (!names.contains(key)) { 822 answer.put(key, value); 823 } 824 }); 825 826 return answer; 827 } 828 829 public String endpointComponentName(String uri) { 830 if (uri != null) { 831 int idx = uri.indexOf(":"); 832 if (idx > 0) { 833 return uri.substring(0, idx); 834 } 835 } 836 return null; 837 } 838 839 public String asEndpointUri(String scheme, String json, boolean encode) throws URISyntaxException { 840 return doAsEndpointUri(scheme, json, "&", encode); 841 } 842 843 public String asEndpointUriXml(String scheme, String json, boolean encode) throws URISyntaxException { 844 return doAsEndpointUri(scheme, json, "&", encode); 845 } 846 847 private String doAsEndpointUri(String scheme, String json, String ampersand, boolean encode) throws URISyntaxException { 848 List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("properties", json, true); 849 850 Map<String, String> copy = new HashMap<>(); 851 for (Map<String, String> row : rows) { 852 String name = row.get("name"); 853 String required = row.get("required"); 854 String value = row.get("value"); 855 String defaultValue = row.get("defaultValue"); 856 857 // only add if either required, or the value is != default value 858 String valueToAdd = null; 859 if ("true".equals(required)) { 860 valueToAdd = value != null ? value : defaultValue; 861 if (valueToAdd == null) { 862 valueToAdd = ""; 863 } 864 } else { 865 // if we have a value and no default then add it 866 if (value != null && defaultValue == null) { 867 valueToAdd = value; 868 } 869 // otherwise only add if the value is != default value 870 if (value != null && defaultValue != null && !value.equals(defaultValue)) { 871 valueToAdd = value; 872 } 873 } 874 875 if (valueToAdd != null) { 876 copy.put(name, valueToAdd); 877 } 878 } 879 880 return doAsEndpointUri(scheme, copy, ampersand, encode); 881 } 882 883 public String asEndpointUri(String scheme, Map<String, String> properties, boolean encode) throws URISyntaxException { 884 return doAsEndpointUri(scheme, properties, "&", encode); 885 } 886 887 public String asEndpointUriXml(String scheme, Map<String, String> properties, boolean encode) throws URISyntaxException { 888 return doAsEndpointUri(scheme, properties, "&", encode); 889 } 890 891 String doAsEndpointUri(String scheme, Map<String, String> properties, String ampersand, boolean encode) throws URISyntaxException { 892 String json = jsonSchemaResolver.getComponentJSonSchema(scheme); 893 if (json == null) { 894 throw new IllegalArgumentException("Cannot find endpoint with scheme " + scheme); 895 } 896 897 // grab the syntax 898 String originalSyntax = null; 899 List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("component", json, false); 900 for (Map<String, String> row : rows) { 901 if (row.containsKey("syntax")) { 902 originalSyntax = row.get("syntax"); 903 break; 904 } 905 } 906 if (originalSyntax == null) { 907 throw new IllegalArgumentException("Endpoint with scheme " + scheme + " has no syntax defined in the json schema"); 908 } 909 910 // do any properties filtering which can be needed for some special components 911 properties = filterProperties(scheme, properties); 912 913 rows = JSonSchemaHelper.parseJsonSchema("properties", json, true); 914 915 // clip the scheme from the syntax 916 String syntax = ""; 917 if (originalSyntax.contains(":")) { 918 originalSyntax = after(originalSyntax, ":"); 919 } 920 921 // build at first according to syntax (use a tree map as we want the uri options sorted) 922 Map<String, String> copy = new TreeMap<>(properties); 923 Matcher syntaxMatcher = COMPONENT_SYNTAX_PARSER.matcher(originalSyntax); 924 while (syntaxMatcher.find()) { 925 syntax += syntaxMatcher.group(1); 926 String propertyName = syntaxMatcher.group(2); 927 String propertyValue = copy.remove(propertyName); 928 syntax += propertyValue != null ? propertyValue : propertyName; 929 } 930 931 // do we have all the options the original syntax needs (easy way) 932 String[] keys = syntaxKeys(originalSyntax); 933 boolean hasAllKeys = properties.keySet().containsAll(Arrays.asList(keys)); 934 935 // build endpoint uri 936 StringBuilder sb = new StringBuilder(); 937 // add scheme later as we need to take care if there is any context-path or query parameters which 938 // affect how the URI should be constructed 939 940 if (hasAllKeys) { 941 // we have all the keys for the syntax so we can build the uri the easy way 942 sb.append(syntax); 943 944 if (!copy.isEmpty()) { 945 boolean hasQuestionmark = sb.toString().contains("?"); 946 // the last option may already contain a ? char, if so we should use & instead of ? 947 sb.append(hasQuestionmark ? ampersand : '?'); 948 String query = createQueryString(copy, ampersand, encode); 949 sb.append(query); 950 } 951 } else { 952 // TODO: revisit this and see if we can do this in another way 953 // oh darn some options is missing, so we need a complex way of building the uri 954 955 // the tokens between the options in the path 956 String[] tokens = syntax.split("[\\w.]+"); 957 958 // parse the syntax into each options 959 Matcher matcher = SYNTAX_PATTERN.matcher(originalSyntax); 960 List<String> options = new ArrayList<>(); 961 while (matcher.find()) { 962 String s = matcher.group(1); 963 options.add(s); 964 } 965 966 // need to preserve {{ and }} from the syntax 967 // (we need to use words only as its provisional placeholders) 968 syntax = syntax.replaceAll("\\{\\{", "BEGINCAMELPLACEHOLDER"); 969 syntax = syntax.replaceAll("\\}\\}", "ENDCAMELPLACEHOLDER"); 970 971 // parse the syntax into each options 972 Matcher matcher2 = SYNTAX_PATTERN.matcher(syntax); 973 List<String> options2 = new ArrayList<>(); 974 while (matcher2.find()) { 975 String s = matcher2.group(1); 976 s = s.replaceAll("BEGINCAMELPLACEHOLDER", "\\{\\{"); 977 s = s.replaceAll("ENDCAMELPLACEHOLDER", "\\}\\}"); 978 options2.add(s); 979 } 980 981 // build the endpoint 982 int range = 0; 983 boolean first = true; 984 boolean hasQuestionmark = false; 985 for (int i = 0; i < options.size(); i++) { 986 String key = options.get(i); 987 String key2 = options2.get(i); 988 String token = null; 989 if (tokens.length > i) { 990 token = tokens[i]; 991 } 992 993 boolean contains = properties.containsKey(key); 994 if (!contains) { 995 // if the key are similar we have no explicit value and can try to find a default value if the option is required 996 if (isPropertyRequired(rows, key)) { 997 String value = getPropertyDefaultValue(rows, key); 998 if (value != null) { 999 properties.put(key, value); 1000 key2 = value; 1001 } 1002 } 1003 } 1004 1005 // was the option provided? 1006 if (properties.containsKey(key)) { 1007 if (!first && token != null) { 1008 sb.append(token); 1009 } 1010 hasQuestionmark |= key.contains("?") || (token != null && token.contains("?")); 1011 sb.append(key2); 1012 first = false; 1013 } 1014 range++; 1015 } 1016 // append any extra options that was in surplus for the last 1017 while (range < options2.size()) { 1018 String token = null; 1019 if (tokens.length > range) { 1020 token = tokens[range]; 1021 } 1022 String key2 = options2.get(range); 1023 sb.append(token); 1024 sb.append(key2); 1025 hasQuestionmark |= key2.contains("?") || (token != null && token.contains("?")); 1026 range++; 1027 } 1028 1029 1030 if (!copy.isEmpty()) { 1031 // the last option may already contain a ? char, if so we should use & instead of ? 1032 sb.append(hasQuestionmark ? ampersand : '?'); 1033 String query = createQueryString(copy, ampersand, encode); 1034 sb.append(query); 1035 } 1036 } 1037 1038 String remainder = sb.toString(); 1039 boolean queryOnly = remainder.startsWith("?"); 1040 if (queryOnly) { 1041 // it has only query parameters 1042 return scheme + remainder; 1043 } else if (!remainder.isEmpty()) { 1044 // it has context path and possible query parameters 1045 return scheme + ":" + remainder; 1046 } else { 1047 // its empty without anything 1048 return scheme; 1049 } 1050 } 1051 1052 @Deprecated 1053 private static String[] syntaxTokens(String syntax) { 1054 // build tokens between the words 1055 List<String> tokens = new ArrayList<>(); 1056 // preserve backwards behavior which had an empty token first 1057 tokens.add(""); 1058 1059 String current = ""; 1060 for (int i = 0; i < syntax.length(); i++) { 1061 char ch = syntax.charAt(i); 1062 if (Character.isLetterOrDigit(ch)) { 1063 // reset for new current tokens 1064 if (current.length() > 0) { 1065 tokens.add(current); 1066 current = ""; 1067 } 1068 } else { 1069 current += ch; 1070 } 1071 } 1072 // anything left over? 1073 if (current.length() > 0) { 1074 tokens.add(current); 1075 } 1076 1077 return tokens.toArray(new String[tokens.size()]); 1078 } 1079 1080 private static String[] syntaxKeys(String syntax) { 1081 // build tokens between the separators 1082 List<String> tokens = new ArrayList<>(); 1083 1084 if (syntax != null) { 1085 String current = ""; 1086 for (int i = 0; i < syntax.length(); i++) { 1087 char ch = syntax.charAt(i); 1088 if (Character.isLetterOrDigit(ch)) { 1089 current += ch; 1090 } else { 1091 // reset for new current tokens 1092 if (current.length() > 0) { 1093 tokens.add(current); 1094 current = ""; 1095 } 1096 } 1097 } 1098 // anything left over? 1099 if (current.length() > 0) { 1100 tokens.add(current); 1101 } 1102 } 1103 1104 return tokens.toArray(new String[tokens.size()]); 1105 } 1106 1107 public SimpleValidationResult validateSimpleExpression(String simple) { 1108 return doValidateSimple(null, simple, false); 1109 } 1110 1111 public SimpleValidationResult validateSimpleExpression(ClassLoader classLoader, String simple) { 1112 return doValidateSimple(classLoader, simple, false); 1113 } 1114 1115 public SimpleValidationResult validateSimplePredicate(String simple) { 1116 return doValidateSimple(null, simple, true); 1117 } 1118 1119 public SimpleValidationResult validateSimplePredicate(ClassLoader classLoader, String simple) { 1120 return doValidateSimple(classLoader, simple, true); 1121 } 1122 1123 private SimpleValidationResult doValidateSimple(ClassLoader classLoader, String simple, boolean predicate) { 1124 if (classLoader == null) { 1125 classLoader = getClass().getClassLoader(); 1126 } 1127 1128 // if there are {{ }}} property placeholders then we need to resolve them to something else 1129 // as the simple parse cannot resolve them before parsing as we dont run the actual Camel application 1130 // with property placeholders setup so we need to dummy this by replace the {{ }} to something else 1131 // therefore we use an more unlikely character: {{XXX}} to ~^XXX^~ 1132 String resolved = simple.replaceAll("\\{\\{(.+)\\}\\}", "~^$1^~"); 1133 1134 SimpleValidationResult answer = new SimpleValidationResult(simple); 1135 1136 Object instance = null; 1137 Class clazz = null; 1138 try { 1139 clazz = classLoader.loadClass("org.apache.camel.language.simple.SimpleLanguage"); 1140 instance = clazz.newInstance(); 1141 } catch (Exception e) { 1142 // ignore 1143 } 1144 1145 if (clazz != null && instance != null) { 1146 Throwable cause = null; 1147 try { 1148 if (predicate) { 1149 instance.getClass().getMethod("createPredicate", String.class).invoke(instance, resolved); 1150 } else { 1151 instance.getClass().getMethod("createExpression", String.class).invoke(instance, resolved); 1152 } 1153 } catch (InvocationTargetException e) { 1154 cause = e.getTargetException(); 1155 } catch (Exception e) { 1156 cause = e; 1157 } 1158 1159 if (cause != null) { 1160 1161 // reverse ~^XXX^~ back to {{XXX}} 1162 String errMsg = cause.getMessage(); 1163 errMsg = errMsg.replaceAll("\\~\\^(.+)\\^\\~", "{{$1}}"); 1164 1165 answer.setError(errMsg); 1166 1167 // is it simple parser exception then we can grab the index where the problem is 1168 if (cause.getClass().getName().equals("org.apache.camel.language.simple.types.SimpleIllegalSyntaxException") 1169 || cause.getClass().getName().equals("org.apache.camel.language.simple.types.SimpleParserException")) { 1170 try { 1171 // we need to grab the index field from those simple parser exceptions 1172 Method method = cause.getClass().getMethod("getIndex"); 1173 Object result = method.invoke(cause); 1174 if (result != null) { 1175 int index = (int) result; 1176 answer.setIndex(index); 1177 } 1178 } catch (Throwable i) { 1179 // ignore 1180 } 1181 } 1182 1183 // we need to grab the short message field from this simple syntax exception 1184 if (cause.getClass().getName().equals("org.apache.camel.language.simple.types.SimpleIllegalSyntaxException")) { 1185 try { 1186 Method method = cause.getClass().getMethod("getShortMessage"); 1187 Object result = method.invoke(cause); 1188 if (result != null) { 1189 String msg = (String) result; 1190 answer.setShortError(msg); 1191 } 1192 } catch (Throwable i) { 1193 // ignore 1194 } 1195 1196 if (answer.getShortError() == null) { 1197 // fallback and try to make existing message short instead 1198 String msg = answer.getError(); 1199 // grab everything before " at location " which would be regarded as the short message 1200 int idx = msg.indexOf(" at location "); 1201 if (idx > 0) { 1202 msg = msg.substring(0, idx); 1203 answer.setShortError(msg); 1204 } 1205 } 1206 } 1207 } 1208 } 1209 1210 return answer; 1211 } 1212 1213 public LanguageValidationResult validateLanguagePredicate(ClassLoader classLoader, String language, String text) { 1214 return doValidateLanguage(classLoader, language, text, true); 1215 } 1216 1217 public LanguageValidationResult validateLanguageExpression(ClassLoader classLoader, String language, String text) { 1218 return doValidateLanguage(classLoader, language, text, false); 1219 } 1220 1221 private LanguageValidationResult doValidateLanguage(ClassLoader classLoader, String language, String text, boolean predicate) { 1222 if (classLoader == null) { 1223 classLoader = getClass().getClassLoader(); 1224 } 1225 1226 SimpleValidationResult answer = new SimpleValidationResult(text); 1227 1228 String json = jsonSchemaResolver.getLanguageJSonSchema(language); 1229 if (json == null) { 1230 answer.setError("Unknown language " + language); 1231 return answer; 1232 } 1233 1234 List<Map<String, String>> rows = JSonSchemaHelper.parseJsonSchema("language", json, false); 1235 String className = null; 1236 for (Map<String, String> row : rows) { 1237 if (row.containsKey("javaType")) { 1238 className = row.get("javaType"); 1239 } 1240 } 1241 1242 if (className == null) { 1243 answer.setError("Cannot find javaType for language " + language); 1244 return answer; 1245 } 1246 1247 Object instance = null; 1248 Class clazz = null; 1249 try { 1250 clazz = classLoader.loadClass(className); 1251 instance = clazz.newInstance(); 1252 } catch (Exception e) { 1253 // ignore 1254 } 1255 1256 if (clazz != null && instance != null) { 1257 Throwable cause = null; 1258 try { 1259 if (predicate) { 1260 instance.getClass().getMethod("createPredicate", String.class).invoke(instance, text); 1261 } else { 1262 instance.getClass().getMethod("createExpression", String.class).invoke(instance, text); 1263 } 1264 } catch (InvocationTargetException e) { 1265 cause = e.getTargetException(); 1266 } catch (Exception e) { 1267 cause = e; 1268 } 1269 1270 if (cause != null) { 1271 answer.setError(cause.getMessage()); 1272 } 1273 } 1274 1275 return answer; 1276 } 1277 1278 /** 1279 * Special logic for log endpoints to deal when showAll=true 1280 */ 1281 private Map<String, String> filterProperties(String scheme, Map<String, String> options) { 1282 if ("log".equals(scheme)) { 1283 String showAll = options.get("showAll"); 1284 if ("true".equals(showAll)) { 1285 Map<String, String> filtered = new LinkedHashMap<>(); 1286 // remove all the other showXXX options when showAll=true 1287 for (Map.Entry<String, String> entry : options.entrySet()) { 1288 String key = entry.getKey(); 1289 boolean skip = key.startsWith("show") && !key.equals("showAll"); 1290 if (!skip) { 1291 filtered.put(key, entry.getValue()); 1292 } 1293 } 1294 return filtered; 1295 } 1296 } 1297 // use as-is 1298 return options; 1299 } 1300 1301 private static boolean validateInteger(String value) { 1302 boolean valid = false; 1303 try { 1304 valid = Integer.valueOf(value) != null; 1305 } catch (Exception e) { 1306 // ignore 1307 } 1308 if (!valid) { 1309 // it may be a time pattern, such as 5s for 5 seconds = 5000 1310 try { 1311 TimePatternConverter.toMilliSeconds(value); 1312 valid = true; 1313 } catch (Exception e) { 1314 // ignore 1315 } 1316 } 1317 return valid; 1318 } 1319 1320 // CHECKSTYLE:ON 1321 1322}