/*
* @(#)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;
}
}