001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.wicket.core.util.lang; 018 019import java.lang.reflect.Array; 020import java.lang.reflect.Field; 021import java.lang.reflect.InvocationTargetException; 022import java.lang.reflect.Method; 023import java.util.List; 024import java.util.Map; 025import java.util.concurrent.ConcurrentHashMap; 026 027import org.apache.wicket.Application; 028import org.apache.wicket.Session; 029import org.apache.wicket.WicketRuntimeException; 030import org.apache.wicket.util.convert.ConversionException; 031import org.apache.wicket.util.lang.Generics; 032import org.apache.wicket.util.string.Strings; 033import org.slf4j.Logger; 034import org.slf4j.LoggerFactory; 035 036/** 037 * This class parses expressions to lookup or set a value on the object that is given. <br/> 038 * The supported expressions are: 039 * <dl> 040 * <dt>"property"</dt> 041 * <dd> 042 * This could be a bean property with getter and setter. Or if a map is given as 043 * an object it will be lookup with the expression as a key when there is not getter for that 044 * property. 045 * </dd> 046 * <dt>"property1.property2"</dt> 047 * <dd> 048 * Both properties are looked up as described above. If property1 evaluates to 049 * null then if there is a setter (or if it is a map) and the Class of the property has a default 050 * constructor then the object will be constructed and set on the object. 051 * </dd> 052 * <dt>"method()"</dt> 053 * <dd> 054 * The corresponding method is invoked. 055 * </dd> 056 * <dt>"property.index" or "property[index]"</dt> 057 * <dd> 058 * If the property is a List or Array then the following expression can be a index on 059 * that list like: 'mylist.0'. The list will grow automatically if the index is greater than the size.<p> 060 * This expression will also map on indexed properties, i.e. {@code getProperty(index)} and {@code setProperty(index,value)} 061 * methods. 062 * </dd> 063 * <dt>"property.key" or "property[key]"</dt> 064 * <dd> 065 * If the property is a Map then the following expression can be a key in that map like: 'myMap.key'. 066 * </dd> 067 * </dl> 068 * <strong>Note that the {@link DefaultPropertyLocator} by default provides access to private members 069 * and methods. If guaranteeing encapsulation of the target objects is a big concern, you should consider 070 * using an alternative implementation.</strong> 071 * <p> 072 * <strong>Note: If a property evaluates to an instance of {@link org.apache.wicket.model.IModel} then 073 * the expression should use '.object' to work with its value.</strong> 074 * 075 * @author jcompagner 076 * @author svenmeier 077 */ 078public final class PropertyResolver 079{ 080 private static final Logger log = LoggerFactory.getLogger(PropertyResolver.class); 081 082 private static final int RETURN_NULL = 0; 083 private static final int CREATE_NEW_VALUE = 1; 084 private static final int RESOLVE_CLASS = 2; 085 086 private static final ConcurrentHashMap<Object, IPropertyLocator> applicationToLocators = Generics.newConcurrentHashMap(2); 087 088 private static final String GET = "get"; 089 private static final String IS = "is"; 090 private static final String SET = "set"; 091 092 /** 093 * Looks up the value from the object with the given expression. If the expression, the object 094 * itself or one property evaluates to null then a null will be returned. 095 * 096 * @param expression 097 * The expression string with the property to be lookup. 098 * @param object 099 * The object which is evaluated. 100 * @return The value that is evaluated. Null something in the expression evaluated to null. 101 */ 102 public static Object getValue(final String expression, final Object object) 103 { 104 if (expression == null || expression.equals("") || object == null) 105 { 106 return object; 107 } 108 109 ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, object, RETURN_NULL); 110 if (objectWithGetAndSet == null) 111 { 112 return null; 113 } 114 115 return objectWithGetAndSet.getValue(); 116 } 117 118 /** 119 * Set the value on the object with the given expression. If the expression can't be evaluated 120 * then a WicketRuntimeException will be thrown. If a null object is encountered then it will 121 * try to generate it by calling the default constructor and set it on the object. 122 * 123 * The value will be tried to convert to the right type with the given converter. 124 * 125 * @param expression 126 * The expression string with the property to be set. 127 * @param object 128 * The object which is evaluated to set the value on. 129 * @param value 130 * The value to set. 131 * @param converter 132 * The converter to convert the value if needed to the right type. 133 * @throws WicketRuntimeException 134 */ 135 public static void setValue(final String expression, final Object object, 136 final Object value, final PropertyResolverConverter converter) 137 { 138 if (Strings.isEmpty(expression)) 139 { 140 throw new WicketRuntimeException("Empty expression setting value: " + value + 141 " on object: " + object); 142 } 143 if (object == null) 144 { 145 throw new WicketRuntimeException( 146 "Attempted to set property value on a null object. Property expression: " + 147 expression + " Value: " + value); 148 } 149 150 ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, object, CREATE_NEW_VALUE); 151 if (objectWithGetAndSet == null) 152 { 153 throw new WicketRuntimeException("Null object returned for expression: " + expression + 154 " for setting value: " + value + " on: " + object); 155 } 156 objectWithGetAndSet.setValue(value, converter == null ? new PropertyResolverConverter(Application.get() 157 .getConverterLocator(), Session.get().getLocale()) : converter); 158 } 159 160 /** 161 * @param expression 162 * @param object 163 * @return class of the target property object 164 * @throws WicketRuntimeException if the cannot be resolved 165 */ 166 public static Class<?> getPropertyClass(final String expression, final Object object) 167 { 168 ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, object, RESOLVE_CLASS); 169 if (objectWithGetAndSet == null) 170 { 171 throw new WicketRuntimeException("Null object returned for expression: " + expression + 172 " for getting the target class of: " + object); 173 } 174 return objectWithGetAndSet.getTargetClass(); 175 } 176 177 /** 178 * @param <T> 179 * @param expression 180 * @param clz 181 * @return class of the target Class property expression 182 * @throws WicketRuntimeException if class cannot be resolved 183 */ 184 @SuppressWarnings("unchecked") 185 public static <T> Class<T> getPropertyClass(final String expression, final Class<?> clz) 186 { 187 ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, null, RESOLVE_CLASS, clz); 188 if (objectWithGetAndSet == null) 189 { 190 throw new WicketRuntimeException("No Class returned for expression: " + expression + 191 " for getting the target class of: " + clz); 192 } 193 return (Class<T>)objectWithGetAndSet.getTargetClass(); 194 } 195 196 /** 197 * @param expression 198 * @param object 199 * @return Field for the property expression 200 * @throws WicketRuntimeException if there is no such field 201 */ 202 public static Field getPropertyField(final String expression, final Object object) 203 { 204 ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, object, RESOLVE_CLASS); 205 if (objectWithGetAndSet == null) 206 { 207 throw new WicketRuntimeException("Null object returned for expression: " + expression + 208 " for getting the target class of: " + object); 209 } 210 return objectWithGetAndSet.getField(); 211 } 212 213 /** 214 * @param expression 215 * @param object 216 * @return Getter method for the property expression 217 * @throws WicketRuntimeException if there is no getter method 218 */ 219 public static Method getPropertyGetter(final String expression, final Object object) 220 { 221 ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, object, RESOLVE_CLASS); 222 if (objectWithGetAndSet == null) 223 { 224 throw new WicketRuntimeException("Null object returned for expression: " + expression + 225 " for getting the target class of: " + object); 226 } 227 return objectWithGetAndSet.getGetter(); 228 } 229 230 /** 231 * @param expression 232 * @param object 233 * @return Setter method for the property expression 234 * @throws WicketRuntimeException if there is no setter method 235 */ 236 public static Method getPropertySetter(final String expression, final Object object) 237 { 238 ObjectWithGetAndSet objectWithGetAndSet = getObjectWithGetAndSet(expression, object, RESOLVE_CLASS); 239 if (objectWithGetAndSet == null) 240 { 241 throw new WicketRuntimeException("Null object returned for expression: " + expression + 242 " for getting the target class of: " + object); 243 } 244 return objectWithGetAndSet.getSetter(); 245 } 246 247 /** 248 * Just delegating the call to the original getObjectAndGetSetter passing the object type as 249 * parameter. 250 * 251 * @param expression 252 * @param object 253 * @param tryToCreateNull 254 * @return {@link ObjectWithGetAndSet} 255 */ 256 private static ObjectWithGetAndSet getObjectWithGetAndSet(final String expression, 257 final Object object, int tryToCreateNull) 258 { 259 return getObjectWithGetAndSet(expression, object, tryToCreateNull, object.getClass()); 260 } 261 262 /** 263 * Receives the class parameter also, since this method can resolve the type for some 264 * expression, only knowing the target class. 265 * 266 * @param expression property expression 267 * @param object root object 268 * @param tryToCreateNull how should null values be handled 269 * @param clz owning clazz 270 * @return final getAndSet and the target to apply it on, or {@code null} if expression results in an intermediate null 271 */ 272 private static ObjectWithGetAndSet getObjectWithGetAndSet(final String expression, final Object object, final int tryToCreateNull, Class<?> clz) 273 { 274 String expressionBracketsSeperated = Strings.replaceAll(expression, "[", ".[").toString(); 275 int index = getNextDotIndex(expressionBracketsSeperated, 0); 276 while (index == 0 && expressionBracketsSeperated.startsWith(".")) 277 { 278 // eat dots at the beginning of the expression since they will confuse 279 // later steps 280 expressionBracketsSeperated = expressionBracketsSeperated.substring(1); 281 index = getNextDotIndex(expressionBracketsSeperated, 0); 282 } 283 int lastIndex = 0; 284 Object value = object; 285 String exp = expressionBracketsSeperated; 286 while (index != -1) 287 { 288 exp = expressionBracketsSeperated.substring(lastIndex, index); 289 if (exp.length() == 0) 290 { 291 exp = expressionBracketsSeperated.substring(index + 1); 292 break; 293 } 294 295 IGetAndSet getAndSet; 296 try 297 { 298 getAndSet = getGetAndSet(exp, clz); 299 } 300 catch (WicketRuntimeException ex) 301 { 302 // expression by itself can't be found. try combined with the following 303 // expression (e.g. for a indexed property); 304 int temp = getNextDotIndex(expressionBracketsSeperated, index + 1); 305 if (temp == -1) 306 { 307 exp = expressionBracketsSeperated.substring(lastIndex); 308 break; 309 } else { 310 index = temp; 311 continue; 312 } 313 } 314 Object nextValue = null; 315 if (value != null) 316 { 317 nextValue = getAndSet.getValue(value); 318 } 319 if (nextValue == null) 320 { 321 if (tryToCreateNull == CREATE_NEW_VALUE) 322 { 323 nextValue = getAndSet.newValue(value); 324 if (nextValue == null) 325 { 326 return null; 327 } 328 } 329 else if (tryToCreateNull == RESOLVE_CLASS) 330 { 331 clz = getAndSet.getTargetClass(); 332 } 333 else 334 { 335 return null; 336 } 337 } 338 value = nextValue; 339 if (value != null) 340 { 341 // value can be null if we are in the RESOLVE_CLASS 342 clz = value.getClass(); 343 } 344 345 lastIndex = index + 1; 346 index = getNextDotIndex(expressionBracketsSeperated, lastIndex); 347 if (index == -1) 348 { 349 exp = expressionBracketsSeperated.substring(lastIndex); 350 break; 351 } 352 } 353 IGetAndSet getAndSet = getGetAndSet(exp, clz); 354 return new ObjectWithGetAndSet(getAndSet, value); 355 } 356 357 /** 358 * 359 * @param expression 360 * @param start 361 * @return next dot index 362 */ 363 private static int getNextDotIndex(final String expression, final int start) 364 { 365 boolean insideBracket = false; 366 for (int i = start; i < expression.length(); i++) 367 { 368 char ch = expression.charAt(i); 369 if (ch == '.' && !insideBracket) 370 { 371 return i; 372 } 373 else if (ch == '[') 374 { 375 insideBracket = true; 376 } 377 else if (ch == ']') 378 { 379 insideBracket = false; 380 } 381 } 382 return -1; 383 } 384 385 private static IGetAndSet getGetAndSet(String exp, final Class<?> clz) 386 { 387 IPropertyLocator locator = getLocator(); 388 389 IGetAndSet getAndSet = locator.get(clz, exp); 390 if (getAndSet == null) { 391 throw new WicketRuntimeException( 392 "Property could not be resolved for class: " + clz + " expression: " + exp); 393 } 394 395 return getAndSet; 396 } 397 398 /** 399 * Utility class: instantiation not allowed. 400 */ 401 private PropertyResolver() 402 { 403 } 404 405 /** 406 * @author jcompagner 407 * 408 */ 409 private static final class ObjectWithGetAndSet 410 { 411 private final IGetAndSet getAndSet; 412 private final Object value; 413 414 /** 415 * @param getAndSet 416 * @param value 417 */ 418 public ObjectWithGetAndSet(IGetAndSet getAndSet, Object value) 419 { 420 this.getAndSet = getAndSet; 421 this.value = value; 422 } 423 424 /** 425 * @param value 426 * @param converter 427 */ 428 public void setValue(Object value, PropertyResolverConverter converter) 429 { 430 getAndSet.setValue(this.value, value, converter); 431 } 432 433 /** 434 * @return The value 435 */ 436 public Object getValue() 437 { 438 return getAndSet.getValue(value); 439 } 440 441 /** 442 * @return class of property value 443 */ 444 public Class<?> getTargetClass() 445 { 446 return getAndSet.getTargetClass(); 447 } 448 449 /** 450 * @return Field or null if no field exists for expression 451 */ 452 public Field getField() 453 { 454 return getAndSet.getField(); 455 } 456 457 /** 458 * @return Getter method or null if no getter exists for expression 459 */ 460 public Method getGetter() 461 { 462 return getAndSet.getGetter(); 463 } 464 465 /** 466 * @return Setter method or null if no setter exists for expression 467 */ 468 public Method getSetter() 469 { 470 return getAndSet.getSetter(); 471 } 472 } 473 474 /** 475 * A property to get and set. 476 * 477 * @author jcompagner 478 */ 479 public interface IGetAndSet 480 { 481 /** 482 * @param object 483 * The object where the value must be taken from. 484 * 485 * @return The value of this property 486 */ 487 Object getValue(final Object object); 488 489 /** 490 * @return The target class of the object that as to be set. 491 */ 492 Class<?> getTargetClass(); 493 494 /** 495 * @param object 496 * The object where the new value must be set on. 497 * 498 * @return The new value for the property that is set back on that object. 499 */ 500 Object newValue(Object object); 501 502 /** 503 * @param object 504 * @param value 505 * @param converter 506 */ 507 void setValue(final Object object, final Object value, 508 PropertyResolverConverter converter); 509 510 /** 511 * @return Field or null if there is no field 512 */ 513 Field getField(); 514 515 /** 516 * @return Getter method or null if there is no getter 517 */ 518 Method getGetter(); 519 520 /** 521 * @return Setter of null if there is no setter 522 */ 523 Method getSetter(); 524 } 525 526 public abstract static class AbstractGetAndSet implements IGetAndSet 527 { 528 @Override 529 public Field getField() 530 { 531 return null; 532 } 533 534 @Override 535 public Method getGetter() 536 { 537 return null; 538 } 539 540 @Override 541 public Method getSetter() 542 { 543 return null; 544 } 545 546 @Override 547 public Class<?> getTargetClass() 548 { 549 return null; 550 } 551 } 552 553 private static final class MapGetAndSet extends AbstractGetAndSet 554 { 555 private final String key; 556 557 MapGetAndSet(String key) 558 { 559 this.key = key; 560 } 561 562 @Override 563 public Object getValue(final Object object) 564 { 565 return ((Map<?, ?>)object).get(key); 566 } 567 568 @Override 569 @SuppressWarnings("unchecked") 570 public void setValue(final Object object, final Object value, 571 final PropertyResolverConverter converter) 572 { 573 ((Map<String, Object>)object).put(key, value); 574 } 575 576 @Override 577 public Object newValue(final Object object) 578 { 579 // Map can't make a newValue or should it look what is more in the 580 // map and try to make one of the class if finds? 581 return null; 582 } 583 } 584 585 private static final class ListGetAndSet extends AbstractGetAndSet 586 { 587 private final int index; 588 589 ListGetAndSet(int index) 590 { 591 this.index = index; 592 } 593 594 @Override 595 public Object getValue(final Object object) 596 { 597 if (((List<?>)object).size() <= index) 598 { 599 return null; 600 } 601 return ((List<?>)object).get(index); 602 } 603 604 @Override 605 @SuppressWarnings("unchecked") 606 public void setValue(final Object object, final Object value, 607 final PropertyResolverConverter converter) 608 { 609 List<Object> lst = (List<Object>)object; 610 611 if (lst.size() > index) 612 { 613 lst.set(index, value); 614 } 615 else if (lst.size() == index) 616 { 617 lst.add(value); 618 } 619 else 620 { 621 while (lst.size() < index) 622 { 623 lst.add(null); 624 } 625 lst.add(value); 626 } 627 } 628 629 @Override 630 public Object newValue(Object object) 631 { 632 // List can't make a newValue or should it look what is more in the 633 // list and try to make one of the class if finds? 634 return null; 635 } 636 } 637 638 private static final class ArrayGetAndSet extends AbstractGetAndSet 639 { 640 private final int index; 641 private final Class<?> clzComponentType; 642 643 ArrayGetAndSet(Class<?> clzComponentType, int index) 644 { 645 this.clzComponentType = clzComponentType; 646 this.index = index; 647 } 648 649 @Override 650 public Object getValue(Object object) 651 { 652 if (Array.getLength(object) > index) 653 { 654 return Array.get(object, index); 655 } 656 return null; 657 } 658 659 @Override 660 public void setValue(Object object, Object value, PropertyResolverConverter converter) 661 { 662 value = converter.convert(value, clzComponentType); 663 Array.set(object, index, value); 664 } 665 666 @Override 667 public Object newValue(Object object) 668 { 669 Object value = null; 670 try 671 { 672 value = clzComponentType.getDeclaredConstructor().newInstance(); 673 Array.set(object, index, value); 674 } 675 catch (Exception e) 676 { 677 log.warn("Cannot set new value {} at index {} for array holding elements of class {}", 678 value, index, clzComponentType, e); 679 } 680 return value; 681 } 682 683 @Override 684 public Class<?> getTargetClass() 685 { 686 return clzComponentType; 687 } 688 } 689 690 private static final class ArrayLengthGetAndSet extends AbstractGetAndSet 691 { 692 ArrayLengthGetAndSet() 693 { 694 } 695 696 @Override 697 public Object getValue(final Object object) 698 { 699 return Array.getLength(object); 700 } 701 702 @Override 703 public void setValue(final Object object, final Object value, 704 final PropertyResolverConverter converter) 705 { 706 throw new WicketRuntimeException("You can't set the length on an array:" + object); 707 } 708 709 @Override 710 public Object newValue(final Object object) 711 { 712 throw new WicketRuntimeException("Can't get a new value from a length of an array: " + 713 object); 714 } 715 716 @Override 717 public Class<?> getTargetClass() 718 { 719 return int.class; 720 } 721 } 722 723 private static final class IndexedPropertyGetAndSet extends AbstractGetAndSet 724 { 725 private final Integer index; 726 private final Method getMethod; 727 private Method setMethod; 728 729 IndexedPropertyGetAndSet(final Method method, final int index) 730 { 731 this.index = index; 732 getMethod = method; 733 getMethod.setAccessible(true); 734 } 735 736 private static Method findSetter(final Method getMethod, final Class<?> clz) 737 { 738 String name = getMethod.getName(); 739 name = SET + name.substring(3); 740 try 741 { 742 return clz.getMethod(name, new Class[] { int.class, getMethod.getReturnType() }); 743 } 744 catch (Exception e) 745 { 746 log.debug("Can't find setter method corresponding to {}", getMethod); 747 } 748 return null; 749 } 750 751 @Override 752 public Object getValue(Object object) 753 { 754 Object ret; 755 try 756 { 757 ret = getMethod.invoke(object, index); 758 } 759 catch (InvocationTargetException ex) 760 { 761 throw new WicketRuntimeException("Error calling index property method: " + 762 getMethod + " on object: " + object, ex.getCause()); 763 } 764 catch (Exception ex) 765 { 766 throw new WicketRuntimeException("Error calling index property method: " + 767 getMethod + " on object: " + object, ex); 768 } 769 return ret; 770 } 771 772 @Override 773 public void setValue(final Object object, final Object value, 774 final PropertyResolverConverter converter) 775 { 776 if (setMethod == null) 777 { 778 setMethod = findSetter(getMethod, object.getClass()); 779 } 780 if (setMethod != null) 781 { 782 setMethod.setAccessible(true); 783 Object converted = converter.convert(value, getMethod.getReturnType()); 784 if (converted == null && value != null) 785 { 786 throw new ConversionException("Can't convert value: " + value + " to class: " + 787 getMethod.getReturnType() + " for setting it on " + object); 788 } 789 try 790 { 791 setMethod.invoke(object, index, converted); 792 } 793 catch (InvocationTargetException ex) 794 { 795 throw new WicketRuntimeException("Error index property calling method: " + 796 setMethod + " on object: " + object, ex.getCause()); 797 } 798 catch (Exception ex) 799 { 800 throw new WicketRuntimeException("Error index property calling method: " + 801 setMethod + " on object: " + object, ex); 802 } 803 } 804 else 805 { 806 throw new WicketRuntimeException("No set method defined for value: " + value + 807 " on object: " + object); 808 } 809 } 810 811 @Override 812 public Class<?> getTargetClass() 813 { 814 return getMethod.getReturnType(); 815 } 816 817 @Override 818 public Object newValue(Object object) 819 { 820 if (setMethod == null) 821 { 822 setMethod = findSetter(getMethod, object.getClass()); 823 } 824 825 if (setMethod == null) 826 { 827 log.warn("Null setMethod"); 828 return null; 829 } 830 831 Class<?> clz = getMethod.getReturnType(); 832 Object value = null; 833 try 834 { 835 value = clz.getDeclaredConstructor().newInstance(); 836 setMethod.invoke(object, index, value); 837 } 838 catch (Exception e) 839 { 840 log.warn("Cannot set new value " + value + " at index " + index, e); 841 } 842 return value; 843 } 844 } 845 846 private static final class MethodGetAndSet extends AbstractGetAndSet 847 { 848 private final Method getMethod; 849 private final Method setMethod; 850 private final Field field; 851 852 MethodGetAndSet(Method getMethod, Method setMethod, Field field) 853 { 854 this.getMethod = getMethod; 855 this.getMethod.setAccessible(true); 856 this.field = field; 857 this.setMethod = setMethod; 858 } 859 860 @Override 861 public final Object getValue(final Object object) 862 { 863 Object ret; 864 try 865 { 866 ret = getMethod.invoke(object, (Object[])null); 867 } 868 catch (InvocationTargetException ex) 869 { 870 throw new WicketRuntimeException("Error calling method: " + getMethod + 871 " on object: " + object, ex.getCause()); 872 } 873 catch (Exception ex) 874 { 875 throw new WicketRuntimeException("Error calling method: " + getMethod + 876 " on object: " + object, ex); 877 } 878 return ret; 879 } 880 881 @Override 882 public final void setValue(final Object object, final Object value, 883 PropertyResolverConverter converter) 884 { 885 Class<?> type = null; 886 if (setMethod != null) 887 { 888 type = setMethod.getParameterTypes()[0]; 889 } 890 else if (field != null) 891 { 892 type = field.getType(); 893 } 894 895 Object converted = null; 896 if (type != null) 897 { 898 converted = converter.convert(value, type); 899 if (converted == null) 900 { 901 if (value != null) 902 { 903 throw new ConversionException("Method [" + getMethod + 904 "]. Can't convert value: " + value + " to class: " + 905 type + " for setting it on " + object); 906 } 907 else if (setMethod != null && type.isPrimitive()) 908 { 909 throw new ConversionException("Method [" + setMethod + 910 "]. Can't convert null value to a primitive class: " + 911 type + " for setting it on " + object); 912 } 913 } 914 } 915 916 if (setMethod != null) 917 { 918 try 919 { 920 setMethod.invoke(object, converted); 921 } 922 catch (InvocationTargetException ex) 923 { 924 throw new WicketRuntimeException("Error calling method: " + setMethod + 925 " on object: " + object, ex.getCause()); 926 } 927 catch (Exception ex) 928 { 929 throw new WicketRuntimeException("Error calling method: " + setMethod + 930 " on object: " + object, ex); 931 } 932 } 933 else if (field != null) 934 { 935 try 936 { 937 field.set(object, converted); 938 } 939 catch (Exception ex) 940 { 941 throw new WicketRuntimeException("Error setting field: " + field + 942 " on object: " + object, ex); 943 } 944 } 945 else 946 { 947 throw new WicketRuntimeException("no set method defined for value: " + value + 948 " on object: " + object + " while respective getMethod being " + 949 getMethod.getName()); 950 } 951 } 952 953 private static Method findSetter(Method getMethod, Class<?> clz) 954 { 955 String name = getMethod.getName(); 956 if (name.startsWith(GET)) 957 { 958 name = SET + name.substring(3); 959 } 960 else 961 { 962 name = SET + name.substring(2); 963 } 964 try 965 { 966 Method method = clz.getMethod(name, getMethod.getReturnType()); 967 if (method != null) 968 { 969 method.setAccessible(true); 970 } 971 return method; 972 } 973 catch (NoSuchMethodException e) 974 { 975 Method[] methods = clz.getMethods(); 976 for (Method method : methods) 977 { 978 if (method.getName().equals(name)) 979 { 980 Class<?>[] parameterTypes = method.getParameterTypes(); 981 if (parameterTypes.length == 1) 982 { 983 if (parameterTypes[0].isAssignableFrom(getMethod.getReturnType())) 984 { 985 return method; 986 } 987 } 988 } 989 } 990 log.debug("Cannot find setter corresponding to " + getMethod, e); 991 } 992 catch (Exception e) 993 { 994 log.debug("Cannot find setter corresponding to " + getMethod, e); 995 } 996 return null; 997 } 998 999 @Override 1000 public Object newValue(Object object) 1001 { 1002 if (setMethod == null) 1003 { 1004 log.warn("Null setMethod"); 1005 return null; 1006 } 1007 1008 Class<?> clz = getMethod.getReturnType(); 1009 Object value = null; 1010 try 1011 { 1012 value = clz.getDeclaredConstructor().newInstance(); 1013 setMethod.invoke(object, value); 1014 } 1015 catch (Exception e) 1016 { 1017 log.warn("Cannot set new value " + value, e); 1018 } 1019 return value; 1020 } 1021 1022 @Override 1023 public Class<?> getTargetClass() 1024 { 1025 return getMethod.getReturnType(); 1026 } 1027 1028 @Override 1029 public Method getGetter() 1030 { 1031 return getMethod; 1032 } 1033 1034 @Override 1035 public Method getSetter() 1036 { 1037 return setMethod; 1038 } 1039 1040 @Override 1041 public Field getField() 1042 { 1043 return field; 1044 } 1045 } 1046 1047 private static class FieldGetAndSet extends AbstractGetAndSet 1048 { 1049 private final Field field; 1050 1051 /** 1052 * Construct. 1053 * 1054 * @param field 1055 */ 1056 public FieldGetAndSet(final Field field) 1057 { 1058 super(); 1059 this.field = field; 1060 this.field.setAccessible(true); 1061 } 1062 1063 @Override 1064 public Object getValue(final Object object) 1065 { 1066 try 1067 { 1068 return field.get(object); 1069 } 1070 catch (Exception ex) 1071 { 1072 throw new WicketRuntimeException("Error getting field value of field " + field + 1073 " from object " + object, ex); 1074 } 1075 } 1076 1077 @Override 1078 public Object newValue(final Object object) 1079 { 1080 Class<?> clz = field.getType(); 1081 Object value = null; 1082 try 1083 { 1084 value = clz.getDeclaredConstructor().newInstance(); 1085 field.set(object, value); 1086 } 1087 catch (Exception e) 1088 { 1089 log.warn("Cannot set field " + field + " to " + value, e); 1090 } 1091 return value; 1092 } 1093 1094 @Override 1095 public void setValue(final Object object, Object value, 1096 final PropertyResolverConverter converter) 1097 { 1098 value = converter.convert(value, field.getType()); 1099 try 1100 { 1101 field.set(object, value); 1102 } 1103 catch (Exception ex) 1104 { 1105 throw new WicketRuntimeException("Error setting field value of field " + field + 1106 " on object " + object + ", value " + value, ex); 1107 } 1108 } 1109 1110 @Override 1111 public Class<?> getTargetClass() 1112 { 1113 return field.getType(); 1114 } 1115 1116 @Override 1117 public Field getField() 1118 { 1119 return field; 1120 } 1121 } 1122 1123 /** 1124 * Clean up cache for this app. 1125 * 1126 * @param application 1127 */ 1128 public static void destroy(Application application) 1129 { 1130 applicationToLocators.remove(application); 1131 } 1132 1133 /** 1134 * Get the current {@link IPropertyLocator}. 1135 * 1136 * @return locator for the current {@link Application} or a general one if no current application is present 1137 * @see Application#get() 1138 */ 1139 public static IPropertyLocator getLocator() 1140 { 1141 Object key; 1142 if (Application.exists()) 1143 { 1144 key = Application.get(); 1145 } 1146 else 1147 { 1148 key = PropertyResolver.class; 1149 } 1150 IPropertyLocator result = applicationToLocators.get(key); 1151 if (result == null) 1152 { 1153 IPropertyLocator tmpResult = applicationToLocators.putIfAbsent(key, result = new CachingPropertyLocator(new DefaultPropertyLocator())); 1154 if (tmpResult != null) 1155 { 1156 result = tmpResult; 1157 } 1158 } 1159 return result; 1160 } 1161 1162 /** 1163 * Set a locator for the given application. 1164 * 1165 * @param application application, may be {@code null} 1166 * @param locator locator 1167 */ 1168 public static void setLocator(final Application application, final IPropertyLocator locator) 1169 { 1170 if (application == null) 1171 { 1172 applicationToLocators.put(PropertyResolver.class, locator); 1173 } 1174 else 1175 { 1176 applicationToLocators.put(application, locator); 1177 } 1178 } 1179 1180 /** 1181 * A locator of properties. 1182 * 1183 * @see <a href="https://issues.apache.org/jira/browse/WICKET-5623">WICKET-5623</a> 1184 */ 1185 public interface IPropertyLocator 1186 { 1187 /** 1188 * Get {@link IGetAndSet} for a property. 1189 * 1190 * @param clz owning class 1191 * @param exp identifying the property 1192 * @return getAndSet or {@code null} if non located 1193 */ 1194 IGetAndSet get(Class<?> clz, String exp); 1195 } 1196 1197 /** 1198 * A wrapper for another {@link IPropertyLocator} that caches results of {@link #get(Class, String)}. 1199 */ 1200 public static class CachingPropertyLocator implements IPropertyLocator 1201 { 1202 private final ConcurrentHashMap<String, IGetAndSet> map = Generics.newConcurrentHashMap(16); 1203 1204 /** 1205 * Special token to put into the cache representing no located {@link IGetAndSet}. 1206 */ 1207 private IGetAndSet NONE = new AbstractGetAndSet() { 1208 1209 @Override 1210 public Object getValue(Object object) { 1211 return null; 1212 } 1213 1214 @Override 1215 public Object newValue(Object object) { 1216 return null; 1217 } 1218 1219 @Override 1220 public void setValue(Object object, Object value, PropertyResolverConverter converter) { 1221 } 1222 }; 1223 1224 private final IPropertyLocator locator; 1225 1226 public CachingPropertyLocator(IPropertyLocator locator) { 1227 this.locator = locator; 1228 } 1229 1230 @Override 1231 public IGetAndSet get(Class<?> clz, String exp) { 1232 String key = clz.getName() + "#" + exp; 1233 1234 IGetAndSet located = map.get(key); 1235 if (located == null) { 1236 located = locator.get(clz, exp); 1237 if (located == null) { 1238 located = NONE; 1239 } 1240 map.put(key, located); 1241 } 1242 1243 if (located == NONE) { 1244 located = null; 1245 } 1246 1247 return located; 1248 } 1249 } 1250 1251 /** 1252 * Default locator supporting <em>Java Beans</em> properties, maps, lists and method invocations. 1253 */ 1254 public static class DefaultPropertyLocator implements IPropertyLocator 1255 { 1256 @Override 1257 public IGetAndSet get(Class<?> clz, String exp) { 1258 IGetAndSet getAndSet = null; 1259 1260 Method method = null; 1261 Field field; 1262 if (exp.startsWith("[")) 1263 { 1264 // if expression begins with [ skip method finding and use it as 1265 // a key/index lookup on a map. 1266 exp = exp.substring(1, exp.length() - 1); 1267 } 1268 else if (exp.endsWith("()")) 1269 { 1270 // if expression ends with (), don't test for setters just skip 1271 // directly to method finding. 1272 method = findMethod(clz, exp); 1273 } 1274 else 1275 { 1276 method = findGetter(clz, exp); 1277 } 1278 if (method == null) 1279 { 1280 if (List.class.isAssignableFrom(clz)) 1281 { 1282 try 1283 { 1284 int index = Integer.parseInt(exp); 1285 getAndSet = new ListGetAndSet(index); 1286 } 1287 catch (NumberFormatException ex) 1288 { 1289 // can't parse the exp as an index, maybe the exp was a 1290 // method. 1291 method = findMethod(clz, exp); 1292 if (method != null) 1293 { 1294 getAndSet = new MethodGetAndSet(method, MethodGetAndSet.findSetter( 1295 method, clz), null); 1296 } 1297 else 1298 { 1299 field = findField(clz, exp); 1300 if (field != null) 1301 { 1302 getAndSet = new FieldGetAndSet(field); 1303 } 1304 else 1305 { 1306 throw new WicketRuntimeException( 1307 "The expression '" + 1308 exp + 1309 "' is neither an index nor is it a method or field for the list " + 1310 clz); 1311 } 1312 } 1313 } 1314 } 1315 else if (Map.class.isAssignableFrom(clz)) 1316 { 1317 getAndSet = new MapGetAndSet(exp); 1318 } 1319 else if (clz.isArray()) 1320 { 1321 try 1322 { 1323 int index = Integer.parseInt(exp); 1324 getAndSet = new ArrayGetAndSet(clz.getComponentType(), index); 1325 } 1326 catch (NumberFormatException ex) 1327 { 1328 if (exp.equals("length") || exp.equals("size")) 1329 { 1330 getAndSet = new ArrayLengthGetAndSet(); 1331 } 1332 else 1333 { 1334 throw new WicketRuntimeException("Can't parse the expression '" + exp + 1335 "' as an index for an array lookup"); 1336 } 1337 } 1338 } 1339 else 1340 { 1341 field = findField(clz, exp); 1342 if (field == null) 1343 { 1344 method = findMethod(clz, exp); 1345 if (method == null) 1346 { 1347 int index = exp.indexOf('.'); 1348 if (index != -1) 1349 { 1350 String propertyName = exp.substring(0, index); 1351 String propertyIndex = exp.substring(index + 1); 1352 try 1353 { 1354 int parsedIndex = Integer.parseInt(propertyIndex); 1355 // if so then it could be a getPropertyIndex(int) 1356 // and setPropertyIndex(int, object) 1357 String name = Character.toUpperCase(propertyName.charAt(0)) + 1358 propertyName.substring(1); 1359 method = clz.getMethod(GET + name, int.class); 1360 getAndSet = new IndexedPropertyGetAndSet(method, parsedIndex); 1361 } 1362 catch (Exception e) 1363 { 1364 throw new WicketRuntimeException( 1365 "No get method defined for class: " + clz + 1366 " expression: " + propertyName); 1367 } 1368 } 1369 } 1370 else 1371 { 1372 getAndSet = new MethodGetAndSet(method, MethodGetAndSet.findSetter( 1373 method, clz), null); 1374 } 1375 } 1376 else 1377 { 1378 getAndSet = new FieldGetAndSet(field); 1379 } 1380 } 1381 } 1382 else 1383 { 1384 field = findField(clz, exp); 1385 getAndSet = new MethodGetAndSet(method, MethodGetAndSet.findSetter(method, clz), 1386 field); 1387 } 1388 1389 return getAndSet; 1390 } 1391 1392 /** 1393 * @param clz 1394 * @param expression 1395 * @return introspected field 1396 */ 1397 private Field findField(final Class<?> clz, final String expression) 1398 { 1399 Field field = null; 1400 try 1401 { 1402 field = clz.getField(expression); 1403 } 1404 catch (Exception e) 1405 { 1406 Class<?> tmp = clz; 1407 while (tmp != null && tmp != Object.class) 1408 { 1409 Field[] fields = tmp.getDeclaredFields(); 1410 for (Field aField : fields) 1411 { 1412 if (aField.getName().equals(expression)) 1413 { 1414 aField.setAccessible(true); 1415 return aField; 1416 } 1417 } 1418 tmp = tmp.getSuperclass(); 1419 } 1420 log.debug("Cannot find field {}.{}", clz, expression); 1421 } 1422 return field; 1423 } 1424 1425 /** 1426 * @param clz 1427 * @param expression 1428 * @return The method for the expression null if not found 1429 */ 1430 private Method findGetter(final Class<?> clz, final String expression) 1431 { 1432 String name = Character.toUpperCase(expression.charAt(0)) + expression.substring(1); 1433 Method method = null; 1434 try 1435 { 1436 method = clz.getMethod(GET + name, (Class[])null); 1437 } 1438 catch (Exception ignored) 1439 { 1440 } 1441 if (method == null) 1442 { 1443 try 1444 { 1445 method = clz.getMethod(IS + name, (Class[])null); 1446 } 1447 catch (Exception e) 1448 { 1449 log.debug("Cannot find getter {}.{}", clz, expression); 1450 } 1451 } 1452 return method; 1453 } 1454 1455 private Method findMethod(final Class<?> clz, String expression) 1456 { 1457 if (expression.endsWith("()")) 1458 { 1459 expression = expression.substring(0, expression.length() - 2); 1460 } 1461 Method method = null; 1462 try 1463 { 1464 method = clz.getMethod(expression, (Class[])null); 1465 } 1466 catch (Exception e) 1467 { 1468 log.debug("Cannot find method {}.{}", clz, expression); 1469 } 1470 return method; 1471 } 1472 } 1473}