/** * 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.component.facebook.data; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.Arrays; 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 facebook4j.Facebook; import facebook4j.FacebookException; import org.apache.camel.RuntimeCamelException; import org.apache.camel.component.facebook.config.FacebookNameStyle; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Helper class for working with {@link FacebookMethodsType}. */ public final class FacebookMethodsTypeHelper { private static final Logger LOG = LoggerFactory.getLogger(FacebookMethodsTypeHelper.class); // maps method name to FacebookMethodsType private static final Map<String, List<FacebookMethodsType>> METHOD_MAP = new HashMap<String, List<FacebookMethodsType>>(); // maps method name to method arguments of the form Class type1, String name1, Class type2, String name2,... private static final Map<String, List<Object>> ARGUMENTS_MAP = new HashMap<String, List<Object>>(); // maps argument name to argument type private static final Map<String, Class<?>> VALID_ARGUMENTS = new HashMap<String, Class<?>>(); static { final FacebookMethodsType[] methods = FacebookMethodsType.values(); // load lookup maps for FacebookMethodsType for (FacebookMethodsType method : methods) { // map method name to Enum final String name = method.getName(); List<FacebookMethodsType> overloads = METHOD_MAP.get(name); if (overloads == null) { overloads = new ArrayList<FacebookMethodsType>(); METHOD_MAP.put(method.getName(), overloads); } overloads.add(method); // add arguments for this method List<Object> arguments = ARGUMENTS_MAP.get(name); if (arguments == null) { arguments = new ArrayList<Object>(); ARGUMENTS_MAP.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, also detect clashes here final Class<?> previousType = VALID_ARGUMENTS.get(argName); if (previousType != null && previousType != argType) { throw new ExceptionInInitializerError(String.format( "Argument %s has ambiguous types (%s, %s) across methods!", name, previousType, argType)); } else if (previousType == null) { VALID_ARGUMENTS.put(argName, argType); } } } LOG.debug("Found {} unique method names in {} methods", METHOD_MAP.size(), methods.length); } private FacebookMethodsTypeHelper() { } /** * 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 full method name 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 static List<FacebookMethodsType> getCandidateMethods(String name, String... argNames) { final List<FacebookMethodsType> methods = METHOD_MAP.get(name); if (methods == null) { LOG.debug("No matching method for method {}", name); return Collections.emptyList(); } int nArgs = argNames != null ? argNames.length : 0; if (nArgs == 0) { LOG.debug("Found {} methods for method {}", methods.size(), name); return Collections.unmodifiableList(methods); } else { final List<FacebookMethodsType> filteredSet = filterMethods(methods, MatchType.SUBSET, argNames); if (LOG.isDebugEnabled()) { LOG.debug("Found {} filtered methods for {}", filteredSet.size(), name + Arrays.toString(argNames).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 * @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 static List<FacebookMethodsType> filterMethods(List<FacebookMethodsType> methods, MatchType matchType, String... argNames) { List<String> argsList = Arrays.asList(argNames); // list of methods that have all args in the given names final List<FacebookMethodsType> result = new ArrayList<FacebookMethodsType>(); final List<FacebookMethodsType> extraArgs = new ArrayList<FacebookMethodsType>(); for (FacebookMethodsType method : methods) { final List<String> methodArgs = method.getArgNames(); switch (matchType) { case EXACT: // method must take all args, and no more if (methodArgs.containsAll(argsList) && argsList.containsAll(methodArgs)) { result.add(method); } break; case SUBSET: // all args are required, method may take more if (methodArgs.containsAll(argsList)) { result.add(method); } break; default: case SUPER_SET: // all method args must be present if (argsList.containsAll(methodArgs)) { if (methodArgs.containsAll(argsList)) { // prefer exact match to avoid unused args result.add(method); } else { // method takes a subset, unused args extraArgs.add(method); } } break; } } return Collections.unmodifiableList(result.isEmpty() ? extraArgs : result); } /** * Gets argument types and names for all overloaded methods with the given name. * @param name method name, must be a long form (i.e. get*, or search*) * @return list of arguments of the form Class type1, String name1, Class type2, String name2,... */ public static List<Object> getArguments(String name) throws IllegalArgumentException { final List<Object> arguments = ARGUMENTS_MAP.get(name); if (arguments == null) { throw new IllegalArgumentException(name); } return Collections.unmodifiableList(arguments); } /** * Gets argument types and names for all overloaded methods with the given short form name. * @param name method name, may be a short form * @param style name style * @return list of arguments of the form Class type1, String name1, Class type2, String name2,... */ public static List<Object> getArgumentsForNameStyle(String name, FacebookNameStyle style) throws IllegalArgumentException { if (style == null) { throw new IllegalArgumentException("Parameters style cannot be null"); } switch (style) { case EXACT: return getArguments(name); case GET: return getArguments(convertToGetMethod(name)); case SEARCH: return getArguments(convertToSearchMethod(name)); case GET_AND_SEARCH: default: final List<Object> arguments = new ArrayList<Object>(); arguments.addAll(getArguments(convertToGetMethod(name))); arguments.addAll(getArguments(convertToSearchMethod(name))); return Collections.unmodifiableList(arguments); } } /** * Get missing properties. * @param methodName method name * @param nameStyle method name style * @param argNames available arguments * @return Set of missing argument names */ public static Set<String> getMissingProperties(String methodName, FacebookNameStyle nameStyle, Set<String> argNames) { final List<Object> argsWithTypes = getArgumentsForNameStyle(methodName, nameStyle); final Set<String> missingArgs = new HashSet<String>(); 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; } /** * Get argument types and names used by all methods. * @return map with argument names as keys, and types as values */ public static Map<String, Class<?>> allArguments() { return Collections.unmodifiableMap(VALID_ARGUMENTS); } /** * Get the type for the given argument name. * @param argName argument name * @return argument type */ public static Class<?> getType(String argName) throws IllegalArgumentException { final Class<?> type = VALID_ARGUMENTS.get(argName); if (type == null) { throw new IllegalArgumentException(argName); } return type; } public static String convertToGetMethod(String name) throws IllegalArgumentException { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("Name cannot be null or empty"); } return "get" + Character.toUpperCase(name.charAt(0)) + name.substring(1); } public static String convertToSearchMethod(String name) throws IllegalArgumentException { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("Name cannot be null or empty"); } return "search" + Character.toUpperCase(name.charAt(0)) + name.substring(1); } public static FacebookMethodsType getHighestPriorityMethod(List<FacebookMethodsType> filteredMethods) { FacebookMethodsType highest = null; for (FacebookMethodsType method : filteredMethods) { if (highest == null || method.ordinal() > highest.ordinal()) { highest = method; } } return highest; } /** * Invokes given method with argument values from given properties. * * @param facebook Facebook4J target object for invoke * @param method method to invoke * @param properties Map of arguments * @return result of method invocation * @throws RuntimeCamelException on errors */ public static Object invokeMethod(Facebook facebook, FacebookMethodsType method, Map<String, Object> properties) throws RuntimeCamelException { 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 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(facebook, values); } catch (Throwable e) { // skip wrapper exception to simplify stack String msg; if (e.getCause() != null && e.getCause() instanceof FacebookException) { e = e.getCause(); msg = ((FacebookException)e).getErrorMessage(); } else { msg = e.getMessage(); } throw new RuntimeCamelException( String.format("Error invoking %s with %s: %s", method.getName(), properties, msg), e); } } public enum MatchType { EXACT, SUBSET, SUPER_SET } }