1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package net.wotonomy.foundation.internal;
20
21 import java.lang.reflect.Field;
22 import java.lang.reflect.InvocationTargetException;
23 import java.lang.reflect.Method;
24 import java.util.ArrayList;
25 import java.util.HashMap;
26 import java.util.Hashtable;
27 import java.util.Iterator;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Set;
31
32 /***
33 * This Introspector is a static utility class written to work
34 * around limitations in PropertyDescriptor and Introspector.<br><br>
35 *
36 * Of particular note are the get() and set() methods, which will attempt
37 * to get and set artibrary values on arbitrary objects to the best of its
38 * ability, converting values as appropriate. Properties of the form
39 * "property.nestedproperty.anotherproperty" are supported to get and set
40 * values on property values directly.<br><br>
41 *
42 * Note that for naming getter methods, this class supports "get", "is",
43 * and also the property name itself, which supports NeXT-style properties.
44 * Introspector supports Maps by treating the keys a property names,
45 * supports Lists by treating the indexes as property names. <br><br>
46 *
47 * Numeric and boolean types can be inverted by prepending a "!" before
48 * the name of the property, like "manager.!active" or "task.!lag".
49 *
50 * @author michael@mpowers.net
51 * @author $Author: cgruber $
52 * @version $Revision: 893 $
53 */
54
55 public class Introspector
56 {
57
58 public static boolean strict = false;
59
60
61 private static boolean debug = true;
62
63
64 public static final String SEPARATOR = ".";
65
66
67 private static Map getterMethods = new Hashtable();
68 private static Map setterMethods = new Hashtable();
69
70
71
72
73 public static Class WILD = Introspector.class;
74
75
76 private static final Class[] EMPTY_CLASS_ARRAY = new Class[0];
77
78
79 private static boolean useOGNL;
80
81 static
82 {
83 try
84 {
85 useOGNL = ( Class.forName( "ognl.Ognl" ) != null );
86 }
87 catch ( ClassNotFoundException t )
88 {
89 useOGNL = false;
90 }
91 }
92
93 /***
94 * Utility method to get the read method for a property belonging to a class.
95 * Will search for methods in the form of "getProperty" and failing that
96 * "isProperty" (to handle booleans).
97 * @param objectClass the class whose property methods will be retrieved.
98 * @param aProperty The property whose method will be retrieved.
99 * @param paramTypes An array of class objects representing the types of parameters.
100 * @return The appropriate method for the class, or null if not found.
101 */
102 static public Method getPropertyReadMethod(
103 Class objectClass, String aProperty, Class[] paramTypes )
104 {
105 Method result = null;
106
107 result = getMethodFromClass( objectClass, aProperty, paramTypes, true );
108
109 return result;
110 }
111
112 /***
113 * Utility method to get the write method for a property belonging to a class.
114 * Will search for methods in the form of "setProperty".
115 * @param objectClass the class whose property methods will be retrieved.
116 * @param aProperty The property whose method will be retrieved.
117 * @param paramTypes An array of class objects representing the types of parameters.
118 * @return The appropriate method for the class, or null if not found.
119 */
120 static public Method getPropertyWriteMethod(
121 Class objectClass, String aProperty, Class[] paramTypes )
122 {
123 Method result = null;
124
125 result = getMethodFromClass( objectClass, aProperty, paramTypes, false );
126
127 return result;
128 }
129
130 /***
131 * Gets a named method from a class. Using this method is preferred because
132 * the results are cached and should be faster than calling Class.getMethod().
133 * Note that if an object has a "get" getter method and an "is" getter method
134 * with the same signature defined for a given property. The "get" method
135 * is called.
136 * @param objectClass the Class whose property methods will be retrieved.
137 * @param aMethodName A String containing the name of the desired method.
138 * @param paramTypes An array of class objects representing the types of parameters.
139 * @return The appropriate Method from the Class, or null if not found.
140 */
141 static private Method getMethodFromClass(
142 Class objectClass, String aProperty, Class[] paramTypes, boolean doGetter)
143 {
144
145 Map classesToMethods = (doGetter ?
146 getterMethods :
147 setterMethods);
148
149 Map allMethods = (Map) classesToMethods.get( objectClass );
150 if (allMethods == null)
151 {
152
153 mapPropertiesForClass( objectClass );
154
155 allMethods = (Map) classesToMethods.get( objectClass );
156 }
157
158 Method[] methods = (Method[]) allMethods.get( aProperty );
159 if ( methods == null )
160 {
161 return null;
162 }
163
164 methods_loop:
165 for ( int i = 0; i < methods.length; i++ )
166 {
167 Class[] types = methods[i].getParameterTypes();
168
169
170 if ( types.length != paramTypes.length )
171 {
172
173 continue methods_loop;
174 }
175
176
177 for ( int j = 0; j < types.length; j++ )
178 {
179
180
181 if ( types[j].isPrimitive() )
182 {
183 if ( types[j] == Boolean.TYPE )
184 {
185 types[j] = Boolean.class;
186 }
187 else
188 if ( types[j] == Character.TYPE )
189 {
190 types[j] = Character.class;
191 }
192 else
193 if ( types[j] == Byte.TYPE )
194 {
195 types[j] = Byte.class;
196 }
197 else
198 if ( types[j] == Short.TYPE )
199 {
200 types[j] = Short.class;
201 }
202 else
203 if ( types[j] == Integer.TYPE )
204 {
205 types[j] = Integer.class;
206 }
207 else
208 if ( types[j] == Long.TYPE )
209 {
210 types[j] = Long.class;
211 }
212 else
213 if ( types[j] == Float.TYPE )
214 {
215 types[j] = Float.class;
216 }
217 else
218 if ( types[j] == Double.TYPE )
219 {
220 types[j] = Double.class;
221 }
222 }
223
224
225 if ( ( paramTypes[j] != WILD ) && ( ! types[j].isAssignableFrom( paramTypes[j] ) ) )
226 {
227
228
229 continue methods_loop;
230 }
231 }
232
233
234 return methods[i];
235 }
236
237
238 return null;
239 }
240
241 static private final Method[] getAllMethodsForClass( Class aClass )
242 {
243 Method[] local = aClass.getDeclaredMethods();
244 Method[] all = aClass.getMethods();
245 Method[] result = new Method[ local.length + all.length ];
246 System.arraycopy( local, 0, result, 0, local.length );
247 System.arraycopy( all, 0, result, local.length, all.length );
248 return result;
249 }
250
251 /***
252 * Generates a map of properties to both getter or setter methods for the given class.
253 * Then assigned those maps into the appropriate getterMethods and setterMethods maps
254 * keyed by the specified class. Even on error, this method will at least place empty
255 * property maps into each of the methods maps.
256 */
257 static private void mapPropertiesForClass( Class objectClass )
258 {
259 try
260 {
261 Map readProperties = new HashMap();
262 getterMethods.put( objectClass, readProperties );
263 Map writeProperties = new HashMap();
264 setterMethods.put( objectClass, writeProperties );
265
266 String name, property;
267 Method[] methods = getAllMethodsForClass( objectClass );
268 for ( int i = 0; i < methods.length; i++ )
269 {
270 name = methods[i].getName();
271 methods[i].setAccessible( true );
272 if ( name.startsWith( "set" ) )
273 {
274 name = name.substring( 3 );
275 if ( ! "".equals( name ) )
276 {
277 putMethodIntoPropertyMap( name, methods[i], writeProperties );
278 }
279 }
280 else
281 if ( methods[i].getReturnType() != void.class )
282 {
283 String fullname = name;
284 if ( name.startsWith( "get" ) )
285 {
286 name = name.substring( 3 );
287 }
288 else
289 if ( name.startsWith( "is" ) )
290 {
291 name = name.substring( 2 );
292 }
293 else
294 if ( name.startsWith( "has" ) && ( !strict ) )
295 {
296 name = name.substring( 3 );
297 }
298
299 if ( ! "".equals( name ) && ( !strict ) )
300 {
301 putMethodIntoPropertyMap( name, methods[i], readProperties );
302 if ( fullname != name )
303 {
304 putMethodIntoPropertyMap( fullname, methods[i], readProperties );
305 }
306 }
307 }
308 }
309 }
310 catch ( SecurityException se )
311 {
312 System.out.println( "Introspector.getMethodFromClass: " + se );
313
314 }
315 }
316
317 /***
318 * Places a property-method pair into one of the properties maps.
319 * This in effect maps a property to an array of methods.
320 */
321 private static void putMethodIntoPropertyMap( String aProperty, Method aMethod, Map aMap )
322 {
323
324 StringBuffer buffer = new StringBuffer( aProperty );
325 buffer.setCharAt(0, Character.toLowerCase(buffer.charAt(0)));
326 String key = buffer.toString();
327
328
329 Method[] result = (Method[]) aMap.get( key );
330 if ( result == null )
331 {
332 result = new Method[] { aMethod };
333 }
334 else
335 {
336
337 int i;
338 Method[] enlarged = new Method[ result.length + 1 ];
339 for ( i = 0; i < result.length; i ++ )
340 {
341 enlarged[i] = result[i];
342 }
343
344 enlarged[i] = aMethod;
345 result = enlarged;
346 }
347 aMap.put( key, result );
348 }
349
350 /***
351 * Utility method to get a method for a property belonging to a class.
352 * Use this if you don't feel like making the Class array from the parameters
353 * you will be using - pass in the parameters themselves.
354 * @param objectClass the Class whose property methods will be retrieved.
355 * @param aProperty The property whose method will be retrieved.
356 * @param params An array of parameters to be used.
357 * @return The appropriate method for the class, or null if not found.
358 */
359 static public Method getPropertyReadMethod(
360 Class objectClass, String aProperty, Object[] params )
361 {
362
363 if ( params.length == 0 )
364 {
365 return getPropertyReadMethod(
366 objectClass, aProperty, EMPTY_CLASS_ARRAY );
367 }
368
369 Class[] paramList = new Class[ params.length ];
370 for ( int i = 0; i < params.length; i++ )
371 {
372 if ( params[i] != null )
373 {
374 paramList[i] = params[i].getClass();
375 }
376 else
377 {
378 paramList[i] = WILD;
379 }
380 }
381 return getPropertyReadMethod( objectClass, aProperty, paramList );
382 }
383
384 /***
385 * Utility method to get a method for a property belonging to a class.
386 * Use this if you don't feel like making the Class array from the parameters
387 * you will be using - pass in the parameters themselves.
388 * @param objectClass the Class whose property methods will be retrieved.
389 * @param aProperty The property whose method will be retrieved.
390 * @param params An array of parameters to be used.
391 * @return The appropriate method for the class, or null if not found.
392 */
393 static public Method getPropertyWriteMethod(
394 Class objectClass, String aProperty, Object[] params )
395 {
396 Class[] paramList = new Class[ params.length ];
397 for ( int i = 0; i < params.length; i++ )
398 {
399 if ( params[i] != null )
400 {
401 paramList[i] = params[i].getClass();
402 }
403 else
404 {
405 paramList[i] = WILD;
406 }
407 }
408 return getPropertyWriteMethod( objectClass, aProperty, paramList );
409 }
410
411 /***
412 * Gets a list of the readable properties for the given class.
413 * Note that readable properties may not be writable - see getWriteProperties().
414 * @return An array of property names in no particular order
415 * where each name is a string with the first character in lower case.
416 */
417 public static String[] getReadPropertiesForClass( Class objectClass )
418 {
419 Map properties = (Map) getterMethods.get( objectClass );
420 if ( properties == null )
421 {
422
423 mapPropertiesForClass( objectClass );
424
425 properties = (Map) getterMethods.get( objectClass );
426 }
427
428
429 Set keys = properties.keySet();
430 Iterator it = keys.iterator();
431 int len = keys.size();
432 String[] result = new String[ len ];
433 for ( int i = 0; i < len; i++ )
434 {
435 result[i] = (String) it.next();
436 }
437 return result;
438 }
439
440 /***
441 * Gets a list of the writable properties for the given class.
442 * Note that writable properties may not be writable - see getReadProperties().
443 * @return An array of property names in no particular order
444 * where each name is a string with the first character in lower case.
445 */
446 public static String[] getWritePropertiesForClass( Class objectClass )
447 {
448 Map properties = (Map) setterMethods.get( objectClass );
449 if ( properties == null )
450 {
451
452 mapPropertiesForClass( objectClass );
453
454 properties = (Map) setterMethods.get( objectClass );
455 }
456
457
458 Set keys = properties.keySet();
459 Iterator it = keys.iterator();
460 int len = keys.size();
461 String[] result = new String[ len ];
462 for ( int i = 0; i < len; i++ )
463 {
464 result[i] = (String) it.next();
465 }
466 return result;
467 }
468
469 /***
470 * Gets a list of the readable properties for the given object, which may
471 * not be null. This method is more useful than getReadPropertiesForClass
472 * in that Maps will return their keys as properties and Lists will return
473 * their element indices as properties.
474 * Note that readable properties may not be writable - see getWriteProperties().
475 * @return An array of property names in no particular order
476 * where each name is a string with the first character in lower case.
477 */
478 public static String[] getReadPropertiesForObject( Object anObject )
479 {
480 List properties = new ArrayList();
481 String[] classProperties =
482 getReadPropertiesForClass( anObject.getClass() );
483 if ( anObject instanceof List )
484 {
485 properties.addAll( getPropertiesForList( (List) anObject ) );
486 }
487 if ( anObject instanceof Map )
488 {
489 properties.addAll( getPropertiesForMap( (Map) anObject ) );
490 }
491 int i;
492 int len = classProperties.length + properties.size();
493 String[] result = new String[ len ];
494 for ( i = 0; i < classProperties.length; i++ )
495 {
496 result[i] = classProperties[i];
497 }
498 Iterator it = properties.iterator();
499 while ( it.hasNext() )
500 {
501 result[i++] = it.next().toString();
502 }
503 return result;
504 }
505
506 /***
507 * Gets a list of the writable properties for the given object, which may
508 * not be null. This method is more useful than getWritePropertiesForClass
509 * in that Maps will return their keys as properties and Lists will return
510 * their element indices as properties.
511 * Note that writable properties may not be writable - see getReadProperties().
512 * @return An array of property names in no particular order
513 * where each name is a string with the first character in lower case.
514 */
515 public static String[] getWritePropertiesForObject( Object anObject )
516 {
517 List properties = new ArrayList();
518 String[] classProperties =
519 getWritePropertiesForClass( anObject.getClass() );
520 if ( anObject instanceof List )
521 {
522 properties.addAll( getPropertiesForList( (List) anObject ) );
523 }
524 if ( anObject instanceof Map )
525 {
526 properties.addAll( getPropertiesForMap( (Map) anObject ) );
527 }
528
529 int i;
530 int len = classProperties.length + properties.size();
531 String[] result = new String[ len ];
532 for ( i = 0; i < classProperties.length; i++ )
533 {
534 result[i] = classProperties[i];
535 }
536 Iterator it = properties.iterator();
537 while ( it.hasNext() )
538 {
539 result[i++] = it.next().toString();
540 }
541 return result;
542 }
543
544 private static List getPropertiesForList( List aList )
545 {
546 List result = new ArrayList();
547 int len = aList.size();
548 for ( int i = 0; i < len; i++ )
549 {
550 result.add( new Integer( i ).toString() );
551 }
552 return result;
553 }
554
555 private static List getPropertiesForMap( Map aMap )
556 {
557 List result = new ArrayList();
558 Iterator it = ((Map)aMap).keySet().iterator();
559 while ( it.hasNext() )
560 {
561 result.add( it.next().toString() );
562 }
563 return result;
564 }
565
566 private static Object[] EMPTY_ARRAY = new Object[0];
567
568 /***
569 * Convenience to get a value for a property from an object.
570 * An empty property string is considered the identity property
571 * and simply returns the object.
572 * @throws MissingPropertyException if the property cannot be
573 * found on the object.
574 */
575 public static Object getValueForObject( Object anObject, String aProperty )
576 {
577 if ( ( aProperty == null ) || ( "".equals( aProperty ) ) )
578 {
579 return anObject;
580 }
581
582 if ( useOGNL && aProperty.startsWith( "ognl:" ) )
583 {
584 try
585 {
586 return ognl.Ognl.getValue( aProperty, anObject );
587 }
588 catch ( Throwable t )
589 {
590 if ( debug )
591 {
592 System.err.println(
593 "Introspector.getValueForObject: "
594 + anObject + "' ( " + anObject.getClass() + " )"
595 + ", ognl:" + aProperty );
596 System.err.println( t );
597 }
598 return null;
599 }
600 }
601
602 boolean invert = false;
603 if ( aProperty.startsWith( "!" ) )
604 {
605 aProperty = aProperty.substring(1);
606 invert = true;
607 }
608
609 Object result = null;
610 try
611 {
612 Method m = Introspector.getPropertyReadMethod(
613 anObject.getClass(), aProperty, EMPTY_ARRAY );
614 if ( m != null )
615 {
616 result = m.invoke( anObject, EMPTY_ARRAY );
617 }
618 else
619 {
620 try
621 {
622 Field field = anObject.getClass().getDeclaredField( aProperty );
623 if ( field != null )
624 {
625 field.setAccessible( true );
626 result = field.get( anObject );
627 }
628 }
629 catch ( Throwable t )
630 {
631
632 }
633 }
634
635 if ( result == null )
636 {
637 if ( anObject instanceof Map )
638 {
639 result = ((Map)anObject).get( aProperty );
640 }
641 else
642 if ( anObject instanceof List )
643 {
644 result = ((List)anObject).get( Integer.parseInt( aProperty ) );
645 }
646 }
647
648 if ( invert )
649 {
650 Object inverted = ValueConverter.invert( result );
651 if ( inverted != null ) result = inverted;
652 }
653
654 return result;
655 }
656 catch ( Throwable exc )
657 {
658 if ( exc instanceof InvocationTargetException )
659 {
660 exc = ((InvocationTargetException)exc).getTargetException();
661 }
662 if ( exc instanceof RuntimeException )
663 {
664 throw (RuntimeException)exc;
665 }
666 if ( debug )
667 {
668 System.out.println(
669 "Introspector.getValueForObject: "
670 + anObject + "' ( " + anObject.getClass() + " )"
671 + ", " + aProperty + ": " );
672 }
673 throw new WotonomyException( exc );
674 }
675
676 }
677
678 /***
679 * Convenience to set a value for a property from an object.
680 * Returns the return value from executing the specified method,
681 * or null if the method returns type void.
682 * @throws MissingPropertyException if the property cannot be
683 * found on the object.
684 * @throws NullPrimitiveException if the property is of primitive
685 * type and the value is null.
686 */
687 public static Object setValueForObject(
688 Object anObject, String aProperty, Object aValue )
689 {
690 if ( useOGNL && aProperty.startsWith( "ognl:" ) )
691 {
692 try
693 {
694 ognl.Ognl.setValue( aProperty, anObject, aValue );
695 }
696 catch ( Throwable t )
697 {
698 if ( debug )
699 {
700 System.err.println(
701 "Introspector.setValueForObject: "
702 + anObject + "' ( " + anObject.getClass() + " )"
703 + ", ognl:" + aProperty + " : " + aValue );
704 System.err.println( t );
705 }
706 }
707 return null;
708 }
709
710 try
711 {
712 if ( aProperty.startsWith( "!" ) )
713 {
714 aProperty = aProperty.substring(1);
715 Object inverted = ValueConverter.invert( aValue );
716 if ( inverted != null ) aValue = inverted;
717 }
718
719 Method m = null;
720 if ( aValue != null )
721 {
722 m = Introspector.getPropertyWriteMethod(
723 anObject.getClass(), aProperty, new Class[] { aValue.getClass() } );
724 }
725 if ( m == null )
726 {
727 m = Introspector.getPropertyWriteMethod(
728 anObject.getClass(), aProperty, new Class[] { WILD } );
729 if ( ( m != null ) && ( aValue != null ) )
730 {
731
732 if ( ( aValue == null ) &&
733 ( m.getParameterTypes()[0].isPrimitive() ) )
734 {
735 throw new NullPrimitiveException();
736 }
737
738
739 Object o = ValueConverter.convertObjectToClass(
740 aValue, m.getParameterTypes()[0] );
741 if ( o != null )
742 {
743 aValue = o;
744 }
745 }
746 }
747 if ( m != null )
748 {
749 return m.invoke( anObject, new Object[] { aValue } );
750 }
751 else
752 {
753 try
754 {
755 Field field = anObject.getClass().getDeclaredField( aProperty );
756 if ( field != null )
757 {
758 field.setAccessible( true );
759 field.set( anObject, aValue );
760 return null;
761 }
762 }
763 catch ( Throwable t )
764 {
765
766 }
767 }
768
769 if ( anObject instanceof Map )
770 {
771 return ((Map)anObject).put( aProperty, aValue );
772 }
773 if ( anObject instanceof List )
774 {
775 List list = (List) anObject;
776 int i = Integer.parseInt( aProperty );
777 if ( list.size() < i+1 )
778 {
779
780 for ( int j = list.size(); j <= i; j++ )
781 {
782 list.add( new Object() );
783 }
784 }
785 return list.set( i, aValue );
786 }
787 }
788 catch ( Throwable exc )
789 {
790 if ( exc instanceof IllegalArgumentException )
791 {
792 System.out.println(
793 "Introspector.setValueForObject: "
794 + anObject + " , " + aProperty + " , '"
795 + aValue + "' ):" );
796 System.out.println( exc );
797 }
798 else
799 if ( exc instanceof InvocationTargetException )
800 {
801 exc = ((InvocationTargetException)exc).getTargetException();
802 }
803 if ( exc instanceof RuntimeException )
804 {
805 throw (RuntimeException)exc;
806 }
807 if ( debug )
808 {
809 System.out.println(
810 "Introspector.setValueForObject: "
811 + anObject + " , " + aProperty + " , '"
812 + aValue + "' ):" );
813 }
814 throw new WotonomyException( exc );
815 }
816 return null;
817
818 }
819
820 /***
821 * Gets a value from an object or any of its child objects.
822 * This will parse the property string for "."'s and get
823 * values for each successive object's property in the path.
824 * An empty property string is considered the identity property
825 * and simply returns the object.
826 */
827 public static Object get( Object anObject, String aProperty )
828 {
829 int i = aProperty.indexOf( SEPARATOR );
830 if ( i == -1 ) return getValueForObject( anObject, aProperty );
831
832 String pathElement = aProperty.substring( 0, i );
833 String remainder = aProperty.substring( i+1 );
834
835 Object result = getValueForObject( anObject, pathElement );
836 if ( result == null ) return null;
837 return get( result, remainder );
838 }
839
840 /***
841 * Sets a value in an object or any of its child objects.
842 * This will parse the property string for "."'s and set
843 * values for each successive object's property in the path.<br><br>
844 *
845 * If a property is not found, this method will try to
846 * implicitly create hash maps (if possible) to fill out the path.
847 * This is useful when dealing with trees of nested maps.
848 */
849 public static Object set( Object anObject, String aProperty, Object aValue )
850 {
851 int i = aProperty.indexOf( SEPARATOR );
852 if ( i == -1 ) return setValueForObject( anObject, aProperty, aValue );
853
854 String pathElement = aProperty.substring( 0, i );
855 String remainder = aProperty.substring( i+1 );
856
857 Object result = getValueForObject( anObject, pathElement );
858 if ( result == null )
859 {
860 result = new HashMap(2);
861 setValueForObject( anObject, pathElement, result );
862 }
863 return set( result, remainder, aValue );
864 }
865
866 /***
867 * If set to true, exceptions printed to System.out.println.
868 * Defaults to true.
869 */
870 public void setDebug( boolean isDebug )
871 {
872 debug = isDebug;
873 }
874 }
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941