| /* |
| * 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.felix.gogo.runtime; |
| |
| import java.lang.annotation.Annotation; |
| import java.lang.reflect.Array; |
| import java.lang.reflect.InvocationTargetException; |
| import java.lang.reflect.Method; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| import org.apache.felix.service.command.CommandSession; |
| import org.apache.felix.service.command.Parameter; |
| |
| public final class Reflective |
| { |
| public final static Object NO_MATCH = new Object(); |
| public final static String MAIN = "_main"; |
| public final static Set<String> KEYWORDS = new HashSet<>( |
| Arrays.asList(new String[] { "abstract", "continue", "for", "new", "switch", |
| "assert", "default", "goto", "package", "synchronized", "boolean", "do", |
| "if", "private", "this", "break", "double", "implements", "protected", |
| "throw", "byte", "else", "import", "public", "throws", "case", "enum", |
| "instanceof", "return", "transient", "catch", "extends", "int", "short", |
| "try", "char", "final", "interface", "static", "void", "class", |
| "finally", "long", "strictfp", "volatile", "const", "float", "native", |
| "super", "while" })); |
| |
| /** |
| * invokes the named method on the given target using the supplied args, |
| * which are converted if necessary. |
| * @return the result of the invoked method |
| * @throws Exception |
| */ |
| public static Object invoke(CommandSession session, Object target, String name, |
| List<Object> args) throws Exception |
| { |
| Method[] methods = target.getClass().getMethods(); |
| name = name.toLowerCase(); |
| |
| String org = name; |
| String get = "get" + name; |
| String is = "is" + name; |
| String set = "set" + name; |
| |
| if (KEYWORDS.contains(name)) |
| { |
| name = "_" + name; |
| } |
| |
| if (target instanceof Class<?>) |
| { |
| Method[] staticMethods = ((Class<?>) target).getMethods(); |
| for (Method m : staticMethods) |
| { |
| String mname = m.getName().toLowerCase(); |
| if (mname.equals(name) || mname.equals(get) || mname.equals(set) |
| || mname.equals(is) || mname.equals(MAIN)) |
| { |
| methods = staticMethods; |
| break; |
| } |
| } |
| } |
| |
| Method bestMethod = null; |
| Object[] bestArgs = null; |
| int lowestMatch = Integer.MAX_VALUE; |
| ArrayList<Class<?>[]> possibleTypes = new ArrayList<>(); |
| |
| for (Method m : methods) |
| { |
| String mname = m.getName().toLowerCase(); |
| if (mname.equals(name) || mname.equals(get) || mname.equals(set) |
| || mname.equals(is) || mname.equals(MAIN)) |
| { |
| Class<?>[] types = m.getParameterTypes(); |
| ArrayList<Object> xargs = new ArrayList<>(args); |
| |
| // pass command name as argv[0] to main, so it can handle |
| // multiple commands |
| if (mname.equals(MAIN)) |
| { |
| xargs.add(0, org); |
| } |
| |
| Object[] parms = new Object[types.length]; |
| int match = coerce(session, target, m, types, parms, xargs); |
| |
| if (match < 0) |
| { |
| // coerce failed |
| possibleTypes.add(types); |
| } |
| else |
| { |
| if (match < lowestMatch) |
| { |
| lowestMatch = match; |
| bestMethod = m; |
| bestArgs = parms; |
| } |
| |
| if (match == 0) |
| break; // can't get better score |
| } |
| } |
| } |
| |
| if (bestMethod != null) |
| { |
| bestMethod.setAccessible(true); |
| try |
| { |
| return bestMethod.invoke(target, bestArgs); |
| } |
| catch (InvocationTargetException e) |
| { |
| Throwable cause = e.getCause(); |
| if (cause instanceof Exception) |
| { |
| throw (Exception) cause; |
| } |
| throw e; |
| } |
| } |
| else |
| { |
| ArrayList<String> list = new ArrayList<>(); |
| for (Class<?>[] types : possibleTypes) |
| { |
| StringBuilder buf = new StringBuilder(); |
| buf.append('('); |
| for (Class<?> type : types) |
| { |
| if (buf.length() > 1) |
| { |
| buf.append(", "); |
| } |
| buf.append(type.getSimpleName()); |
| } |
| buf.append(')'); |
| list.add(buf.toString()); |
| } |
| |
| StringBuilder params = new StringBuilder(); |
| for (Object arg : args) |
| { |
| if (params.length() > 1) |
| { |
| params.append(", "); |
| } |
| params.append(arg == null ? "null" : arg.getClass().getSimpleName()); |
| } |
| |
| throw new IllegalArgumentException(String.format( |
| "Cannot coerce %s(%s) to any of %s", name, params, list)); |
| } |
| } |
| |
| /** |
| * transform name/value parameters into ordered argument list. |
| * params: --param2, value2, --flag1, arg3 |
| * args: true, value2, arg3 |
| * @return new ordered list of args. |
| */ |
| private static List<Object> transformParameters(Method method, List<Object> in) |
| { |
| Annotation[][] pas = method.getParameterAnnotations(); |
| ArrayList<Object> out = new ArrayList<>(); |
| ArrayList<Object> parms = new ArrayList<>(in); |
| |
| for (Annotation as[] : pas) |
| { |
| for (Annotation a : as) |
| { |
| if (a instanceof Parameter) |
| { |
| int i = -1; |
| Parameter p = (Parameter) a; |
| for (String name : p.names()) |
| { |
| i = parms.indexOf(name); |
| if (i >= 0) |
| break; |
| } |
| |
| if (i >= 0) |
| { |
| // parameter present |
| parms.remove(i); |
| Object value = p.presentValue(); |
| if (Parameter.UNSPECIFIED.equals(value)) |
| { |
| if (i >= parms.size()) |
| return null; // missing parameter, so try other methods |
| value = parms.remove(i); |
| } |
| out.add(value); |
| } |
| else |
| { |
| out.add(p.absentValue()); |
| } |
| |
| } |
| } |
| } |
| |
| out.addAll(parms); |
| |
| return out; |
| } |
| |
| /** |
| * Complex routein to convert the arguments given from the command line to |
| * the arguments of the method call. First, an attempt is made to convert |
| * each argument. If this fails, a check is made to see if varargs can be |
| * applied. This happens when the last method argument is an array. |
| * @return -1 if arguments can't be coerced; 0 if no coercion was necessary; |
| * > 0 if coercion was needed. |
| */ |
| private static int coerce(CommandSession session, Object target, Method m, |
| Class<?> types[], Object out[], List<Object> in) |
| { |
| in = transformParameters(m, in); |
| if (in == null) |
| { |
| // missing parameter argument? |
| return -1; |
| } |
| |
| int[] convert = { 0 }; |
| |
| // Check if the command takes a session |
| if ((types.length > 0) && types[0].isInterface() |
| && types[0].isAssignableFrom(session.getClass())) |
| { |
| in.add(0, session); |
| } |
| |
| int i = 0; |
| while (i < out.length) |
| { |
| out[i] = null; |
| |
| // Try to convert one argument |
| if (in.size() == 0 || i == types.length - 1 && types[i].isArray() && in.size() > 1) |
| { |
| out[i] = NO_MATCH; |
| } |
| else |
| { |
| out[i] = coerce(session, types[i], in.get(0), convert); |
| |
| if (out[i] == null && types[i].isArray() && in.size() > 0) |
| { |
| // don't coerce null to array FELIX-2432 |
| out[i] = NO_MATCH; |
| } |
| |
| if (out[i] != NO_MATCH) |
| { |
| in.remove(0); |
| } |
| } |
| |
| if (out[i] == NO_MATCH) |
| { |
| // No match, check for varargs |
| if (types[i].isArray() && (i == types.length - 1)) |
| { |
| // Try to parse the remaining arguments in an array |
| Class<?> ctype = types[i].getComponentType(); |
| int asize = in.size(); |
| Object array = Array.newInstance(ctype, asize); |
| int n = i; |
| while (in.size() > 0) |
| { |
| Object t = coerce(session, ctype, in.remove(0), convert); |
| if (t == NO_MATCH) |
| { |
| return -1; |
| } |
| Array.set(array, i - n, t); |
| i++; |
| } |
| out[n] = array; |
| |
| /* |
| * 1. prefer f() to f(T[]) with empty array |
| * 2. prefer f(T) to f(T[1]) |
| * 3. prefer f(T) to f(Object[1]) even if there is a conversion cost for T |
| * |
| * 1 & 2 require to add 1 to conversion cost, but 3 also needs to match |
| * the conversion cost for T. |
| */ |
| return convert[0] + 1 + (asize * 2); |
| } |
| return -1; |
| } |
| i++; |
| } |
| |
| if (in.isEmpty()) |
| return convert[0]; |
| return -1; |
| } |
| |
| /** |
| * converts given argument to specified type and increments convert[0] if any conversion was needed. |
| * @param convert convert[0] is incremented according to the conversion needed, |
| * to allow the "best" conversion to be determined. |
| * @return converted arg or NO_MATCH if no conversion possible. |
| */ |
| public static Object coerce(CommandSession session, Class<?> type, Object arg, |
| int[] convert) |
| { |
| if (arg == null) |
| { |
| return null; |
| } |
| |
| if (type.isAssignableFrom(arg.getClass())) |
| { |
| return arg; |
| } |
| |
| if (type.isArray()) |
| { |
| return NO_MATCH; |
| } |
| |
| if (type.isPrimitive() && arg instanceof Long) |
| { |
| // no-cost conversions between integer types |
| Number num = (Number) arg; |
| |
| if (type == short.class) |
| { |
| return num.shortValue(); |
| } |
| if (type == int.class) |
| { |
| return num.intValue(); |
| } |
| if (type == long.class) |
| { |
| return num.longValue(); |
| } |
| } |
| |
| // all following conversions cost 2 points |
| convert[0] += 2; |
| |
| Object converted = ((CommandSessionImpl) session).doConvert(type, arg); |
| if (converted != null) |
| { |
| return converted; |
| } |
| |
| String string = toString(arg); |
| |
| if (type.isAssignableFrom(String.class)) |
| { |
| return string; |
| } |
| |
| if (type.isEnum()) |
| { |
| for (Object o : type.getEnumConstants()) |
| { |
| if (o.toString().equalsIgnoreCase(string)) |
| { |
| return o; |
| } |
| } |
| } |
| |
| if (type.isPrimitive()) |
| { |
| type = primitiveToObject(type); |
| } |
| |
| try |
| { |
| return type.getConstructor(String.class).newInstance(string); |
| } |
| catch (Exception e) |
| { |
| } |
| |
| if (type == Character.class && string.length() == 1) |
| { |
| return string.charAt(0); |
| } |
| |
| return NO_MATCH; |
| } |
| |
| private static String toString(Object arg) |
| { |
| if (arg instanceof Map) |
| { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("["); |
| boolean first = true; |
| for (Map.Entry<?,?> entry : ((Map<?,?>) arg).entrySet()) |
| { |
| if (!first) { |
| sb.append(" "); |
| } |
| first = false; |
| writeValue(sb, entry.getKey()); |
| sb.append("="); |
| writeValue(sb, entry.getValue()); |
| } |
| sb.append("]"); |
| return sb.toString(); |
| } |
| else if (arg instanceof Collection) |
| { |
| StringBuilder sb = new StringBuilder(); |
| sb.append("["); |
| boolean first = true; |
| for (Object o : ((Collection) arg)) |
| { |
| if (!first) { |
| sb.append(" "); |
| } |
| first = false; |
| writeValue(sb, o); |
| } |
| sb.append("]"); |
| return sb.toString(); |
| } |
| else |
| { |
| return arg.toString(); |
| } |
| } |
| |
| private static void writeValue(StringBuilder sb, Object o) { |
| if (o == null || o instanceof Boolean || o instanceof Number) |
| { |
| sb.append(o); |
| } |
| else |
| { |
| String s = o.toString(); |
| sb.append("\""); |
| for (int i = 0; i < s.length(); i++) |
| { |
| char c = s.charAt(i); |
| if (c == '\"' || c == '=') |
| { |
| sb.append("\\"); |
| } |
| sb.append(c); |
| } |
| sb.append("\""); |
| } |
| } |
| |
| private static Class<?> primitiveToObject(Class<?> type) |
| { |
| if (type == boolean.class) |
| { |
| return Boolean.class; |
| } |
| if (type == byte.class) |
| { |
| return Byte.class; |
| } |
| if (type == char.class) |
| { |
| return Character.class; |
| } |
| if (type == short.class) |
| { |
| return Short.class; |
| } |
| if (type == int.class) |
| { |
| return Integer.class; |
| } |
| if (type == float.class) |
| { |
| return Float.class; |
| } |
| if (type == double.class) |
| { |
| return Double.class; |
| } |
| if (type == long.class) |
| { |
| return Long.class; |
| } |
| return null; |
| } |
| |
| } |