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