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