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