/* * @(#)Reflection.java * * $Date: 2015-05-28 11:10:57 -0700 (Thu, 28 May 2015) $ * * Copyright (c) 2011 by Jeremy Wood. * All rights reserved. * * The copyright of this software is owned by Jeremy Wood. * You may not use, copy or modify this software, except in * accordance with the license agreement you entered into with * Jeremy Wood. For details see accompanying license terms. * * This software is probably, but not necessarily, discussed here: * https://javagraphics.java.net/ * * That site should also contain the most recent official version * of this software. (See the SVN repository for more details.) */ package com.bric.reflect; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; import java.util.SortedSet; import java.util.Stack; import java.util.TreeSet; import java.util.Vector; import java.util.regex.Pattern; /** A set of static methods relating to reflection. * */ public class Reflection { /** Uses reflection to retrieve a static field from a class. */ public static Object getFieldValue(String className,String fieldName) { try { Class<?> c = Class.forName(className); Field f = c.getField(fieldName); return f.get(null); } catch(Throwable t) { return null; } } /** Return an array of objects based on a comma-separated description of its contents. */ protected static Object[] parseCommaSeparatedList(String arguments) { if(arguments.trim().length()==0) return new Object[] {}; List<String> listElements = new ArrayList<String>(); Stack<Character> constructs = new Stack<Character>(); int startingIndex = 0; for(int i = 0; i<arguments.length(); i++) { char ch = arguments.charAt(i); if(ch==',' && constructs.size()==0) { listElements.add(arguments.substring(startingIndex, i)); startingIndex = i+1; } else if(ch=='(' && (constructs.size()==0 || constructs.peek()!='"')) { constructs.add('('); } else if(ch==')' && constructs.size()>0 && constructs.peek()=='(') { constructs.pop(); } else if(ch=='"') { if(constructs.size()>0 && constructs.peek()=='"') { constructs.pop(); } else { constructs.add('"'); } } else if(ch=='\\' && constructs.size()>0 && constructs.peek()=='"') { i++; } } listElements.add(arguments.substring(startingIndex)); Object[] returnValue = new Object[listElements.size()]; for(int a = 0; a<returnValue.length; a++) { returnValue[a] = parse(listElements.get(a).trim()); } return returnValue; } static Pattern longPattern = Pattern.compile("\\d+L"); static Pattern intPattern = Pattern.compile("\\d+"); static Pattern floatPattern = Pattern.compile("[0-9]*\\.?[0-9]+"); static Pattern fieldPattern = Pattern.compile("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)*[a-zA-Z_$][a-zA-Z\\d_$]*"); static Pattern newPattern = Pattern.compile("new\\s+([a-zA-Z_$][a-zA-Z\\d_$]*\\.)*[a-zA-Z_$][a-zA-Z\\d_$]*\\(\\s*.*\\s*\\)"); static Pattern methodPattern = Pattern.compile("([a-zA-Z_$][a-zA-Z\\d_$]*\\.)*[a-zA-Z_$][a-zA-Z\\d_$]*\\(\\s*.*\\s*\\)"); static Pattern stringPattern = Pattern.compile("\"[\\.|[^\"]]*\""); /** Parse an object as null, an int, a long, a float, a String, a static field, or "new xyz(..)" (where ".." looks recursively for a comma-separated * list of arguments)). * <p>For example, you can call: * <br><code>parse( "new Color( 255, 0, 128 )" );</code> * <br><code>parse( "new com.bric.swing.resources.ArrowIcon( javax.swing.SwingConstants.EAST, 24, 24 )" );</code> * <br><code>parse( "new com.bric.swing.resources.TriangleIcon( javax.swing.SwingConstants.EAST, 24, 24, new Color(0)" );</code> * <p>The class name/constants must be fully qualified. */ public static Object parse(final String input) { String string = input.trim(); if("true".equalsIgnoreCase(string)) { return Boolean.TRUE; } else if("false".equalsIgnoreCase(string)) { return Boolean.FALSE; } else if("null".equalsIgnoreCase(string)) { return null; } else if(intPattern.matcher(string).matches()) { return Integer.parseInt(string); } else if(longPattern.matcher(string).matches()) { return Long.parseLong(string); } else if(floatPattern.matcher(string).matches()) { return Float.parseFloat(string); } else if(stringPattern.matcher(string).matches()) { return string.substring(1, string.length()-1 ); } else if(newPattern.matcher(string).matches()) { string = string.substring(3).trim(); int i1 = string.indexOf('('); int i2 = string.lastIndexOf(')'); String className = string.substring(0,i1); String arguments = string.substring(i1+1,i2); try { Class<?> t = Class.forName(className); return construct(t, arguments); } catch(ClassNotFoundException | SecurityException | IllegalArgumentException e) { throw new RuntimeException("An error occurred parsing \""+input+"\" as a field.", e); } } else if(methodPattern.matcher(string).matches()) { int i1 = string.indexOf('('); int i2 = string.lastIndexOf(')'); String lhs = string.substring(0,i1); String arguments = string.substring(i1+1,i2); i1 = lhs.lastIndexOf('.'); String className = lhs.substring(0,i1); String methodName = lhs.substring(i1+1); Object obj = null; try { Class<?> t = null; List<String> fieldNames = new ArrayList<String>(); while(t==null) { try { t = Class.forName(className); } catch(ClassNotFoundException e) { int i = className.lastIndexOf('.'); if(i==-1) { break; } String fieldName = className.substring(i+1); className = className.substring(0,i); fieldNames.add(fieldName); } } for(int a = 0; a<fieldNames.size(); a++) { if(obj!=null) t = obj.getClass(); obj = t.getField(fieldNames.get(0)).get(obj); } if(obj!=null) t = obj.getClass(); Method[] m = t.getMethods(); for(int a = 0; a<m.length; a++) { //TODO: consider overloaded method names, similar to how //we look for the best-fit constructor boolean isStatic = (m[a].getModifiers() | Modifier.STATIC) > 0; if(m[a].getName().equals(methodName) && isStatic ) { return m[a].invoke(obj, parseCommaSeparatedList(arguments)); } } } catch(SecurityException | IllegalArgumentException | IllegalAccessException | InvocationTargetException | NoSuchFieldException e) { throw new RuntimeException("An error occurred parsing \""+input+"\" as a method.", e); } throw new RuntimeException("Unrecognized method for "+input+"\""); } else if(fieldPattern.matcher(string).matches()) { int i = string.lastIndexOf('.'); String className = string.substring(0,i); if(className.length()==0) throw new RuntimeException("Unrecognized argument \""+input+"\". An attempt was made to parse as a field name, but no identifying class name was found."); String fieldName = string.substring(i+1); try { Class<?> t = Class.forName(className); if(fieldName.equals("class")) return t; Field f = t.getDeclaredField(fieldName); if( (f.getModifiers() & Modifier.STATIC)== 0) throw new RuntimeException("Unrecognized argument \""+input+"\". An attempt was made to parse as a field name, the field used was not static."); return f.get(null); } catch(ClassNotFoundException | NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { throw new RuntimeException("An error occurred parsing \""+input+"\" as a field.", e); } } throw new RuntimeException("Unrecognized argument \""+input+"\""); } /** This constructs an instance of t if a constructor is found * that matches the arguments provided. This will not identify * a varargs constructor. If multiple constructors match the arguments * provided, this method will choose one to use * * @param t the type of class to construct. * @param arguments a comma separated list of simple arguments, such * as primitives, strings, or static fields. * @return a new instance of t created using the arguments provided, * or an exception will be thrown. */ protected static <T> T construct(Class<T> t,String arguments) { Object[] argumentObjects = parseCommaSeparatedList(arguments); Constructor<?>[] constructors = t.getDeclaredConstructors(); class IncompatibleArgumentsException extends Exception { private static final long serialVersionUID = 1L; } class Match implements Comparable<Match> { Constructor<?> constructor; Object[] literalArguments; Object[] convertedArguments; double score = 0; Match(Constructor<?> constructor,Class<?>[] idealArgTypes,Object[] arguments) throws IncompatibleArgumentsException { this.constructor = constructor; this.literalArguments = arguments; this.convertedArguments = new Object[literalArguments.length]; for(int a = 0; a<idealArgTypes.length; a++) { Class<?> currentClass = arguments[a]==null ? null : arguments[a].getClass(); if(idealArgTypes[a].equals(currentClass)) { score += 1; convertedArguments[a] = arguments[a]; } else if( isInteger(idealArgTypes[a]) && isInteger(currentClass)) { score += 1; convertedArguments[a] = arguments[a]; } else if( isLong(idealArgTypes[a]) && isLong(currentClass)) { score += 1; convertedArguments[a] = arguments[a]; } else if( isFloat(idealArgTypes[a]) && isFloat(currentClass)) { score += 1; convertedArguments[a] = arguments[a]; } else if( isShort(idealArgTypes[a]) && isShort(currentClass)) { score += 1; convertedArguments[a] = arguments[a]; } else if( isChar(idealArgTypes[a]) && isChar(currentClass)) { score += 1; convertedArguments[a] = arguments[a]; } else if( isBoolean(idealArgTypes[a]) && isBoolean(currentClass)) { score += 1; convertedArguments[a] = arguments[a]; } else if( (!idealArgTypes[a].isPrimitive()) && currentClass==null ) { score += .95; convertedArguments[a] = null; } else if(idealArgTypes[a].isAssignableFrom(currentClass)) { score += .5; convertedArguments[a] = arguments[a]; } else if( isLong(idealArgTypes[a]) && isInteger(currentClass)) { score += .25; convertedArguments[a] = new Long( ((Integer)arguments[a]).longValue() ); } else if( isDouble(idealArgTypes[a]) && isInteger(currentClass)) { score += .25; convertedArguments[a] = new Double( ((Integer)arguments[a]).doubleValue() ); } else if( isFloat(idealArgTypes[a]) && isInteger(currentClass)) { score += .25; convertedArguments[a] = new Float( ((Integer)arguments[a]).floatValue() ); } else if( isShort(idealArgTypes[a]) && isInteger(currentClass)) { score += .25; convertedArguments[a] = new Short( ((Integer)arguments[a]).shortValue() ); } else if( isDouble(idealArgTypes[a]) && isFloat(currentClass)) { score += .15; convertedArguments[a] = new Double( ((Float)arguments[a]).doubleValue() ); } else { throw new IncompatibleArgumentsException(); } } score = score / ((double)idealArgTypes.length); } private boolean isInteger(Class<?> c) { return Integer.TYPE.equals(c) || Integer.class.equals(c); } private boolean isLong(Class<?> c) { return Long.TYPE.equals(c) || Long.class.equals(c); } private boolean isDouble(Class<?> c) { return Double.TYPE.equals(c) || Double.class.equals(c); } private boolean isFloat(Class<?> c) { return Float.TYPE.equals(c) || Float.class.equals(c); } private boolean isShort(Class<?> c) { return Short.TYPE.equals(c) || Short.class.equals(c); } private boolean isChar(Class<?> c) { return Character.TYPE.equals(c) || Character.class.equals(c); } private boolean isBoolean(Class<?> c) { return Boolean.TYPE.equals(c) || Boolean.class.equals(c); } @Override public int compareTo(Match o) { if(score<o.score) { return -1; } if(score>o.score) { return 1; } return 0; } } SortedSet<Match> matches = new TreeSet<Match>(); for(Constructor<?> constructor : constructors) { Class<?>[] argT = constructor.getParameterTypes(); if(argT.length==argumentObjects.length) { try { matches.add(new Match(constructor, argT, argumentObjects)); } catch(IncompatibleArgumentsException e) { //keep searching } } } if(matches.size()>0) { try { Match match = matches.last(); match.constructor.setAccessible(true); return (T)match.constructor.newInstance(match.convertedArguments); } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw new RuntimeException("an error occurred invoking a constructor for "+t.getName()+" using \""+arguments+"\"", e); } } throw new RuntimeException("no constructor for "+t.getName()+" was found that matched \""+arguments+"\""); } /** This object is returned by <code>invokeMethod(..)</code> when an error occurs. */ public static final Object INVOCATION_ERROR = new Object(); /** This uses reflection to call a method that may not exist in the compiling JVM. * * @return INVOCATION_ERROR if an error occurs (details are printed to the console), or the return * value of the invocation. */ public static Object invokeMethod(Class<?> c,Object obj,String methodName,Object[] arguments) { try { Method[] methods = c.getMethods(); for(int a = 0; a<methods.length; a++) { if(methods[a].getName().equals(methodName)) { try { return methods[a].invoke(obj, arguments); } catch(Throwable t) {} } } } catch(Throwable t) { t.printStackTrace(); } return INVOCATION_ERROR; } /** This debugging tool combs through a class and * tells you what public static field has the value * you've provided. * <P> For example, you can call: * <BR><code>nameStaticField(BufferedImage.class,new Integer(BufferedImage.TYPE_INT_ARGB))</code> * <BR>And this method will return "TYPE_INT_ARGB". * @param c the class of interest * @param value the value. Primitives must be wrapped. * @return the string of the field with that value, or * <code>null</code> if not hits were found. * (Or if multiple hits were found, this returns a * list of possible matches.) */ public static String nameStaticField(Class<?> c,Object value) { Field[] f = c.getDeclaredFields(); Vector<Field> v = new Vector<Field>(); for(int a = 0; a<f.length; a++) { if((f[a].getModifiers() & Modifier.STATIC)>0) { try { f[a].setAccessible(true); Object obj = f[a].get(null); if(obj!=null && obj.equals(value)) { v.add(f[a]); } } catch(IllegalAccessException e) {} } } if(v.size()==0) return null; if(v.size()==1) { return (v.get(0)).getName(); } //uh-oh, more than 1 field equalled the desired value... //could be the case a static float and a static int both //point to the number 1? int a = 0; while(a<v.size()) { try { Object obj = (v.get(a)).get(null); if(obj.getClass().equals(value.getClass())==false) { v.remove(a); } else { a++; } } catch(IllegalAccessException e) { return "An unexpected error occurred the second time I tried to access a field."; } } if(v.size()==1) { return (v.get(0)).getName(); } else if(v.size()>1) { return describe(v); } //last attempt: for(a = 0; a<v.size(); a++) { try { Object obj = (v.get(a)).get(null); if(obj.getClass().isInstance(value)) { return (v.get(a)).getName(); } } catch(IllegalAccessException e) { return "An unexpected error occurred the second time I tried to access a field."; } } return describe(v); } private static String describe(Vector<Field> v) { //we failed. try to give helpful info: String s = "[ "+(v.get(0)).getName(); for(int a = 1; a<v.size(); a++) { s+=", "+(v.get(a)).getName(); } s+=" ]"; return s; } }