001: package org.jicengine.operation;
002:
003: import java.lang.reflect.InvocationTargetException;
004: import java.lang.reflect.Method;
005: import java.lang.reflect.Constructor;
006: import java.lang.reflect.Field;
007: import java.lang.reflect.Modifier;
008: import java.util.Map;
009: import java.util.HashMap;
010:
011: /**
012: *
013: * <p>
014: * Copyright (C) 2004 Timo Laitinen
015: * </p>
016: * @author .timo
017: * @version 1.0
018: */
019:
020: public class ReflectionUtils {
021:
022: private static final String FIELD_CLASS = "class";
023:
024: /**
025: *
026: * @author timo
027: */
028: protected static class NoSuchMethodException extends
029: java.lang.NoSuchMethodException {
030: public NoSuchMethodException(Class actorClass, String methodName) {
031: super ("Class '" + actorClass.getName()
032: + "' doesn't have a method '" + methodName + "'.");
033: }
034: }
035:
036: /**
037: * Indicates that one or more methods with the right name were found but the
038: * method-parameters didn't match.
039: *
040: * @author timo
041: */
042: protected static class NoMethodWithSuchParametersException extends
043: java.lang.NoSuchMethodException {
044: public NoMethodWithSuchParametersException(Class actorClass,
045: String methodName, Object[] arguments) {
046: super ("Class '" + actorClass.getName()
047: + "' doesn't have a method '" + methodName
048: + " accepting arguments ("
049: + getArgumentTypeList(arguments) + ")");
050: }
051: }
052:
053: /**
054: *
055: *
056: * @author timo
057: * @created 29. elokuuta 2004
058: */
059: protected static class NoSuchConstructorException extends
060: java.lang.NoSuchMethodException {
061: public NoSuchConstructorException(Class actorClass,
062: Object[] arguments) {
063: super (
064: "Class '"
065: + actorClass.getName()
066: + "' doesn't have constructor accepting arguments ("
067: + getArgumentTypeList(arguments) + ")");
068: }
069: }
070:
071: private static Map objectTypesToPrimitiveTypes = new HashMap();
072: private static Map primitiveTypesToObjectTypes = new HashMap();
073:
074: private static final int ASSIGNABILITY_DIFFERENT_PARAMETER_COUNTS = -1;
075: private static final int ASSIGNABILITY_NON_ASSIGNABLE_PARAMETERS = 0;
076: private static final int ASSIGNABILITY_EXACT_MATCH = 1;
077:
078: /**
079: * <p>
080: * Mimics method <code>Class.isAssignableFrom</code>, but handles the
081: * conversions between primitives and primitive wrappers automatically.
082: * </p>
083: *
084: * @param class1 Class
085: * @param class2 Class
086: * @return boolean
087: */
088: public static boolean isAssignableFrom(Class class1, Class class2) {
089: if (class1.isAssignableFrom(class2)) {
090: return true;
091: } else {
092: Class primitive = primitiveWrapperToPrimitiveType(class2);
093: if (primitive != null) {
094: return class1.isAssignableFrom(primitive);
095: } else {
096: return false;
097: }
098: }
099: }
100:
101: public static void setFieldValue(Object instance, Class ownerClass,
102: String fieldName, Object fieldValue) throws Exception {
103: try {
104: Field field = ownerClass.getField(fieldName);
105: field.set(instance, fieldValue);
106:
107: } catch (NoSuchFieldException e) {
108: // for better error-message
109: throw new NoSuchFieldException("Field '" + fieldName
110: + "' not found in class '" + ownerClass.getName()
111: + "'.");
112: }
113: }
114:
115: /**
116: * @param instance the instance whose field is referenced. may be null, if
117: * the field in question is a static field.
118: *
119: * @param ownerClass the class that 'owns' the field.
120: * @param fieldName the name of the field.
121: */
122: public static Object getFieldValue(Object instance,
123: Class ownerClass, String fieldName) throws Exception {
124: if (instance == null && fieldName.equals(FIELD_CLASS)) {
125: // this is not a real static field but an expression
126: // like 'java.lang.String.class'.
127: // reflection does not understand the pseudo-field
128: // 'class', so we handle this situation here
129: // by simply returning the class.
130: return ownerClass;
131: } else {
132: try {
133: Field field = ownerClass.getField(fieldName);
134: return field.get(instance);
135: } catch (NoSuchFieldException e) {
136: // for better error-message
137: throw new NoSuchFieldException("Field '" + fieldName
138: + "' not found in class '"
139: + ownerClass.getName() + "'.");
140: }
141: }
142: }
143:
144: protected static Class getActorClass(Object instanceOrClass) {
145: if (instanceOrClass instanceof Class) {
146: return (Class) instanceOrClass;
147: } else {
148: return instanceOrClass.getClass();
149: }
150: }
151:
152: /**
153: * <p>
154: * Resolves the 'parameter assignability level' that a set of parameters
155: * has against a set of parameter types.
156: * </p>
157: * <p>
158: * The assignability level is returned as a int value that follows a little
159: * peculiar scheme:
160: * <ul>
161: * <li><b> -1 </b> - no match. even the number of parameters doesn't match. </li>
162: * <li><b> 0 </b> - no match. the number of parameter matches, but at least
163: * one of the parameters has a wrong type. </li>
164: * <li><b> 1 </b> - exact match. the parameter types match the runtime
165: * classes of the parameter objects exactly. </li>
166: * <li><b> 2,3,4,.. </b> - the parameters match. the parameters are assignable
167: * to the expected parameter types, but the expected types are superclasses
168: * or interfaces of the actual types of the parameter objects. the greater
169: * the number, the further away the parameter objects are. </li>
170: * </ul>
171: * <p>
172: * THEREFORE: a positive assignability level that is closer to 1 is better
173: * than a positive value that is further away from it. 1 is the best. values
174: * that are 0 or negative mean that the parameters are incompatible.
175: * </p>
176: *
177: * @param parameterTypes declared parameter types of a method/constructor.
178: * @param parameters runtime parameters given to the method/constructor,
179: * whose types are checked agains the declared types,
180: * in order to find out the correct method/constructor
181: * to call.
182: *
183: * @return The parameterAssignabilityLevel value
184: */
185: private static int getParameterAssignabilityLevel(
186: Class[] parameterTypes, Object[] parameters) {
187: if (parameterTypes.length == parameters.length) {
188: // the number of parameters matches. search further
189:
190: // exactly assignable by default
191: int assignability = ASSIGNABILITY_EXACT_MATCH;
192:
193: for (int i = 0; i < parameterTypes.length; i++) {
194: Class parameterType = parameterTypes[i];
195: Object parameter = parameters[i];
196: if (parameter == null) {
197: // we can't resolve the type of a null-parameter!
198: throw new IllegalArgumentException("parameter "
199: + (i + 1) + " was null.");
200: }
201:
202: Class candidateType = parameter.getClass();
203:
204: if (parameterType.equals(candidateType)) {
205: // ok, an exact match.
206: assignability *= ASSIGNABILITY_EXACT_MATCH;
207: } else if (parameterType
208: .isAssignableFrom(candidateType)) {
209: // not an exact match but a match anyways
210: // TODO: calculate the distance between these two classes.
211: assignability *= 2;
212: } else if (parameterType.isPrimitive()) {
213: // we have still hope: lets do the primitive conversion.
214: Class correspondingPrimitive = primitiveWrapperToPrimitiveType(candidateType);
215:
216: // if the required parameter type is a primitive, we can't simply
217: // test for equals instead of the assignable thing. int = int,
218: // there are no subclasses!
219: if (correspondingPrimitive != null
220: && correspondingPrimitive
221: .equals(parameterType)) {
222: // this is considered as an exact match.
223: assignability *= ASSIGNABILITY_EXACT_MATCH;
224: } else {
225: // no match, stop the search.
226: assignability *= ASSIGNABILITY_NON_ASSIGNABLE_PARAMETERS;
227: break;
228: }
229: } else {
230: // no exact match, no assignable match and no match after primitive
231: // conversions. certainly no match!
232: assignability *= ASSIGNABILITY_NON_ASSIGNABLE_PARAMETERS;
233: break;
234: }
235: }
236:
237: return assignability;
238: } else {
239: // even the number of parameters isn't exact. the worst case
240: return ASSIGNABILITY_DIFFERENT_PARAMETER_COUNTS;
241: }
242: }
243:
244: /**
245: * Invokes a method.
246: *
247: * @param methodName the name of the method, like
248: * 'addLayer' or 'setName'
249: * @param actor Description of the Parameter
250: * @param arguments Description of the Parameter
251: * @return an Object, if the invoked method
252: * returned something. null if the methods return-type is void.
253: * @throws NoSuchMethodException if no matching method was found
254: * @throws IllegalAccessException see Method.invoke()
255: * @throws IllegalArgumentException see Method.invoke()
256: * @throws InvocationTargetException if the invoked method throwed an
257: * Exception
258: */
259: public static Object invokeMethod(Object actor, String methodName,
260: Object[] arguments) throws java.lang.NoSuchMethodException,
261: IllegalAccessException, IllegalArgumentException,
262: InvocationTargetException {
263: if (actor == null) {
264: throw new NullPointerException("when calling method '"
265: + methodName + "' (with " + arguments.length
266: + " arguments)");
267: } else {
268: return invokeMethod(actor, actor.getClass(), methodName,
269: arguments);
270: }
271: }
272:
273: public static Object invokeStaticMethod(Class actorClass,
274: String methodName, Object[] arguments)
275: throws java.lang.NoSuchMethodException,
276: IllegalAccessException, IllegalArgumentException,
277: InvocationTargetException {
278: if (actorClass == null) {
279: throw new NullPointerException(
280: "when calling static method '" + methodName
281: + "' (with " + arguments.length
282: + " arguments)");
283: } else {
284: return invokeMethod(null, actorClass, methodName, arguments);
285: }
286: }
287:
288: public static Object instantiate(Class instantiatedClass,
289: Object[] arguments) throws java.lang.NoSuchMethodException,
290: InstantiationException, IllegalAccessException,
291: InvocationTargetException {
292: // find the constructor.
293: Constructor constructor = findConstructor(instantiatedClass,
294: arguments);
295:
296: // instantiate object
297: try {
298: return constructor.newInstance(arguments);
299: } catch (IllegalArgumentException e) {
300: throw new IllegalArgumentException(
301: "Problems instantiating with constructor '"
302: + constructor + "'");
303: }
304: }
305:
306: private static Constructor findConstructor(Class instantiatedClass,
307: Object[] arguments) throws java.lang.NoSuchMethodException,
308: InstantiationException, IllegalAccessException {
309: Constructor[] constructors = instantiatedClass
310: .getConstructors();
311: Constructor match = null;
312: int parameterAssignability = ASSIGNABILITY_NON_ASSIGNABLE_PARAMETERS;
313:
314: for (int i = 0; i < constructors.length; i++) {
315: Constructor candidate = constructors[i];
316: Class[] parameterTypes = candidate.getParameterTypes();
317:
318: int candidateParameterAssignability = getParameterAssignabilityLevel(
319: parameterTypes, arguments);
320:
321: if (candidateParameterAssignability > ASSIGNABILITY_NON_ASSIGNABLE_PARAMETERS) {
322: // parameters a assignable. find out how assignable
323:
324: if (candidateParameterAssignability == ASSIGNABILITY_EXACT_MATCH) {
325: // the parameters match exactly. we can stop the seach here!
326: match = candidate;
327: parameterAssignability = candidateParameterAssignability;
328: break;
329: } else if (parameterAssignability < ASSIGNABILITY_EXACT_MATCH
330: || candidateParameterAssignability < parameterAssignability) {
331: // not an exact match but a better match than our previous match
332: // (if had one).
333: // this is the best candidate so far, but we have to continue the
334: // search.
335: match = candidate;
336: parameterAssignability = candidateParameterAssignability;
337: continue;
338: }
339: }
340: }
341:
342: if (match == null) {
343: throw new NoSuchConstructorException(instantiatedClass,
344: arguments);
345: } else {
346: return match;
347: }
348: }
349:
350: /**
351: * @param objectClass Description of the Parameter
352: * @return the Class representing a primitive: if the argument
353: * Class is java.lang.Integer, returns Integer.TYPe, etc.
354: * null if the argument class is not a wrapper.
355: */
356: protected static Class primitiveWrapperToPrimitiveType(
357: Class objectClass) {
358: return (Class) objectTypesToPrimitiveTypes.get(objectClass);
359: }
360:
361: /**
362: * @param primitiveType Description of the Parameter
363: * @return null if the argument class is not a primitive type.
364: */
365: protected static Class primitiveTypeToWrapperType(
366: Class primitiveType) {
367: return (Class) primitiveTypesToObjectTypes.get(primitiveType);
368: }
369:
370: /**
371: *
372: * @param arguments Object[]
373: * @return String types in format [arg1 class], [arg2 class], etc.
374: */
375: protected static String getArgumentTypeList(Object[] arguments) {
376: String paramString = "";
377: for (int i = 0; i < arguments.length; i++) {
378: if (arguments[i] == null) {
379: paramString += "" + arguments[i];
380: } else {
381: // should array and primitive types be handled separately?
382: paramString += arguments[i].getClass().getName();
383: }
384:
385: if ((i + 1) < arguments.length) {
386: paramString += ",";
387: }
388: }
389: return paramString;
390: }
391:
392: /**
393: * a method for invoking both instance methods and static methods dynamically.
394: *
395: * @param actor the instance whose method is
396: * invoked. null, if a static method is in question. NOTE: the null means
397: * that the method must be static. filter unintentional null-values away
398: * before calling this method!
399: * @param actorClass the class that owns the invoked
400: * method. this should be the class of the actor.
401: * @param methodName name of the method
402: * @param parameters parameters as Object[]
403: * @return Description of the Return Value
404: * @throws java.lang.NoSuchMethodException Description of the Exception
405: * @throws IllegalAccessException Description of the Exception
406: * @throws IllegalArgumentException Description of the Exception
407: * @throws InvocationTargetException Description of the Exception
408: */
409: private static Object invokeMethod(Object actor, Class actorClass,
410: String methodName, Object[] arguments)
411: throws java.lang.NoSuchMethodException,
412: IllegalAccessException, IllegalArgumentException,
413: InvocationTargetException {
414: // some validity checks
415: if (methodName == null) {
416: throw new IllegalArgumentException(
417: "Can't invoke a method: method name was null");
418: }
419: if (arguments == null) {
420: throw new IllegalArgumentException(
421: "Can't invoke a method: arguments[] was null");
422: }
423:
424: if (!Modifier.isPublic(actorClass.getModifiers())) {
425: // problem: a non-public class. we have no permissions to read it.
426: // this support has to be implemented later.
427: throw new UnsupportedOperationException(
428: "Class '"
429: + actorClass.getName()
430: + "' is private or protected. only public classes are supported currently.");
431: }
432:
433: Method method;
434:
435: // we try first the method Class.getMethod().
436: // it works only if the types of the parameters match exactly the
437: // declared parameter types of a method in the owner class.
438: // but it is fast - its worth trying although it might result in
439: // exception.
440: //
441: // note: we could use some heuristics for deciding whether it is better
442: // to try getMethod() or manual search first.
443: try {
444: method = actorClass.getMethod(methodName,
445: getTypes(arguments));
446: } catch (java.lang.NoSuchMethodException e) {
447: method = findMethod(actorClass, methodName, arguments);
448: }
449:
450: // call the method and return the result.
451: // note: a better error message for situations where the method was supposed
452: // to be static but wasn't? now, only a nullpointer is thrown..
453: return method.invoke(actor, arguments);
454: }
455:
456: private static Method findMethod(Class actorClass,
457: String methodName, Object[] arguments)
458: throws java.lang.NoSuchMethodException,
459: IllegalAccessException, IllegalArgumentException,
460: InvocationTargetException {
461: Method[] methods = actorClass.getMethods();
462: Method match = null;
463: int parameterAssignability = ASSIGNABILITY_DIFFERENT_PARAMETER_COUNTS;
464: boolean foundMethodWithTheSameName = false;
465:
466: for (int i = 0; i < methods.length; i++) {
467: Method candidate = methods[i];
468: if (candidate.getName().equals(methodName)) {
469: // good, the name matches..
470: foundMethodWithTheSameName = true;
471: Class[] parameterTypes = candidate.getParameterTypes();
472:
473: int candidateParameterAssignability = getParameterAssignabilityLevel(
474: parameterTypes, arguments);
475:
476: if (candidateParameterAssignability > ASSIGNABILITY_NON_ASSIGNABLE_PARAMETERS) {
477: // parameters a assignable. find out how assignable
478:
479: if (candidateParameterAssignability == ASSIGNABILITY_EXACT_MATCH) {
480: // the parameters match exactly. we can stop the seach here!
481: match = candidate;
482: parameterAssignability = candidateParameterAssignability;
483: break;
484: } else if (parameterAssignability < ASSIGNABILITY_EXACT_MATCH
485: || candidateParameterAssignability < parameterAssignability) {
486: // not an exact match but a better match than our previous match
487: // (if had one).
488: // this is the best candidate so far, but we have to continue the
489: // search.
490:
491: // NOTE: 16.5.2005: is the test correct? shouldn't it use '>'
492: // instead of '<'..
493:
494: match = candidate;
495: parameterAssignability = candidateParameterAssignability;
496: continue;
497: }
498: }
499: }
500: }
501:
502: if (match == null) {
503: // no method found.
504: if (foundMethodWithTheSameName) {
505: throw new NoMethodWithSuchParametersException(
506: actorClass, methodName, arguments);
507: } else {
508: throw new ReflectionUtils.NoSuchMethodException(
509: actorClass, methodName);
510: }
511: } else {
512: return match;
513: }
514: }
515:
516: protected static Class[] getTypes(Object[] parameters) {
517: Class[] types = new Class[parameters.length];
518: for (int i = 0; i < parameters.length; i++) {
519: types[i] = parameters[i].getClass();
520: }
521: return types;
522: }
523:
524: static {
525: objectTypesToPrimitiveTypes.put(Double.class, Double.TYPE);
526: objectTypesToPrimitiveTypes.put(Integer.class, Integer.TYPE);
527: objectTypesToPrimitiveTypes.put(Long.class, Long.TYPE);
528: objectTypesToPrimitiveTypes
529: .put(Character.class, Character.TYPE);
530: objectTypesToPrimitiveTypes.put(Boolean.class, Boolean.TYPE);
531: objectTypesToPrimitiveTypes.put(Byte.class, Byte.TYPE);
532: objectTypesToPrimitiveTypes.put(Float.class, Float.TYPE);
533: objectTypesToPrimitiveTypes.put(Short.class, Short.TYPE);
534:
535: primitiveTypesToObjectTypes.put(Double.TYPE, Double.class);
536: primitiveTypesToObjectTypes.put(Integer.TYPE, Integer.class);
537: primitiveTypesToObjectTypes.put(Long.TYPE, Long.class);
538: primitiveTypesToObjectTypes
539: .put(Character.TYPE, Character.class);
540: primitiveTypesToObjectTypes.put(Boolean.TYPE, Boolean.class);
541: primitiveTypesToObjectTypes.put(Byte.TYPE, Byte.class);
542: primitiveTypesToObjectTypes.put(Float.TYPE, Float.class);
543: primitiveTypesToObjectTypes.put(Short.TYPE, Short.class);
544:
545: }
546: }
|