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.util.component; 018 019import java.lang.reflect.Array; 020import java.lang.reflect.InvocationTargetException; 021import java.util.ArrayList; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.HashMap; 025import java.util.HashSet; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029import java.util.regex.Matcher; 030import java.util.regex.Pattern; 031 032import org.apache.camel.RuntimeCamelException; 033import org.slf4j.Logger; 034import org.slf4j.LoggerFactory; 035 036/** 037 * Helper class for working with {@link ApiMethod}. 038 */ 039public final class ApiMethodHelper<T extends Enum<T> & ApiMethod> { 040 041 private static final Logger LOG = LoggerFactory.getLogger(ApiMethodHelper.class); 042 043 // maps method name to ApiMethod 044 private final Map<String, List<T>> methodMap; 045 046 // maps method name to method arguments of the form Class type1, String name1, Class type2, String name2,... 047 private final Map<String, List<Object>> argumentsMap; 048 049 // maps argument name to argument type 050 private final Map<String, Class<?>> validArguments; 051 052 // maps aliases to actual method names 053 private final Map<String, Set<String>> aliasesMap; 054 055 // nullable args 056 private final List<String> nullableArguments; 057 058 /** 059 * Create a helper to work with a {@link ApiMethod}, using optional method aliases. 060 * @param apiMethodEnum {@link ApiMethod} enumeration class 061 * @param aliases Aliases mapped to actual method names 062 * @param nullableArguments names of arguments that default to null value 063 */ 064 public ApiMethodHelper(Class<T> apiMethodEnum, Map<String, String> aliases, List<String> nullableArguments) { 065 066 Map<String, List<T>> tmpMethodMap = new HashMap<>(); 067 Map<String, List<Object>> tmpArgumentsMap = new HashMap<>(); 068 Map<String, Class<?>> tmpValidArguments = new HashMap<>(); 069 Map<String, Set<String>> tmpAliasesMap = new HashMap<>(); 070 071 // validate ApiMethod Enum 072 if (apiMethodEnum == null) { 073 throw new IllegalArgumentException("ApiMethod enumeration cannot be null"); 074 } 075 076 if (nullableArguments != null && !nullableArguments.isEmpty()) { 077 this.nullableArguments = Collections.unmodifiableList(new ArrayList<>(nullableArguments)); 078 } else { 079 this.nullableArguments = Collections.emptyList(); 080 } 081 082 final Map<Pattern, String> aliasPatterns = new HashMap<>(); 083 for (Map.Entry<String, String> alias : aliases.entrySet()) { 084 if (alias.getKey() == null || alias.getValue() == null) { 085 throw new IllegalArgumentException("Alias pattern and replacement cannot be null"); 086 } 087 aliasPatterns.put(Pattern.compile(alias.getKey()), alias.getValue()); 088 } 089 090 LOG.debug("Processing {}", apiMethodEnum.getName()); 091 final T[] methods = apiMethodEnum.getEnumConstants(); 092 093 // load lookup maps 094 for (T method : methods) { 095 096 final String name = method.getName(); 097 098 // add method name aliases 099 for (Map.Entry<Pattern, String> aliasEntry : aliasPatterns.entrySet()) { 100 final Matcher matcher = aliasEntry.getKey().matcher(name); 101 if (matcher.find()) { 102 // add method name alias 103 String alias = matcher.replaceAll(aliasEntry.getValue()); 104 // convert first character to lowercase 105 assert alias.length() > 1; 106 final char firstChar = alias.charAt(0); 107 if (!Character.isLowerCase(firstChar)) { 108 final StringBuilder builder = new StringBuilder(); 109 builder.append(Character.toLowerCase(firstChar)).append(alias.substring(1)); 110 alias = builder.toString(); 111 } 112 Set<String> names = tmpAliasesMap.get(alias); 113 if (names == null) { 114 names = new HashSet<>(); 115 tmpAliasesMap.put(alias, names); 116 } 117 names.add(name); 118 } 119 } 120 121 // map method name to Enum 122 List<T> overloads = tmpMethodMap.get(name); 123 if (overloads == null) { 124 overloads = new ArrayList<>(); 125 tmpMethodMap.put(method.getName(), overloads); 126 } 127 overloads.add(method); 128 129 // add arguments for this method 130 List<Object> arguments = tmpArgumentsMap.get(name); 131 if (arguments == null) { 132 arguments = new ArrayList<>(); 133 tmpArgumentsMap.put(name, arguments); 134 } 135 136 // process all arguments for this method 137 final int nArgs = method.getArgNames().size(); 138 final String[] argNames = method.getArgNames().toArray(new String[nArgs]); 139 final Class<?>[] argTypes = method.getArgTypes().toArray(new Class[nArgs]); 140 for (int i = 0; i < nArgs; i++) { 141 final String argName = argNames[i]; 142 final Class<?> argType = argTypes[i]; 143 if (!arguments.contains(argName)) { 144 arguments.add(argType); 145 arguments.add(argName); 146 } 147 148 // also collect argument names for all methods, and detect clashes here 149 final Class<?> previousType = tmpValidArguments.get(argName); 150 if (previousType != null && previousType != argType) { 151 throw new IllegalArgumentException(String.format( 152 "Argument %s has ambiguous types (%s, %s) across methods!", 153 name, previousType, argType)); 154 } else if (previousType == null) { 155 tmpValidArguments.put(argName, argType); 156 } 157 } 158 159 } 160 161 // validate nullableArguments 162 if (!tmpValidArguments.keySet().containsAll(this.nullableArguments)) { 163 List<String> unknowns = new ArrayList<>(this.nullableArguments); 164 unknowns.removeAll(tmpValidArguments.keySet()); 165 throw new IllegalArgumentException("Unknown nullable arguments " + unknowns.toString()); 166 } 167 168 // validate aliases 169 for (Map.Entry<String, Set<String>> entry : tmpAliasesMap.entrySet()) { 170 171 // look for aliases that match multiple methods 172 final Set<String> methodNames = entry.getValue(); 173 if (methodNames.size() > 1) { 174 175 // get mapped methods 176 final List<T> aliasedMethods = new ArrayList<>(); 177 for (String methodName : methodNames) { 178 List<T> mappedMethods = tmpMethodMap.get(methodName); 179 aliasedMethods.addAll(mappedMethods); 180 } 181 182 // look for argument overlap 183 for (T method : aliasedMethods) { 184 final List<String> argNames = new ArrayList<>(method.getArgNames()); 185 argNames.removeAll(this.nullableArguments); 186 187 final Set<T> ambiguousMethods = new HashSet<>(); 188 for (T otherMethod : aliasedMethods) { 189 if (method != otherMethod) { 190 final List<String> otherArgsNames = new ArrayList<>(otherMethod.getArgNames()); 191 otherArgsNames.removeAll(this.nullableArguments); 192 193 if (argNames.equals(otherArgsNames)) { 194 ambiguousMethods.add(method); 195 ambiguousMethods.add(otherMethod); 196 } 197 } 198 } 199 200 if (!ambiguousMethods.isEmpty()) { 201 throw new IllegalArgumentException( 202 String.format("Ambiguous alias %s for methods %s", entry.getKey(), ambiguousMethods)); 203 } 204 } 205 } 206 } 207 208 this.methodMap = Collections.unmodifiableMap(tmpMethodMap); 209 this.argumentsMap = Collections.unmodifiableMap(tmpArgumentsMap); 210 this.validArguments = Collections.unmodifiableMap(tmpValidArguments); 211 this.aliasesMap = Collections.unmodifiableMap(tmpAliasesMap); 212 213 LOG.debug("Found {} unique method names in {} methods", tmpMethodMap.size(), methods.length); 214 } 215 216 /** 217 * Gets methods that match the given name and arguments.<p/> 218 * Note that the args list is a required subset of arguments for returned methods. 219 * 220 * @param name case sensitive method name or alias to lookup 221 * @return non-null unmodifiable list of methods that take all of the given arguments, empty if there is no match 222 */ 223 public List<ApiMethod> getCandidateMethods(String name) { 224 return getCandidateMethods(name, Collections.emptyList()); 225 } 226 227 /** 228 * Gets methods that match the given name and arguments.<p/> 229 * Note that the args list is a required subset of arguments for returned methods. 230 * 231 * @param name case sensitive method name or alias to lookup 232 * @param argNames unordered required argument names 233 * @return non-null unmodifiable list of methods that take all of the given arguments, empty if there is no match 234 */ 235 public List<ApiMethod> getCandidateMethods(String name, Collection<String> argNames) { 236 List<T> methods = methodMap.get(name); 237 if (methods == null) { 238 if (aliasesMap.containsKey(name)) { 239 methods = new ArrayList<>(); 240 for (String method : aliasesMap.get(name)) { 241 methods.addAll(methodMap.get(method)); 242 } 243 } 244 } 245 if (methods == null) { 246 LOG.debug("No matching method for method {}", name); 247 return Collections.emptyList(); 248 } 249 int nArgs = argNames != null ? argNames.size() : 0; 250 if (nArgs == 0) { 251 LOG.debug("Found {} methods for method {}", methods.size(), name); 252 return Collections.unmodifiableList(methods); 253 } else { 254 final List<ApiMethod> filteredSet = filterMethods(methods, MatchType.SUBSET, argNames); 255 if (LOG.isDebugEnabled()) { 256 LOG.debug("Found {} filtered methods for {}", 257 filteredSet.size(), name + argNames.toString().replace('[', '(').replace(']', ')')); 258 } 259 return filteredSet; 260 } 261 } 262 263 /** 264 * Filters a list of methods to those that take the given set of arguments. 265 * 266 * @param methods list of methods to filter 267 * @param matchType whether the arguments are an exact match, a subset or a super set of method args 268 * @return methods with arguments that satisfy the match type.<p/> 269 * For SUPER_SET match, if methods with exact match are found, methods that take a subset are ignored 270 */ 271 public List<ApiMethod> filterMethods(List<? extends ApiMethod> methods, MatchType matchType) { 272 return filterMethods(methods, matchType, Collections.emptyList()); 273 } 274 275 /** 276 * Filters a list of methods to those that take the given set of arguments. 277 * 278 * @param methods list of methods to filter 279 * @param matchType whether the arguments are an exact match, a subset or a super set of method args 280 * @param argNames argument names to filter the list 281 * @return methods with arguments that satisfy the match type.<p/> 282 * For SUPER_SET match, if methods with exact match are found, methods that take a subset are ignored 283 */ 284 public List<ApiMethod> filterMethods(List<? extends ApiMethod> methods, MatchType matchType, Collection<String> argNames) { 285 // original arguments 286 // supplied arguments with missing nullable arguments 287 final List<String> withNullableArgsList; 288 if (!nullableArguments.isEmpty()) { 289 withNullableArgsList = new ArrayList<>(argNames); 290 withNullableArgsList.addAll(nullableArguments); 291 } else { 292 withNullableArgsList = null; 293 } 294 295 // list of methods that have all args in the given names 296 List<ApiMethod> result = new ArrayList<>(); 297 List<ApiMethod> extraArgs = null; 298 List<ApiMethod> nullArgs = null; 299 300 for (ApiMethod method : methods) { 301 final List<String> methodArgs = method.getArgNames(); 302 switch (matchType) { 303 case EXACT: 304 // method must take all args, and no more 305 if (methodArgs.containsAll(argNames) && argNames.containsAll(methodArgs)) { 306 result.add(method); 307 } 308 break; 309 case SUBSET: 310 // all args are required, method may take more 311 if (methodArgs.containsAll(argNames)) { 312 result.add(method); 313 } 314 break; 315 default: 316 case SUPER_SET: 317 // all method args must be present 318 if (argNames.containsAll(methodArgs)) { 319 if (methodArgs.containsAll(argNames)) { 320 // prefer exact match to avoid unused args 321 result.add(method); 322 } else if (result.isEmpty()) { 323 // if result is empty, add method to extra args list 324 if (extraArgs == null) { 325 extraArgs = new ArrayList<>(); 326 } 327 // method takes a subset, unused args 328 extraArgs.add(method); 329 } 330 } else if (result.isEmpty() && extraArgs == null) { 331 // avoid looking for nullable args by checking for empty result and extraArgs 332 if (withNullableArgsList != null && withNullableArgsList.containsAll(methodArgs)) { 333 if (nullArgs == null) { 334 nullArgs = new ArrayList<>(); 335 } 336 nullArgs.add(method); 337 } 338 } 339 break; 340 } 341 } 342 343 List<ApiMethod> methodList = result.isEmpty() 344 ? extraArgs == null 345 ? nullArgs 346 : extraArgs 347 : result; 348 349 // preference order is exact match, matches with extra args, matches with null args 350 return methodList != null ? Collections.unmodifiableList(methodList) : Collections.emptyList(); 351 } 352 353 /** 354 * Gets argument types and names for all overloaded methods and aliases with the given name. 355 * @param name method name, either an exact name or an alias, exact matches are checked first 356 * @return list of arguments of the form Class type1, String name1, Class type2, String name2,... 357 */ 358 public List<Object> getArguments(final String name) throws IllegalArgumentException { 359 List<Object> arguments = argumentsMap.get(name); 360 if (arguments == null) { 361 if (aliasesMap.containsKey(name)) { 362 arguments = new ArrayList<>(); 363 for (String method : aliasesMap.get(name)) { 364 arguments.addAll(argumentsMap.get(method)); 365 } 366 } 367 } 368 if (arguments == null) { 369 throw new IllegalArgumentException(name); 370 } 371 return Collections.unmodifiableList(arguments); 372 } 373 374 /** 375 * Get missing properties. 376 * @param methodName method name 377 * @param argNames available arguments 378 * @return Set of missing argument names 379 */ 380 public Set<String> getMissingProperties(String methodName, Set<String> argNames) { 381 final List<Object> argsWithTypes = getArguments(methodName); 382 final Set<String> missingArgs = new HashSet<>(); 383 384 for (int i = 1; i < argsWithTypes.size(); i += 2) { 385 final String name = (String) argsWithTypes.get(i); 386 if (!argNames.contains(name)) { 387 missingArgs.add(name); 388 } 389 } 390 391 return missingArgs; 392 } 393 394 /** 395 * Returns alias map. 396 * @return alias names mapped to method names. 397 */ 398 public Map<String, Set<String>> getAliases() { 399 return aliasesMap; 400 } 401 402 /** 403 * Returns argument types and names used by all methods. 404 * @return map with argument names as keys, and types as values 405 */ 406 public Map<String, Class<?>> allArguments() { 407 return validArguments; 408 } 409 410 /** 411 * Returns argument names that can be set to null if not specified. 412 * @return list of argument names 413 */ 414 public List<String> getNullableArguments() { 415 return nullableArguments; 416 } 417 418 /** 419 * Get the type for the given argument name. 420 * @param argName argument name 421 * @return argument type 422 */ 423 public Class<?> getType(String argName) throws IllegalArgumentException { 424 final Class<?> type = validArguments.get(argName); 425 if (type == null) { 426 throw new IllegalArgumentException(argName); 427 } 428 return type; 429 } 430 431 // this method is always called with Enum value lists, so the cast inside is safe 432 // the alternative of trying to convert ApiMethod and associated classes to generic classes would a bear!!! 433 @SuppressWarnings("unchecked") 434 public static ApiMethod getHighestPriorityMethod(List<? extends ApiMethod> filteredMethods) { 435 Comparable<ApiMethod> highest = null; 436 for (ApiMethod method : filteredMethods) { 437 if (highest == null || highest.compareTo(method) <= 0) { 438 highest = (Comparable<ApiMethod>)method; 439 } 440 } 441 return (ApiMethod)highest; 442 } 443 444 /** 445 * Invokes given method with argument values from given properties. 446 * 447 * @param proxy Proxy object for invoke 448 * @param method method to invoke 449 * @param properties Map of arguments 450 * @return result of method invocation 451 * @throws org.apache.camel.RuntimeCamelException on errors 452 */ 453 public static Object invokeMethod(Object proxy, ApiMethod method, Map<String, Object> properties) 454 throws RuntimeCamelException { 455 456 if (LOG.isDebugEnabled()) { 457 LOG.debug("Invoking {} with arguments {}", method.getName(), properties); 458 } 459 460 final List<String> argNames = method.getArgNames(); 461 final Object[] values = new Object[argNames.size()]; 462 final List<Class<?>> argTypes = method.getArgTypes(); 463 final Class<?>[] types = argTypes.toArray(new Class[argTypes.size()]); 464 int index = 0; 465 for (String name : argNames) { 466 Object value = properties.get(name); 467 468 // is the parameter an array type? 469 if (value != null && types[index].isArray()) { 470 Class<?> type = types[index]; 471 472 if (value instanceof Collection) { 473 // convert collection to array 474 Collection<?> collection = (Collection<?>) value; 475 Object array = Array.newInstance(type.getComponentType(), collection.size()); 476 if (array instanceof Object[]) { 477 collection.toArray((Object[]) array); 478 } else { 479 int i = 0; 480 for (Object el : collection) { 481 Array.set(array, i++, el); 482 } 483 } 484 value = array; 485 } else if (value.getClass().isArray() 486 && type.getComponentType().isAssignableFrom(value.getClass().getComponentType())) { 487 // convert derived array to super array if needed 488 if (type.getComponentType() != value.getClass().getComponentType()) { 489 final int size = Array.getLength(value); 490 Object array = Array.newInstance(type.getComponentType(), size); 491 for (int i = 0; i < size; i++) { 492 Array.set(array, i, Array.get(value, i)); 493 } 494 value = array; 495 } 496 } else { 497 throw new IllegalArgumentException( 498 String.format("Cannot convert %s to %s", value.getClass(), type)); 499 } 500 } 501 502 values[index++] = value; 503 } 504 505 try { 506 return method.getMethod().invoke(proxy, values); 507 } catch (Throwable e) { 508 if (e instanceof InvocationTargetException) { 509 // get API exception 510 final Throwable cause = e.getCause(); 511 e = (cause != null) ? cause : e; 512 } 513 throw new RuntimeCamelException( 514 String.format("Error invoking %s with %s: %s", method.getName(), properties, e.getMessage()), e); 515 } 516 } 517 518 public enum MatchType { 519 EXACT, SUBSET, SUPER_SET 520 } 521 522}