blob: d0a97f984dbc4dfe37c18775dcbbc967a88e8472 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.support.component;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.camel.RuntimeCamelException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Helper class for working with {@link ApiMethod}.
*/
public final class ApiMethodHelper<T extends Enum<T> & ApiMethod> {
private static final Logger LOG = LoggerFactory.getLogger(ApiMethodHelper.class);
// maps method name to ApiMethod
private final Map<String, List<T>> methodMap;
// maps method name to method arguments of the form Class type1, String name1, Class type2, String name2,...
private final Map<String, List<Object>> argumentsMap;
// maps argument name to argument type
private final Map<String, Class<?>> validArguments;
// maps aliases to actual method names
private final Map<String, Set<String>> aliasesMap;
// nullable args
private final List<String> nullableArguments;
/**
* Create a helper to work with a {@link ApiMethod}, using optional method aliases.
* @param apiMethodEnum {@link ApiMethod} enumeration class
* @param aliases Aliases mapped to actual method names
* @param nullableArguments names of arguments that default to null value
*/
public ApiMethodHelper(Class<T> apiMethodEnum, Map<String, String> aliases, List<String> nullableArguments) {
Map<String, List<T>> tmpMethodMap = new HashMap<>();
Map<String, List<Object>> tmpArgumentsMap = new HashMap<>();
Map<String, Class<?>> tmpValidArguments = new HashMap<>();
Map<String, Set<String>> tmpAliasesMap = new HashMap<>();
// validate ApiMethod Enum
if (apiMethodEnum == null) {
throw new IllegalArgumentException("ApiMethod enumeration cannot be null");
}
if (nullableArguments != null && !nullableArguments.isEmpty()) {
this.nullableArguments = Collections.unmodifiableList(new ArrayList<>(nullableArguments));
} else {
this.nullableArguments = Collections.emptyList();
}
final Map<Pattern, String> aliasPatterns = new HashMap<>();
for (Map.Entry<String, String> alias : aliases.entrySet()) {
if (alias.getKey() == null || alias.getValue() == null) {
throw new IllegalArgumentException("Alias pattern and replacement cannot be null");
}
aliasPatterns.put(Pattern.compile(alias.getKey()), alias.getValue());
}
LOG.debug("Processing {}", apiMethodEnum.getName());
final T[] methods = apiMethodEnum.getEnumConstants();
// load lookup maps
for (T method : methods) {
final String name = method.getName();
// add method name aliases
for (Map.Entry<Pattern, String> aliasEntry : aliasPatterns.entrySet()) {
final Matcher matcher = aliasEntry.getKey().matcher(name);
if (matcher.find()) {
// add method name alias
String alias = matcher.replaceAll(aliasEntry.getValue());
// convert first character to lowercase
assert alias.length() > 1;
final char firstChar = alias.charAt(0);
if (!Character.isLowerCase(firstChar)) {
final StringBuilder builder = new StringBuilder();
builder.append(Character.toLowerCase(firstChar)).append(alias.substring(1));
alias = builder.toString();
}
Set<String> names = tmpAliasesMap.get(alias);
if (names == null) {
names = new HashSet<>();
tmpAliasesMap.put(alias, names);
}
names.add(name);
}
}
// map method name to Enum
List<T> overloads = tmpMethodMap.get(name);
if (overloads == null) {
overloads = new ArrayList<>();
tmpMethodMap.put(method.getName(), overloads);
}
overloads.add(method);
// add arguments for this method
List<Object> arguments = tmpArgumentsMap.get(name);
if (arguments == null) {
arguments = new ArrayList<>();
tmpArgumentsMap.put(name, arguments);
}
// process all arguments for this method
final int nArgs = method.getArgNames().size();
final String[] argNames = method.getArgNames().toArray(new String[nArgs]);
final Class<?>[] argTypes = method.getArgTypes().toArray(new Class[nArgs]);
for (int i = 0; i < nArgs; i++) {
final String argName = argNames[i];
final Class<?> argType = argTypes[i];
if (!arguments.contains(argName)) {
arguments.add(argType);
arguments.add(argName);
}
// also collect argument names for all methods, and detect clashes here
final Class<?> previousType = tmpValidArguments.get(argName);
if (previousType != null && previousType != argType) {
throw new IllegalArgumentException(String.format(
"Argument %s has ambiguous types (%s, %s) across methods!",
name, previousType, argType));
} else if (previousType == null) {
tmpValidArguments.put(argName, argType);
}
}
}
// validate nullableArguments
if (!tmpValidArguments.keySet().containsAll(this.nullableArguments)) {
List<String> unknowns = new ArrayList<>(this.nullableArguments);
unknowns.removeAll(tmpValidArguments.keySet());
throw new IllegalArgumentException("Unknown nullable arguments " + unknowns.toString());
}
// validate aliases
for (Map.Entry<String, Set<String>> entry : tmpAliasesMap.entrySet()) {
// look for aliases that match multiple methods
final Set<String> methodNames = entry.getValue();
if (methodNames.size() > 1) {
// get mapped methods
final List<T> aliasedMethods = new ArrayList<>();
for (String methodName : methodNames) {
List<T> mappedMethods = tmpMethodMap.get(methodName);
aliasedMethods.addAll(mappedMethods);
}
// look for argument overlap
for (T method : aliasedMethods) {
final List<String> argNames = new ArrayList<>(method.getArgNames());
argNames.removeAll(this.nullableArguments);
final Set<T> ambiguousMethods = new HashSet<>();
for (T otherMethod : aliasedMethods) {
if (method != otherMethod) {
final List<String> otherArgsNames = new ArrayList<>(otherMethod.getArgNames());
otherArgsNames.removeAll(this.nullableArguments);
if (argNames.equals(otherArgsNames)) {
ambiguousMethods.add(method);
ambiguousMethods.add(otherMethod);
}
}
}
if (!ambiguousMethods.isEmpty()) {
throw new IllegalArgumentException(
String.format("Ambiguous alias %s for methods %s", entry.getKey(), ambiguousMethods));
}
}
}
}
this.methodMap = Collections.unmodifiableMap(tmpMethodMap);
this.argumentsMap = Collections.unmodifiableMap(tmpArgumentsMap);
this.validArguments = Collections.unmodifiableMap(tmpValidArguments);
this.aliasesMap = Collections.unmodifiableMap(tmpAliasesMap);
LOG.debug("Found {} unique method names in {} methods", tmpMethodMap.size(), methods.length);
}
/**
* Gets methods that match the given name and arguments.<p/>
* Note that the args list is a required subset of arguments for returned methods.
*
* @param name case sensitive method name or alias to lookup
* @return non-null unmodifiable list of methods that take all of the given arguments, empty if there is no match
*/
public List<ApiMethod> getCandidateMethods(String name) {
return getCandidateMethods(name, Collections.emptyList());
}
/**
* Gets methods that match the given name and arguments.<p/>
* Note that the args list is a required subset of arguments for returned methods.
*
* @param name case sensitive method name or alias to lookup
* @param argNames unordered required argument names
* @return non-null unmodifiable list of methods that take all of the given arguments, empty if there is no match
*/
public List<ApiMethod> getCandidateMethods(String name, Collection<String> argNames) {
List<T> methods = methodMap.get(name);
if (methods == null) {
if (aliasesMap.containsKey(name)) {
methods = new ArrayList<>();
for (String method : aliasesMap.get(name)) {
methods.addAll(methodMap.get(method));
}
}
}
if (methods == null) {
LOG.debug("No matching method for method {}", name);
return Collections.emptyList();
}
int nArgs = argNames != null ? argNames.size() : 0;
if (nArgs == 0) {
LOG.debug("Found {} methods for method {}", methods.size(), name);
return Collections.unmodifiableList(methods);
} else {
final List<ApiMethod> filteredSet = filterMethods(methods, MatchType.SUBSET, argNames);
if (LOG.isDebugEnabled()) {
LOG.debug("Found {} filtered methods for {}",
filteredSet.size(), name + argNames.toString().replace('[', '(').replace(']', ')'));
}
return filteredSet;
}
}
/**
* Filters a list of methods to those that take the given set of arguments.
*
* @param methods list of methods to filter
* @param matchType whether the arguments are an exact match, a subset or a super set of method args
* @return methods with arguments that satisfy the match type.<p/>
* For SUPER_SET match, if methods with exact match are found, methods that take a subset are ignored
*/
public List<ApiMethod> filterMethods(List<? extends ApiMethod> methods, MatchType matchType) {
return filterMethods(methods, matchType, Collections.emptyList());
}
/**
* Filters a list of methods to those that take the given set of arguments.
*
* @param methods list of methods to filter
* @param matchType whether the arguments are an exact match, a subset or a super set of method args
* @param argNames argument names to filter the list
* @return methods with arguments that satisfy the match type.<p/>
* For SUPER_SET match, if methods with exact match are found, methods that take a subset are ignored
*/
public List<ApiMethod> filterMethods(List<? extends ApiMethod> methods, MatchType matchType, Collection<String> argNames) {
// original arguments
// supplied arguments with missing nullable arguments
final List<String> withNullableArgsList;
if (!nullableArguments.isEmpty()) {
withNullableArgsList = new ArrayList<>(argNames);
withNullableArgsList.addAll(nullableArguments);
} else {
withNullableArgsList = null;
}
// list of methods that have all args in the given names
List<ApiMethod> result = new ArrayList<>();
List<ApiMethod> extraArgs = null;
List<ApiMethod> nullArgs = null;
for (ApiMethod method : methods) {
final List<String> methodArgs = method.getArgNames();
switch (matchType) {
case EXACT:
// method must take all args, and no more
if (methodArgs.containsAll(argNames) && argNames.containsAll(methodArgs)) {
result.add(method);
}
break;
case SUBSET:
// all args are required, method may take more
if (methodArgs.containsAll(argNames)) {
result.add(method);
}
break;
default:
case SUPER_SET:
// all method args must be present
if (argNames.containsAll(methodArgs)) {
if (methodArgs.containsAll(argNames)) {
// prefer exact match to avoid unused args
result.add(method);
} else if (result.isEmpty()) {
// if result is empty, add method to extra args list
if (extraArgs == null) {
extraArgs = new ArrayList<>();
}
// method takes a subset, unused args
extraArgs.add(method);
}
} else if (result.isEmpty() && extraArgs == null) {
// avoid looking for nullable args by checking for empty result and extraArgs
if (withNullableArgsList != null && withNullableArgsList.containsAll(methodArgs)) {
if (nullArgs == null) {
nullArgs = new ArrayList<>();
}
nullArgs.add(method);
}
}
break;
}
}
List<ApiMethod> methodList = result.isEmpty()
? extraArgs == null
? nullArgs
: extraArgs
: result;
// preference order is exact match, matches with extra args, matches with null args
return methodList != null ? Collections.unmodifiableList(methodList) : Collections.emptyList();
}
/**
* Gets argument types and names for all overloaded methods and aliases with the given name.
* @param name method name, either an exact name or an alias, exact matches are checked first
* @return list of arguments of the form Class type1, String name1, Class type2, String name2,...
*/
public List<Object> getArguments(final String name) throws IllegalArgumentException {
List<Object> arguments = argumentsMap.get(name);
if (arguments == null) {
if (aliasesMap.containsKey(name)) {
arguments = new ArrayList<>();
for (String method : aliasesMap.get(name)) {
arguments.addAll(argumentsMap.get(method));
}
}
}
if (arguments == null) {
throw new IllegalArgumentException(name);
}
return Collections.unmodifiableList(arguments);
}
/**
* Get missing properties.
* @param methodName method name
* @param argNames available arguments
* @return Set of missing argument names
*/
public Set<String> getMissingProperties(String methodName, Set<String> argNames) {
final List<Object> argsWithTypes = getArguments(methodName);
final Set<String> missingArgs = new HashSet<>();
for (int i = 1; i < argsWithTypes.size(); i += 2) {
final String name = (String) argsWithTypes.get(i);
if (!argNames.contains(name)) {
missingArgs.add(name);
}
}
return missingArgs;
}
/**
* Returns alias map.
* @return alias names mapped to method names.
*/
public Map<String, Set<String>> getAliases() {
return aliasesMap;
}
/**
* Returns argument types and names used by all methods.
* @return map with argument names as keys, and types as values
*/
public Map<String, Class<?>> allArguments() {
return validArguments;
}
/**
* Returns argument names that can be set to null if not specified.
* @return list of argument names
*/
public List<String> getNullableArguments() {
return nullableArguments;
}
/**
* Get the type for the given argument name.
* @param argName argument name
* @return argument type
*/
public Class<?> getType(String argName) throws IllegalArgumentException {
final Class<?> type = validArguments.get(argName);
if (type == null) {
throw new IllegalArgumentException(argName);
}
return type;
}
// this method is always called with Enum value lists, so the cast inside is safe
// the alternative of trying to convert ApiMethod and associated classes to generic classes would a bear!!!
@SuppressWarnings("unchecked")
public static ApiMethod getHighestPriorityMethod(List<? extends ApiMethod> filteredMethods) {
Comparable<ApiMethod> highest = null;
for (ApiMethod method : filteredMethods) {
if (highest == null || highest.compareTo(method) <= 0) {
highest = (Comparable<ApiMethod>)method;
}
}
return (ApiMethod)highest;
}
/**
* Invokes given method with argument values from given properties.
*
* @param proxy Proxy object for invoke
* @param method method to invoke
* @param properties Map of arguments
* @return result of method invocation
* @throws org.apache.camel.RuntimeCamelException on errors
*/
public static Object invokeMethod(Object proxy, ApiMethod method, Map<String, Object> properties)
throws RuntimeCamelException {
if (LOG.isDebugEnabled()) {
LOG.debug("Invoking {} with arguments {}", method.getName(), properties);
}
final List<String> argNames = method.getArgNames();
final Object[] values = new Object[argNames.size()];
final List<Class<?>> argTypes = method.getArgTypes();
final Class<?>[] types = argTypes.toArray(new Class[argTypes.size()]);
int index = 0;
for (String name : argNames) {
Object value = properties.get(name);
// is the parameter an array type?
if (value != null && types[index].isArray()) {
Class<?> type = types[index];
if (value instanceof Collection) {
// convert collection to array
Collection<?> collection = (Collection<?>) value;
Object array = Array.newInstance(type.getComponentType(), collection.size());
if (array instanceof Object[]) {
collection.toArray((Object[]) array);
} else {
int i = 0;
for (Object el : collection) {
Array.set(array, i++, el);
}
}
value = array;
} else if (value.getClass().isArray()
&& type.getComponentType().isAssignableFrom(value.getClass().getComponentType())) {
// convert derived array to super array if needed
if (type.getComponentType() != value.getClass().getComponentType()) {
final int size = Array.getLength(value);
Object array = Array.newInstance(type.getComponentType(), size);
for (int i = 0; i < size; i++) {
Array.set(array, i, Array.get(value, i));
}
value = array;
}
} else {
throw new IllegalArgumentException(
String.format("Cannot convert %s to %s", value.getClass(), type));
}
}
values[index++] = value;
}
try {
return method.getMethod().invoke(proxy, values);
} catch (Throwable e) {
if (e instanceof InvocationTargetException) {
// get API exception
final Throwable cause = e.getCause();
e = (cause != null) ? cause : e;
}
throw new RuntimeCamelException(
String.format("Error invoking %s with %s: %s", method.getName(), properties, e.getMessage()), e);
}
}
public enum MatchType {
EXACT, SUBSET, SUPER_SET
}
}