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