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}