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