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}