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}