1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package org.kuali.rice.krad.uif.util;
17
18 import java.beans.PropertyEditor;
19 import java.lang.reflect.Array;
20 import java.lang.reflect.InvocationTargetException;
21 import java.lang.reflect.Method;
22 import java.lang.reflect.ParameterizedType;
23 import java.lang.reflect.Type;
24 import java.util.List;
25 import java.util.Map;
26
27 import org.apache.log4j.Logger;
28 import org.kuali.rice.krad.datadictionary.Copyable;
29 import org.kuali.rice.krad.uif.util.ObjectPathExpressionParser.PathEntry;
30 import org.kuali.rice.krad.util.KRADUtils;
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 public class ObjectPropertyReference {
46
47
48
49
50 private static final boolean DEFAULT_BOOLEAN = false;
51 private static final byte DEFAULT_BYTE = 0;
52 private static final short DEFAULT_SHORT = 0;
53 private static final int DEFAULT_INT = 0;
54 private static final long DEFAULT_LONG = 0L;
55 private static final float DEFAULT_FLOAT = 0.0f;
56 private static final double DEFAULT_DOUBLE = 0.0d;
57 private static final char DEFAULT_CHAR = '\u0000';
58
59
60
61
62 private static final Logger LOG = Logger.getLogger(ObjectPropertyReference.class);
63
64
65
66
67 private static final ThreadLocal<ObjectPropertyReference> TL_BUILDER_REF = new ThreadLocal<ObjectPropertyReference>();
68
69
70
71
72 private static final ThreadLocal<Boolean> TL_WARN = new ThreadLocal<Boolean>();
73
74
75
76
77
78 private static final ReferencePathEntry LOOKUP_REF_PATH_ENTRY = new ReferencePathEntry(false);
79
80
81
82
83 private static final ReferencePathEntry MUTATE_REF_PATH_ENTRY = new ReferencePathEntry(true);
84
85
86
87
88 private static final class ReferencePathEntry implements PathEntry {
89
90
91
92
93
94 private final boolean grow;
95
96
97
98
99 private ReferencePathEntry(boolean grow) {
100 this.grow = grow;
101 }
102
103
104
105
106
107
108 @Override
109 public Object parse(String parentPath, Object node, String next) {
110 ObjectPropertyReference current = (ObjectPropertyReference) node;
111
112
113
114
115 if (next == null) {
116 ObjectPropertyReference resolved = new ObjectPropertyReference();
117 resolved.rootBean = current.bean;
118 resolved.rootPath = current.rootPath;
119 resolved.bean = current.bean;
120 resolved.beanClass = current.beanClass;
121 resolved.beanType = current.beanType;
122 resolved.name = null;
123 resolved.parentPath = null;
124 return resolved;
125 }
126
127
128
129 Class<?> beanClass = current.getPropertyType();
130 Object bean = current.get();
131 if (bean instanceof Copyable) {
132 bean = CopyUtils.unwrap((Copyable) bean);
133 if (!beanClass.isInstance(bean)) {
134 beanClass = bean.getClass();
135 }
136 }
137
138
139
140 Type beanType;
141 Method readMethod = ObjectPropertyUtils.getReadMethod(current.getImplClass(), current.name);
142 if (readMethod == null) {
143 beanType = beanClass;
144 } else {
145 beanType = readMethod.getGenericReturnType();
146 }
147
148
149 if (grow) {
150 Object newBean = initialize(bean, beanClass);
151 if (newBean != bean) {
152 current.set(newBean);
153 Object verify;
154 assert (verify = current.get()) == newBean : verify + " != " + newBean;
155 bean = newBean;
156 }
157 }
158
159
160 current.bean = bean;
161 current.beanClass = beanClass;
162 current.beanType = beanType;
163 current.name = next;
164 current.parentPath = parentPath;
165
166 return current;
167 }
168 }
169
170
171
172
173
174
175
176
177 private static Object initialize(Object propertyValue, Class<?> propertyType) {
178 Object returnValue = propertyValue;
179
180 if (propertyValue == null) {
181 if (List.class.equals(propertyType)) {
182 returnValue = new java.util.LinkedList<Object>();
183
184 } else if (Map.class.equals(propertyType)) {
185 returnValue = new java.util.HashMap<Object, Object>();
186
187 } else if (!String.class.equals(propertyType)) {
188 try {
189 returnValue = propertyType.newInstance();
190 } catch (InstantiationException e) {
191 throw new IllegalStateException("Failed to create new object for setting property value", e);
192 } catch (IllegalAccessException e) {
193 throw new IllegalStateException("Failed to create new object for setting property value", e);
194 }
195 }
196 }
197
198 return returnValue;
199 }
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215 private static Object getArray(Object array, String name) {
216 if (array == null) {
217 return null;
218 }
219
220 for (int i = 0; i < name.length(); i++) {
221 if (!Character.isDigit(name.charAt(i))) {
222 return null;
223 }
224 }
225
226 int i = Integer.parseInt(name);
227
228 if (i >= Array.getLength(array)) {
229 return null;
230 }
231
232 return Array.get(array, i);
233 }
234
235
236
237
238
239
240
241
242 private static void setArray(Object array, String name, Object value) {
243 Array.set(array, Integer.parseInt(name), value);
244 }
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260 private static Object getList(List<?> list, String name) {
261 int length;
262 if (list == null) {
263 length = 0;
264 } else {
265 length = list.size();
266 }
267
268 for (int i = 0; i < name.length(); i++) {
269 if (!Character.isDigit(name.charAt(i))) {
270 return null;
271 }
272 }
273
274 int i = Integer.parseInt(name);
275 if (i >= length) {
276 return null;
277 }
278
279 return list.get(i);
280 }
281
282
283
284
285
286
287
288
289 @SuppressWarnings("unchecked")
290 private static void setList(List<?> list, String name, Object value) {
291 int i = Integer.parseInt(name);
292 while (i >= list.size()) {
293 list.add(null);
294 }
295 ((List<Object>) list).set(i, value);
296 }
297
298
299
300
301
302
303
304
305 private static Object getMap(Map<?, ?> map, String name) {
306 if (map != null && map.containsKey(name)) {
307 return map.get(name);
308 }
309 return null;
310 }
311
312
313
314
315
316
317
318 public static boolean isWarning() {
319 return Boolean.TRUE.equals(TL_WARN.get());
320 }
321
322
323
324
325
326
327
328 public static void setWarning(boolean warning) {
329 if (warning) {
330 TL_WARN.set(true);
331 } else {
332 TL_WARN.remove();
333 }
334 }
335
336
337
338
339
340
341
342
343
344
345
346 public static ObjectPropertyReference resolvePath(Object bean, Class<?> beanClass, String propertyPath, boolean grow) {
347 if (ObjectPathExpressionParser.isPath(propertyPath)) {
348
349
350
351 ObjectPropertyReference reference = new ObjectPropertyReference();
352 reference.beanClass = beanClass;
353 reference.rootPath = propertyPath;
354 if (bean instanceof Copyable) {
355 reference.bean = CopyUtils.unwrap((Copyable) bean);
356 reference.rootBean = reference.bean;
357 if (!(beanClass.isInstance(reference.bean))) {
358 reference.beanClass = reference.bean.getClass();
359 }
360 } else {
361 reference.bean = bean;
362 reference.rootBean = bean;
363 }
364
365 ObjectPropertyReference resolved = (ObjectPropertyReference) ObjectPathExpressionParser
366 .parsePathExpression(reference, propertyPath,
367 grow ? MUTATE_REF_PATH_ENTRY : LOOKUP_REF_PATH_ENTRY);
368
369 reference.bean = resolved.bean;
370 reference.beanClass = resolved.beanClass;
371 reference.beanType = resolved.beanType;
372 reference.name = resolved.name;
373 return reference;
374
375 } else {
376
377 return resolveProperty(bean, beanClass, propertyPath);
378
379 }
380 }
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397 public static ObjectPropertyReference resolveProperty(Object bean, Class<?> beanClass, String propertyPath) {
398 ObjectPropertyReference reference = TL_BUILDER_REF.get();
399 if (reference == null) {
400 reference = new ObjectPropertyReference();
401 TL_BUILDER_REF.set(reference);
402 }
403 reference.beanClass = beanClass;
404 if (bean instanceof Copyable) {
405 reference.bean = CopyUtils.unwrap((Copyable) bean);
406 if (!(beanClass.isInstance(reference.bean)) && reference.bean != null) {
407 reference.beanClass = reference.bean.getClass();
408 }
409 } else {
410 reference.bean = bean;
411 }
412 reference.rootBean = reference.bean;
413 reference.rootPath = propertyPath;
414 reference.beanType = reference.beanClass;
415 reference.name = propertyPath;
416 return reference;
417 }
418
419
420
421
422 private Object rootBean;
423
424
425
426
427 private Object bean;
428
429
430
431
432 private Class<?> beanClass;
433
434
435
436
437 private Type beanType;
438
439
440
441
442 private String name;
443
444
445
446
447 private String parentPath;
448
449
450
451
452 private String rootPath;
453
454
455
456
457 private ObjectPropertyReference() {}
458
459
460
461
462
463
464
465 private Object convertStringToPropertyType(String propertyValue) {
466 Class<?> propertyType = getPropertyType();
467
468
469
470
471 if (List.class.equals(propertyType)) {
472 return KRADUtils.convertStringParameterToList(propertyValue);
473
474 } else if (Map.class.equals(propertyType)) {
475 return KRADUtils.convertStringParameterToMap(propertyValue);
476
477 } else {
478
479 PropertyEditor editor = ObjectPropertyUtils.getPropertyEditor(rootBean, rootPath);
480 if (editor == null) {
481 throw new IllegalArgumentException("No property editor available for converting '" + propertyValue
482 + "' to " + propertyType);
483 }
484
485 editor.setAsText((String) propertyValue);
486 return editor.getValue();
487 }
488
489 }
490
491
492
493
494
495
496
497 private Object convertPropertyValueToString(Object propertyValue) {
498
499
500
501
502
503 if (propertyValue instanceof List) {
504 StringBuilder listStringBuilder = new StringBuilder();
505 for (Object item : (List<?>) propertyValue) {
506 if (listStringBuilder.length() > 0) {
507 listStringBuilder.append(',');
508 }
509 listStringBuilder.append((String) item);
510 }
511 return listStringBuilder.toString();
512
513 } else if (propertyValue instanceof Map) {
514 @SuppressWarnings("unchecked")
515 Map<String, String> mapPropertyValue = (Map<String, String>) propertyValue;
516 return KRADUtils.buildMapParameterString(mapPropertyValue);
517
518 } else {
519
520 PropertyEditor editor = ObjectPropertyUtils
521 .getPropertyEditor(ObjectPropertyUtils.getPrimitiveType(propertyValue.getClass()));
522 if (editor == null) {
523 throw new IllegalArgumentException("No property editor available for converting '" + propertyValue
524 + "' from " + propertyValue.getClass());
525 }
526
527 editor.setValue(propertyValue);
528 return editor.getAsText();
529 }
530 }
531
532
533
534
535
536
537
538 private Object convertToPropertyType(Object propertyValue) {
539 Class<?> propertyType = getPropertyType();
540
541 if (propertyValue == null) {
542 return primitiveDefault(propertyType);
543 }
544
545 if (propertyType.isInstance(propertyValue)) {
546 return propertyValue;
547 }
548
549 if (propertyValue instanceof String) {
550 return convertStringToPropertyType((String) propertyValue);
551 }
552
553 if (propertyType.equals(String.class)) {
554 return convertPropertyValueToString(propertyValue);
555 }
556
557 return propertyValue;
558 }
559
560
561
562
563
564
565
566 private Object primitiveDefault(Class<?> object) {
567 if (!object.isPrimitive()){
568 return null;
569 } else if (object.equals(boolean.class)) {
570 return DEFAULT_BOOLEAN;
571 } else if (object.equals(byte.class)) {
572 return DEFAULT_BYTE;
573 } else if (object.equals(char.class)) {
574 return DEFAULT_CHAR;
575 } else if (object.equals(short.class)) {
576 return DEFAULT_SHORT;
577 } else if (object.equals(int.class)) {
578 return DEFAULT_INT;
579 } else if (object.equals(long.class)) {
580 return DEFAULT_LONG;
581 } else if (object.equals(float.class)) {
582 return DEFAULT_FLOAT;
583 } else if (object.equals(double.class)) {
584 return DEFAULT_DOUBLE;
585 }
586
587 return null;
588 }
589
590
591
592
593
594 public Object getBean() {
595 return this.bean;
596 }
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611 public Class<?> getBeanClass() {
612 return this.beanClass;
613 }
614
615
616
617
618
619
620
621
622
623 public Class<?> getImplClass() {
624 assert bean == null || beanClass.isInstance(bean) : bean + " is not a " + beanClass;
625 return bean == null ? beanClass : bean.getClass();
626 }
627
628
629
630
631
632
633 public String getName() {
634 return this.name;
635 }
636
637
638
639
640
641
642 private boolean isListOrArrayAndCanReadOrWrite() {
643 Class<?> implClass = getImplClass();
644
645 if (!implClass.isArray() && !List.class.isAssignableFrom(implClass)) {
646 return false;
647 }
648
649 if (name.length() == 0) {
650 return false;
651 }
652
653 for (int i = 0; i < name.length(); i++) {
654 if (!Character.isDigit(name.charAt(i))) {
655 return false;
656 }
657 }
658
659 return true;
660 }
661
662
663
664
665
666
667 private Boolean canReadOrWriteSimple() {
668 if (name == null) {
669
670 return true;
671 }
672
673 Class<?> implClass = getImplClass();
674
675 if (implClass == null) {
676 return false;
677 }
678
679 if (isListOrArrayAndCanReadOrWrite()) {
680 return true;
681 }
682
683 if (Map.class.isAssignableFrom(implClass)) {
684 return true;
685 }
686
687 return null;
688 }
689
690
691
692
693
694
695 public boolean canRead() {
696 Boolean simple = canReadOrWriteSimple();
697
698 if (simple != null) {
699 return simple;
700 }
701
702 return ObjectPropertyUtils.getReadMethod(getImplClass(), name) != null;
703 }
704
705
706
707
708
709
710 public boolean canWrite() {
711 Boolean simple = canReadOrWriteSimple();
712
713 if (simple != null) {
714 return simple;
715 }
716
717 return ObjectPropertyUtils.getWriteMethod(getImplClass(), name) != null;
718 }
719
720
721
722
723
724
725 public Object getFromReadMethod() {
726 Class<?> implClass = getImplClass();
727
728 Method readMethod = ObjectPropertyUtils.getReadMethod(implClass, name);
729
730 if (readMethod == null) {
731 if (isWarning()) {
732 IllegalArgumentException missingPropertyException = new IllegalArgumentException("No property name '"
733 + name + "' is readable on " +
734 (implClass == beanClass ? implClass.toString() : "impl " + implClass + ", bean " + beanClass));
735 LOG.warn(missingPropertyException);
736 }
737
738 return null;
739 }
740
741 try {
742 return readMethod.invoke(bean);
743 } catch (IllegalAccessException e) {
744 throw new IllegalArgumentException("Illegal access invoking property read method " + readMethod, e);
745 } catch (InvocationTargetException e) {
746 Throwable cause = e.getCause();
747 if (cause instanceof RuntimeException) {
748 throw (RuntimeException) cause;
749 } else if (cause instanceof Error) {
750 throw (Error) cause;
751 }
752 throw new IllegalStateException("Unexpected invocation target exception invoking property read method "
753 + readMethod, e);
754 }
755 }
756
757
758
759
760
761
762 public Object get() {
763 if (name == null) {
764 return bean;
765 }
766
767 Class<?> implClass = getImplClass();
768
769 if (implClass == null || bean == null) {
770 return null;
771
772 } else if (implClass.isArray()) {
773 return getArray(bean, name);
774
775 } else if (List.class.isAssignableFrom(implClass)) {
776 return getList((List<?>) bean, name);
777
778 } else if (Map.class.isAssignableFrom(implClass)) {
779 return getMap((Map<?, ?>) bean, name);
780
781 } else {
782 return getFromReadMethod();
783 }
784 }
785
786
787
788
789
790
791
792
793
794 private Class<?> getCollectionPropertyType() {
795 Class<?> implClass = getImplClass();
796 boolean isMap = Map.class.isAssignableFrom(implClass);
797 boolean isList = List.class.isAssignableFrom(implClass);
798
799 Object refBean;
800
801 if (isMap) {
802 refBean = getMap((Map<?, ?>) bean, name);
803 } else if (isList) {
804 refBean = getList((List<?>) bean, name);
805 } else {
806 return null;
807 }
808
809 if (refBean != null) {
810 return refBean.getClass();
811 }
812
813 if (beanType instanceof ParameterizedType) {
814 ParameterizedType parameterizedType = (ParameterizedType) beanType;
815 Type valueType = parameterizedType.getActualTypeArguments()[isList ? 0 : 1];
816
817 if (valueType instanceof Class) {
818 return (Class<?>) valueType;
819 }
820 }
821
822 return Object.class;
823 }
824
825
826
827
828
829
830 private Class<?> getPropertyTypeFromReadOrWriteMethod() {
831 Class<?> implClass = getImplClass();
832
833 Method readMethod = ObjectPropertyUtils.getReadMethod(implClass, name);
834 Method writeMethod;
835
836 if (readMethod == null) {
837
838 writeMethod = ObjectPropertyUtils.getWriteMethod(implClass, name);
839 assert writeMethod == null || writeMethod.getParameterTypes().length == 1 : "Invalid write method "
840 + writeMethod;
841
842 if (writeMethod == null && isWarning()) {
843 IllegalArgumentException missingPropertyException = new IllegalArgumentException("No property name '"
844 + name + "' is readable or writable on " +
845 (implClass == beanClass ? implClass.toString() : "impl " + implClass + ", bean " + beanClass));
846 LOG.warn(missingPropertyException);
847 }
848
849 return writeMethod == null ? null : writeMethod.getParameterTypes()[0];
850
851 } else {
852 Class<?> returnType = readMethod.getReturnType();
853 assert (writeMethod = ObjectPropertyUtils.getWriteMethod(implClass, name)) == null
854 || writeMethod.getParameterTypes()[0].isAssignableFrom(returnType) : "Property types don't match "
855 + readMethod + " " + writeMethod;
856 return returnType;
857 }
858 }
859
860
861
862
863
864
865 public Class<?> getPropertyType() {
866 Class<?> implClass = getImplClass();
867
868 if (implClass == null) {
869 return null;
870 }
871
872 if (name == null) {
873
874 return getImplClass();
875 }
876
877 Class<?> propertyType = getCollectionPropertyType();
878
879 if (propertyType != null) {
880 return propertyType;
881 } else {
882 return getPropertyTypeFromReadOrWriteMethod();
883 }
884 }
885
886
887
888
889
890
891 private void setUsingWriteMethod(Object propertyValue) {
892 Class<?> implClass = getImplClass();
893 Method writeMethod = ObjectPropertyUtils.getWriteMethod(implClass, name);
894
895 if (writeMethod == null) {
896 throw new IllegalArgumentException("No property name '" + name + "' is writable on " +
897 (implClass == beanClass ? implClass.toString() : "impl " + implClass + ", bean " + beanClass));
898 }
899
900 try {
901 writeMethod.invoke(bean, propertyValue);
902 } catch (IllegalAccessException e) {
903 throw new IllegalArgumentException("Illegal access invoking property write method " + writeMethod, e);
904 } catch (InvocationTargetException e) {
905 Throwable cause = e.getCause();
906 if (cause instanceof RuntimeException) {
907 throw (RuntimeException) cause;
908 } else if (cause instanceof Error) {
909 throw (Error) cause;
910 }
911 throw new IllegalStateException(
912 "Unexpected invocation target exception invoking property write method "
913 + writeMethod, e);
914 }
915 }
916
917
918
919
920
921
922 public void set(Object propertyValue) {
923 if (name == null) {
924 throw new IllegalArgumentException("Cannot modify a self-reference");
925 }
926
927 if (bean == null) {
928 throw new IllegalArgumentException("Reference is null");
929 }
930
931 propertyValue = convertToPropertyType(propertyValue);
932
933 Class<?> implClass = getImplClass();
934
935 if (implClass == null) {
936 throw new IllegalArgumentException("No property name '" + name + "' is writable on " + beanClass);
937 }
938
939 if (implClass.isArray()) {
940 setArray(bean, name, propertyValue);
941
942 } else if (List.class.isAssignableFrom(implClass)) {
943 setList((List<?>) bean, name, propertyValue);
944
945 } else if (Map.class.isAssignableFrom(implClass)) {
946 @SuppressWarnings("unchecked")
947 Map<Object, Object> uncheckedMap = (Map<Object, Object>) bean;
948 uncheckedMap.put(name, propertyValue);
949
950 } else {
951 setUsingWriteMethod(propertyValue);
952 }
953 }
954
955 }