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}