All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.camel.util.component.ApiMethodHelper Maven / Gradle / Ivy

There is a newer version: 4.6.0
Show newest version
/**
 * 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.util.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 & ApiMethod> {

    private static final Logger LOG = LoggerFactory.getLogger(ApiMethodHelper.class);

    // maps method name to ApiMethod
    private final Map> methodMap;

    // maps method name to method arguments of the form Class type1, String name1, Class type2, String name2,...
    private final Map> argumentsMap;

    // maps argument name to argument type
    private final Map> validArguments;

    // maps aliases to actual method names
    private final Map> aliasesMap;

    // nullable args
    private final List 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 apiMethodEnum, Map aliases, List nullableArguments) {

        Map> tmpMethodMap = new HashMap<>();
        Map> tmpArgumentsMap = new HashMap<>();
        Map> tmpValidArguments = new HashMap<>();
        Map> 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 aliasPatterns = new HashMap();
        for (Map.Entry 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 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 names = tmpAliasesMap.get(alias);
                    if (names == null) {
                        names = new HashSet();
                        tmpAliasesMap.put(alias, names);
                    }
                    names.add(name);
                }
            }

            // map method name to Enum
            List overloads = tmpMethodMap.get(name);
            if (overloads == null) {
                overloads = new ArrayList();
                tmpMethodMap.put(method.getName(), overloads);
            }
            overloads.add(method);

            // add arguments for this method
            List 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 unknowns = new ArrayList(this.nullableArguments);
            unknowns.removeAll(tmpValidArguments.keySet());
            throw new IllegalArgumentException("Unknown nullable arguments " + unknowns.toString());
        }

        // validate aliases
        for (Map.Entry> entry : tmpAliasesMap.entrySet()) {

            // look for aliases that match multiple methods
            final Set methodNames = entry.getValue();
            if (methodNames.size() > 1) {

                // get mapped methods
                final List aliasedMethods = new ArrayList();
                for (String methodName : methodNames) {
                    List mappedMethods = tmpMethodMap.get(methodName);
                    aliasedMethods.addAll(mappedMethods);
                }

                // look for argument overlap
                for (T method : aliasedMethods) {
                    final List argNames = new ArrayList(method.getArgNames());
                    argNames.removeAll(this.nullableArguments);

                    final Set ambiguousMethods = new HashSet();
                    for (T otherMethod : aliasedMethods) {
                        if (method != otherMethod) {
                            final List 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.

* 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 getCandidateMethods(String name) { return getCandidateMethods(name, Collections.emptyList()); } /** * Gets methods that match the given name and arguments.

* 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 getCandidateMethods(String name, Collection argNames) { List 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 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.

* For SUPER_SET match, if methods with exact match are found, methods that take a subset are ignored */ public List filterMethods(List 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.

* For SUPER_SET match, if methods with exact match are found, methods that take a subset are ignored */ public List filterMethods(List methods, MatchType matchType, Collection argNames) { // original arguments // supplied arguments with missing nullable arguments final List 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 result = new ArrayList<>(); List extraArgs = null; List nullArgs = null; for (ApiMethod method : methods) { final List 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 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 getArguments(final String name) throws IllegalArgumentException { List 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 getMissingProperties(String methodName, Set argNames) { final List argsWithTypes = getArguments(methodName); final Set 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> 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> allArguments() { return validArguments; } /** * Returns argument names that can be set to null if not specified. * @return list of argument names */ public List 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 filteredMethods) { Comparable highest = null; for (ApiMethod method : filteredMethods) { if (highest == null || highest.compareTo(method) <= 0) { highest = (Comparable)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 properties) throws RuntimeCamelException { if (LOG.isDebugEnabled()) { LOG.debug("Invoking {} with arguments {}", method.getName(), properties); } final List argNames = method.getArgNames(); final Object[] values = new Object[argNames.size()]; final List> 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 } }