001/*
002 * Copyright 2002-2020 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.beans;
018
019import java.beans.PropertyChangeEvent;
020import java.lang.reflect.Array;
021import java.lang.reflect.InvocationTargetException;
022import java.lang.reflect.UndeclaredThrowableException;
023import java.security.PrivilegedActionException;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.HashMap;
027import java.util.Iterator;
028import java.util.List;
029import java.util.Map;
030import java.util.Optional;
031import java.util.Set;
032
033import org.apache.commons.logging.Log;
034import org.apache.commons.logging.LogFactory;
035
036import org.springframework.core.CollectionFactory;
037import org.springframework.core.ResolvableType;
038import org.springframework.core.convert.ConversionException;
039import org.springframework.core.convert.ConverterNotFoundException;
040import org.springframework.core.convert.TypeDescriptor;
041import org.springframework.lang.UsesJava8;
042import org.springframework.util.Assert;
043import org.springframework.util.ClassUtils;
044import org.springframework.util.ObjectUtils;
045import org.springframework.util.StringUtils;
046
047/**
048 * A basic {@link ConfigurablePropertyAccessor} that provides the necessary
049 * infrastructure for all typical use cases.
050 *
051 * <p>This accessor will convert collection and array values to the corresponding
052 * target collections or arrays, if necessary. Custom property editors that deal
053 * with collections or arrays can either be written via PropertyEditor's
054 * {@code setValue}, or against a comma-delimited String via {@code setAsText},
055 * as String arrays are converted in such a format if the array itself is not
056 * assignable.
057 *
058 * @author Juergen Hoeller
059 * @author Stephane Nicoll
060 * @author Rod Johnson
061 * @author Rob Harrop
062 * @since 4.2
063 * @see #registerCustomEditor
064 * @see #setPropertyValues
065 * @see #setPropertyValue
066 * @see #getPropertyValue
067 * @see #getPropertyType
068 * @see BeanWrapper
069 * @see PropertyEditorRegistrySupport
070 */
071public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyAccessor {
072
073        /**
074         * We'll create a lot of these objects, so we don't want a new logger every time.
075         */
076        private static final Log logger = LogFactory.getLog(AbstractNestablePropertyAccessor.class);
077
078        private static Class<?> javaUtilOptionalClass = null;
079
080        static {
081                try {
082                        javaUtilOptionalClass =
083                                        ClassUtils.forName("java.util.Optional", AbstractNestablePropertyAccessor.class.getClassLoader());
084                }
085                catch (ClassNotFoundException ex) {
086                        // Java 8 not available - Optional references simply not supported then.
087                }
088        }
089
090
091        private int autoGrowCollectionLimit = Integer.MAX_VALUE;
092
093        Object wrappedObject;
094
095        private String nestedPath = "";
096
097        Object rootObject;
098
099        /** Map with cached nested Accessors: nested path -> Accessor instance */
100        private Map<String, AbstractNestablePropertyAccessor> nestedPropertyAccessors;
101
102
103        /**
104         * Create a new empty accessor. Wrapped instance needs to be set afterwards.
105         * Registers default editors.
106         * @see #setWrappedInstance
107         */
108        protected AbstractNestablePropertyAccessor() {
109                this(true);
110        }
111
112        /**
113         * Create a new empty accessor. Wrapped instance needs to be set afterwards.
114         * @param registerDefaultEditors whether to register default editors
115         * (can be suppressed if the accessor won't need any type conversion)
116         * @see #setWrappedInstance
117         */
118        protected AbstractNestablePropertyAccessor(boolean registerDefaultEditors) {
119                if (registerDefaultEditors) {
120                        registerDefaultEditors();
121                }
122                this.typeConverterDelegate = new TypeConverterDelegate(this);
123        }
124
125        /**
126         * Create a new accessor for the given object.
127         * @param object object wrapped by this accessor
128         */
129        protected AbstractNestablePropertyAccessor(Object object) {
130                registerDefaultEditors();
131                setWrappedInstance(object);
132        }
133
134        /**
135         * Create a new accessor, wrapping a new instance of the specified class.
136         * @param clazz class to instantiate and wrap
137         */
138        protected AbstractNestablePropertyAccessor(Class<?> clazz) {
139                registerDefaultEditors();
140                setWrappedInstance(BeanUtils.instantiateClass(clazz));
141        }
142
143        /**
144         * Create a new accessor for the given object,
145         * registering a nested path that the object is in.
146         * @param object object wrapped by this accessor
147         * @param nestedPath the nested path of the object
148         * @param rootObject the root object at the top of the path
149         */
150        protected AbstractNestablePropertyAccessor(Object object, String nestedPath, Object rootObject) {
151                registerDefaultEditors();
152                setWrappedInstance(object, nestedPath, rootObject);
153        }
154
155        /**
156         * Create a new accessor for the given object,
157         * registering a nested path that the object is in.
158         * @param object object wrapped by this accessor
159         * @param nestedPath the nested path of the object
160         * @param parent the containing accessor (must not be {@code null})
161         */
162        protected AbstractNestablePropertyAccessor(Object object, String nestedPath, AbstractNestablePropertyAccessor parent) {
163                setWrappedInstance(object, nestedPath, parent.getWrappedInstance());
164                setExtractOldValueForEditor(parent.isExtractOldValueForEditor());
165                setAutoGrowNestedPaths(parent.isAutoGrowNestedPaths());
166                setAutoGrowCollectionLimit(parent.getAutoGrowCollectionLimit());
167                setConversionService(parent.getConversionService());
168        }
169
170
171        /**
172         * Specify a limit for array and collection auto-growing.
173         * <p>Default is unlimited on a plain accessor.
174         */
175        public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) {
176                this.autoGrowCollectionLimit = autoGrowCollectionLimit;
177        }
178
179        /**
180         * Return the limit for array and collection auto-growing.
181         */
182        public int getAutoGrowCollectionLimit() {
183                return this.autoGrowCollectionLimit;
184        }
185
186        /**
187         * Switch the target object, replacing the cached introspection results only
188         * if the class of the new object is different to that of the replaced object.
189         * @param object the new target object
190         */
191        public void setWrappedInstance(Object object) {
192                setWrappedInstance(object, "", null);
193        }
194
195        /**
196         * Switch the target object, replacing the cached introspection results only
197         * if the class of the new object is different to that of the replaced object.
198         * @param object the new target object
199         * @param nestedPath the nested path of the object
200         * @param rootObject the root object at the top of the path
201         */
202        public void setWrappedInstance(Object object, String nestedPath, Object rootObject) {
203                Assert.notNull(object, "Target object must not be null");
204                if (object.getClass() == javaUtilOptionalClass) {
205                        this.wrappedObject = OptionalUnwrapper.unwrap(object);
206                }
207                else {
208                        this.wrappedObject = object;
209                }
210                this.nestedPath = (nestedPath != null ? nestedPath : "");
211                this.rootObject = (!"".equals(this.nestedPath) ? rootObject : this.wrappedObject);
212                this.nestedPropertyAccessors = null;
213                this.typeConverterDelegate = new TypeConverterDelegate(this, this.wrappedObject);
214        }
215
216        public final Object getWrappedInstance() {
217                return this.wrappedObject;
218        }
219
220        public final Class<?> getWrappedClass() {
221                return (this.wrappedObject != null ? this.wrappedObject.getClass() : null);
222        }
223
224        /**
225         * Return the nested path of the object wrapped by this accessor.
226         */
227        public final String getNestedPath() {
228                return this.nestedPath;
229        }
230
231        /**
232         * Return the root object at the top of the path of this accessor.
233         * @see #getNestedPath
234         */
235        public final Object getRootInstance() {
236                return this.rootObject;
237        }
238
239        /**
240         * Return the class of the root object at the top of the path of this accessor.
241         * @see #getNestedPath
242         */
243        public final Class<?> getRootClass() {
244                return (this.rootObject != null ? this.rootObject.getClass() : null);
245        }
246
247        @Override
248        public void setPropertyValue(String propertyName, Object value) throws BeansException {
249                AbstractNestablePropertyAccessor nestedPa;
250                try {
251                        nestedPa = getPropertyAccessorForPropertyPath(propertyName);
252                }
253                catch (NotReadablePropertyException ex) {
254                        throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName,
255                                        "Nested property in path '" + propertyName + "' does not exist", ex);
256                }
257                PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
258                nestedPa.setPropertyValue(tokens, new PropertyValue(propertyName, value));
259        }
260
261        @Override
262        public void setPropertyValue(PropertyValue pv) throws BeansException {
263                PropertyTokenHolder tokens = (PropertyTokenHolder) pv.resolvedTokens;
264                if (tokens == null) {
265                        String propertyName = pv.getName();
266                        AbstractNestablePropertyAccessor nestedPa;
267                        try {
268                                nestedPa = getPropertyAccessorForPropertyPath(propertyName);
269                        }
270                        catch (NotReadablePropertyException ex) {
271                                throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName,
272                                                "Nested property in path '" + propertyName + "' does not exist", ex);
273                        }
274                        tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
275                        if (nestedPa == this) {
276                                pv.getOriginalPropertyValue().resolvedTokens = tokens;
277                        }
278                        nestedPa.setPropertyValue(tokens, pv);
279                }
280                else {
281                        setPropertyValue(tokens, pv);
282                }
283        }
284
285        protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException {
286                if (tokens.keys != null) {
287                        processKeyedProperty(tokens, pv);
288                }
289                else {
290                        processLocalProperty(tokens, pv);
291                }
292        }
293
294        @SuppressWarnings("unchecked")
295        private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) {
296                Object propValue = getPropertyHoldingValue(tokens);
297                String lastKey = tokens.keys[tokens.keys.length - 1];
298
299                if (propValue.getClass().isArray()) {
300                        PropertyHandler ph = getLocalPropertyHandler(tokens.actualName);
301                        Class<?> requiredType = propValue.getClass().getComponentType();
302                        int arrayIndex = Integer.parseInt(lastKey);
303                        Object oldValue = null;
304                        try {
305                                if (isExtractOldValueForEditor() && arrayIndex < Array.getLength(propValue)) {
306                                        oldValue = Array.get(propValue, arrayIndex);
307                                }
308                                Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
309                                                requiredType, ph.nested(tokens.keys.length));
310                                int length = Array.getLength(propValue);
311                                if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) {
312                                        Class<?> componentType = propValue.getClass().getComponentType();
313                                        Object newArray = Array.newInstance(componentType, arrayIndex + 1);
314                                        System.arraycopy(propValue, 0, newArray, 0, length);
315                                        setPropertyValue(tokens.actualName, newArray);
316                                        propValue = getPropertyValue(tokens.actualName);
317                                }
318                                Array.set(propValue, arrayIndex, convertedValue);
319                        }
320                        catch (IndexOutOfBoundsException ex) {
321                                throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
322                                                "Invalid array index in property path '" + tokens.canonicalName + "'", ex);
323                        }
324                }
325
326                else if (propValue instanceof List) {
327                        PropertyHandler ph = getPropertyHandler(tokens.actualName);
328                        Class<?> requiredType = ph.getCollectionType(tokens.keys.length);
329                        List<Object> list = (List<Object>) propValue;
330                        int index = Integer.parseInt(lastKey);
331                        Object oldValue = null;
332                        if (isExtractOldValueForEditor() && index < list.size()) {
333                                oldValue = list.get(index);
334                        }
335                        Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
336                                        requiredType, ph.nested(tokens.keys.length));
337                        int size = list.size();
338                        if (index >= size && index < this.autoGrowCollectionLimit) {
339                                for (int i = size; i < index; i++) {
340                                        try {
341                                                list.add(null);
342                                        }
343                                        catch (NullPointerException ex) {
344                                                throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
345                                                                "Cannot set element with index " + index + " in List of size " +
346                                                                size + ", accessed using property path '" + tokens.canonicalName +
347                                                                "': List does not support filling up gaps with null elements");
348                                        }
349                                }
350                                list.add(convertedValue);
351                        }
352                        else {
353                                try {
354                                        list.set(index, convertedValue);
355                                }
356                                catch (IndexOutOfBoundsException ex) {
357                                        throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
358                                                        "Invalid list index in property path '" + tokens.canonicalName + "'", ex);
359                                }
360                        }
361                }
362
363                else if (propValue instanceof Map) {
364                        PropertyHandler ph = getLocalPropertyHandler(tokens.actualName);
365                        Class<?> mapKeyType = ph.getMapKeyType(tokens.keys.length);
366                        Class<?> mapValueType = ph.getMapValueType(tokens.keys.length);
367                        Map<Object, Object> map = (Map<Object, Object>) propValue;
368                        // IMPORTANT: Do not pass full property name in here - property editors
369                        // must not kick in for map keys but rather only for map values.
370                        TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
371                        Object convertedMapKey = convertIfNecessary(null, null, lastKey, mapKeyType, typeDescriptor);
372                        Object oldValue = null;
373                        if (isExtractOldValueForEditor()) {
374                                oldValue = map.get(convertedMapKey);
375                        }
376                        // Pass full property name and old value in here, since we want full
377                        // conversion ability for map values.
378                        Object convertedMapValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
379                                        mapValueType, ph.nested(tokens.keys.length));
380                        map.put(convertedMapKey, convertedMapValue);
381                }
382
383                else {
384                        throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
385                                        "Property referenced in indexed property path '" + tokens.canonicalName +
386                                        "' is neither an array nor a List nor a Map; returned value was [" + propValue + "]");
387                }
388        }
389
390        private Object getPropertyHoldingValue(PropertyTokenHolder tokens) {
391                // Apply indexes and map keys: fetch value for all keys but the last one.
392                PropertyTokenHolder getterTokens = new PropertyTokenHolder();
393                getterTokens.canonicalName = tokens.canonicalName;
394                getterTokens.actualName = tokens.actualName;
395                getterTokens.keys = new String[tokens.keys.length - 1];
396                System.arraycopy(tokens.keys, 0, getterTokens.keys, 0, tokens.keys.length - 1);
397
398                Object propValue;
399                try {
400                        propValue = getPropertyValue(getterTokens);
401                }
402                catch (NotReadablePropertyException ex) {
403                        throw new NotWritablePropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
404                                        "Cannot access indexed value in property referenced " +
405                                        "in indexed property path '" + tokens.canonicalName + "'", ex);
406                }
407
408                if (propValue == null) {
409                        // null map value case
410                        if (isAutoGrowNestedPaths()) {
411                                int lastKeyIndex = tokens.canonicalName.lastIndexOf('[');
412                                getterTokens.canonicalName = tokens.canonicalName.substring(0, lastKeyIndex);
413                                propValue = setDefaultValue(getterTokens);
414                        }
415                        else {
416                                throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName,
417                                                "Cannot access indexed value in property referenced " +
418                                                "in indexed property path '" + tokens.canonicalName + "': returned null");
419                        }
420                }
421                return propValue;
422        }
423
424        private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) {
425                PropertyHandler ph = getLocalPropertyHandler(tokens.actualName);
426                if (ph == null || !ph.isWritable()) {
427                        if (pv.isOptional()) {
428                                if (logger.isDebugEnabled()) {
429                                        logger.debug("Ignoring optional value for property '" + tokens.actualName +
430                                                        "' - property not found on bean class [" + getRootClass().getName() + "]");
431                                }
432                                return;
433                        }
434                        if (this.suppressNotWritablePropertyException) {
435                                // Optimization for common ignoreUnknown=true scenario since the
436                                // exception would be caught and swallowed higher up anyway...
437                                return;
438                        }
439                        throw createNotWritablePropertyException(tokens.canonicalName);
440                }
441
442                Object oldValue = null;
443                try {
444                        Object originalValue = pv.getValue();
445                        Object valueToApply = originalValue;
446                        if (!Boolean.FALSE.equals(pv.conversionNecessary)) {
447                                if (pv.isConverted()) {
448                                        valueToApply = pv.getConvertedValue();
449                                }
450                                else {
451                                        if (isExtractOldValueForEditor() && ph.isReadable()) {
452                                                try {
453                                                        oldValue = ph.getValue();
454                                                }
455                                                catch (Exception ex) {
456                                                        if (ex instanceof PrivilegedActionException) {
457                                                                ex = ((PrivilegedActionException) ex).getException();
458                                                        }
459                                                        if (logger.isDebugEnabled()) {
460                                                                logger.debug("Could not read previous value of property '" +
461                                                                                this.nestedPath + tokens.canonicalName + "'", ex);
462                                                        }
463                                                }
464                                        }
465                                        valueToApply = convertForProperty(
466                                                        tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());
467                                }
468                                pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue);
469                        }
470                        ph.setValue(this.wrappedObject, valueToApply);
471                }
472                catch (TypeMismatchException ex) {
473                        throw ex;
474                }
475                catch (InvocationTargetException ex) {
476                        PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent(
477                                        this.rootObject, this.nestedPath + tokens.canonicalName, oldValue, pv.getValue());
478                        if (ex.getTargetException() instanceof ClassCastException) {
479                                throw new TypeMismatchException(propertyChangeEvent, ph.getPropertyType(), ex.getTargetException());
480                        }
481                        else {
482                                Throwable cause = ex.getTargetException();
483                                if (cause instanceof UndeclaredThrowableException) {
484                                        // May happen e.g. with Groovy-generated methods
485                                        cause = cause.getCause();
486                                }
487                                throw new MethodInvocationException(propertyChangeEvent, cause);
488                        }
489                }
490                catch (Exception ex) {
491                        PropertyChangeEvent pce = new PropertyChangeEvent(
492                                        this.rootObject, this.nestedPath + tokens.canonicalName, oldValue, pv.getValue());
493                        throw new MethodInvocationException(pce, ex);
494                }
495        }
496
497        @Override
498        public Class<?> getPropertyType(String propertyName) throws BeansException {
499                try {
500                        PropertyHandler ph = getPropertyHandler(propertyName);
501                        if (ph != null) {
502                                return ph.getPropertyType();
503                        }
504                        else {
505                                // Maybe an indexed/mapped property...
506                                Object value = getPropertyValue(propertyName);
507                                if (value != null) {
508                                        return value.getClass();
509                                }
510                                // Check to see if there is a custom editor,
511                                // which might give an indication on the desired target type.
512                                Class<?> editorType = guessPropertyTypeFromEditors(propertyName);
513                                if (editorType != null) {
514                                        return editorType;
515                                }
516                        }
517                }
518                catch (InvalidPropertyException ex) {
519                        // Consider as not determinable.
520                }
521                return null;
522        }
523
524        @Override
525        public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException {
526                try {
527                        AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName);
528                        String finalPath = getFinalPath(nestedPa, propertyName);
529                        PropertyTokenHolder tokens = getPropertyNameTokens(finalPath);
530                        PropertyHandler ph = nestedPa.getLocalPropertyHandler(tokens.actualName);
531                        if (ph != null) {
532                                if (tokens.keys != null) {
533                                        if (ph.isReadable() || ph.isWritable()) {
534                                                return ph.nested(tokens.keys.length);
535                                        }
536                                }
537                                else {
538                                        if (ph.isReadable() || ph.isWritable()) {
539                                                return ph.toTypeDescriptor();
540                                        }
541                                }
542                        }
543                }
544                catch (InvalidPropertyException ex) {
545                        // Consider as not determinable.
546                }
547                return null;
548        }
549
550        @Override
551        public boolean isReadableProperty(String propertyName) {
552                try {
553                        PropertyHandler ph = getPropertyHandler(propertyName);
554                        if (ph != null) {
555                                return ph.isReadable();
556                        }
557                        else {
558                                // Maybe an indexed/mapped property...
559                                getPropertyValue(propertyName);
560                                return true;
561                        }
562                }
563                catch (InvalidPropertyException ex) {
564                        // Cannot be evaluated, so can't be readable.
565                }
566                return false;
567        }
568
569        @Override
570        public boolean isWritableProperty(String propertyName) {
571                try {
572                        PropertyHandler ph = getPropertyHandler(propertyName);
573                        if (ph != null) {
574                                return ph.isWritable();
575                        }
576                        else {
577                                // Maybe an indexed/mapped property...
578                                getPropertyValue(propertyName);
579                                return true;
580                        }
581                }
582                catch (InvalidPropertyException ex) {
583                        // Cannot be evaluated, so can't be writable.
584                }
585                return false;
586        }
587
588        private Object convertIfNecessary(String propertyName, Object oldValue, Object newValue, Class<?> requiredType,
589                        TypeDescriptor td) throws TypeMismatchException {
590                try {
591                        return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td);
592                }
593                catch (ConverterNotFoundException ex) {
594                        PropertyChangeEvent pce =
595                                        new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue);
596                        throw new ConversionNotSupportedException(pce, td.getType(), ex);
597                }
598                catch (ConversionException ex) {
599                        PropertyChangeEvent pce =
600                                        new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue);
601                        throw new TypeMismatchException(pce, requiredType, ex);
602                }
603                catch (IllegalStateException ex) {
604                        PropertyChangeEvent pce =
605                                        new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue);
606                        throw new ConversionNotSupportedException(pce, requiredType, ex);
607                }
608                catch (IllegalArgumentException ex) {
609                        PropertyChangeEvent pce =
610                                        new PropertyChangeEvent(this.rootObject, this.nestedPath + propertyName, oldValue, newValue);
611                        throw new TypeMismatchException(pce, requiredType, ex);
612                }
613        }
614
615        protected Object convertForProperty(String propertyName, Object oldValue, Object newValue, TypeDescriptor td)
616                        throws TypeMismatchException {
617
618                return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td);
619        }
620
621        @Override
622        public Object getPropertyValue(String propertyName) throws BeansException {
623                AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName);
624                PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
625                return nestedPa.getPropertyValue(tokens);
626        }
627
628        @SuppressWarnings("unchecked")
629        protected Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException {
630                String propertyName = tokens.canonicalName;
631                String actualName = tokens.actualName;
632                PropertyHandler ph = getLocalPropertyHandler(actualName);
633                if (ph == null || !ph.isReadable()) {
634                        throw new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName);
635                }
636                try {
637                        Object value = ph.getValue();
638                        if (tokens.keys != null) {
639                                if (value == null) {
640                                        if (isAutoGrowNestedPaths()) {
641                                                value = setDefaultValue(tokens.actualName);
642                                        }
643                                        else {
644                                                throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName,
645                                                                "Cannot access indexed value of property referenced in indexed " +
646                                                                                "property path '" + propertyName + "': returned null");
647                                        }
648                                }
649                                String indexedPropertyName = tokens.actualName;
650                                // apply indexes and map keys
651                                for (int i = 0; i < tokens.keys.length; i++) {
652                                        String key = tokens.keys[i];
653                                        if (value == null) {
654                                                throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName,
655                                                                "Cannot access indexed value of property referenced in indexed " +
656                                                                                "property path '" + propertyName + "': returned null");
657                                        }
658                                        else if (value.getClass().isArray()) {
659                                                int index = Integer.parseInt(key);
660                                                value = growArrayIfNecessary(value, index, indexedPropertyName);
661                                                value = Array.get(value, index);
662                                        }
663                                        else if (value instanceof List) {
664                                                int index = Integer.parseInt(key);
665                                                List<Object> list = (List<Object>) value;
666                                                growCollectionIfNecessary(list, index, indexedPropertyName, ph, i + 1);
667                                                value = list.get(index);
668                                        }
669                                        else if (value instanceof Set) {
670                                                // Apply index to Iterator in case of a Set.
671                                                Set<Object> set = (Set<Object>) value;
672                                                int index = Integer.parseInt(key);
673                                                if (index < 0 || index >= set.size()) {
674                                                        throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
675                                                                        "Cannot get element with index " + index + " from Set of size " +
676                                                                                        set.size() + ", accessed using property path '" + propertyName + "'");
677                                                }
678                                                Iterator<Object> it = set.iterator();
679                                                for (int j = 0; it.hasNext(); j++) {
680                                                        Object elem = it.next();
681                                                        if (j == index) {
682                                                                value = elem;
683                                                                break;
684                                                        }
685                                                }
686                                        }
687                                        else if (value instanceof Map) {
688                                                Map<Object, Object> map = (Map<Object, Object>) value;
689                                                Class<?> mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0);
690                                                // IMPORTANT: Do not pass full property name in here - property editors
691                                                // must not kick in for map keys but rather only for map values.
692                                                TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
693                                                Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor);
694                                                value = map.get(convertedMapKey);
695                                        }
696                                        else {
697                                                throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
698                                                                "Property referenced in indexed property path '" + propertyName +
699                                                                                "' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]");
700                                        }
701                                        indexedPropertyName += PROPERTY_KEY_PREFIX + key + PROPERTY_KEY_SUFFIX;
702                                }
703                        }
704                        return value;
705                }
706                catch (IndexOutOfBoundsException ex) {
707                        throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
708                                        "Index of out of bounds in property path '" + propertyName + "'", ex);
709                }
710                catch (NumberFormatException ex) {
711                        throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
712                                        "Invalid index in property path '" + propertyName + "'", ex);
713                }
714                catch (TypeMismatchException ex) {
715                        throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
716                                        "Invalid index in property path '" + propertyName + "'", ex);
717                }
718                catch (InvocationTargetException ex) {
719                        throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
720                                        "Getter for property '" + actualName + "' threw exception", ex);
721                }
722                catch (Exception ex) {
723                        throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
724                                        "Illegal attempt to get property '" + actualName + "' threw exception", ex);
725                }
726        }
727
728
729        /**
730         * Return the {@link PropertyHandler} for the specified {@code propertyName}, navigating
731         * if necessary. Return {@code null} if not found rather than throwing an exception.
732         * @param propertyName the property to obtain the descriptor for
733         * @return the property descriptor for the specified property,
734         * or {@code null} if not found
735         * @throws BeansException in case of introspection failure
736         */
737        protected PropertyHandler getPropertyHandler(String propertyName) throws BeansException {
738                Assert.notNull(propertyName, "Property name must not be null");
739                AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName);
740                return nestedPa.getLocalPropertyHandler(getFinalPath(nestedPa, propertyName));
741        }
742
743        /**
744         * Return a {@link PropertyHandler} for the specified local {@code propertyName}. Only
745         * used to reach a property available in the current context.
746         * @param propertyName the name of a local property
747         * @return the handler for that property or {@code null} if it has not been found
748         */
749        protected abstract PropertyHandler getLocalPropertyHandler(String propertyName);
750
751        /**
752         * Create a new nested property accessor instance.
753         * Can be overridden in subclasses to create a PropertyAccessor subclass.
754         * @param object object wrapped by this PropertyAccessor
755         * @param nestedPath the nested path of the object
756         * @return the nested PropertyAccessor instance
757         */
758        protected abstract AbstractNestablePropertyAccessor newNestedPropertyAccessor(Object object, String nestedPath);
759
760        /**
761         * Create a {@link NotWritablePropertyException} for the specified property.
762         */
763        protected abstract NotWritablePropertyException createNotWritablePropertyException(String propertyName);
764
765
766        private Object growArrayIfNecessary(Object array, int index, String name) {
767                if (!isAutoGrowNestedPaths()) {
768                        return array;
769                }
770                int length = Array.getLength(array);
771                if (index >= length && index < this.autoGrowCollectionLimit) {
772                        Class<?> componentType = array.getClass().getComponentType();
773                        Object newArray = Array.newInstance(componentType, index + 1);
774                        System.arraycopy(array, 0, newArray, 0, length);
775                        for (int i = length; i < Array.getLength(newArray); i++) {
776                                Array.set(newArray, i, newValue(componentType, null, name));
777                        }
778                        setPropertyValue(name, newArray);
779                        return getPropertyValue(name);
780                }
781                else {
782                        return array;
783                }
784        }
785
786        private void growCollectionIfNecessary(Collection<Object> collection, int index, String name,
787                        PropertyHandler ph, int nestingLevel) {
788
789                if (!isAutoGrowNestedPaths()) {
790                        return;
791                }
792                int size = collection.size();
793                if (index >= size && index < this.autoGrowCollectionLimit) {
794                        Class<?> elementType = ph.getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric();
795                        if (elementType != null) {
796                                for (int i = collection.size(); i < index + 1; i++) {
797                                        collection.add(newValue(elementType, null, name));
798                                }
799                        }
800                }
801        }
802
803        /**
804         * Get the last component of the path. Also works if not nested.
805         * @param pa property accessor to work on
806         * @param nestedPath property path we know is nested
807         * @return last component of the path (the property on the target bean)
808         */
809        protected String getFinalPath(AbstractNestablePropertyAccessor pa, String nestedPath) {
810                if (pa == this) {
811                        return nestedPath;
812                }
813                return nestedPath.substring(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(nestedPath) + 1);
814        }
815
816        /**
817         * Recursively navigate to return a property accessor for the nested property path.
818         * @param propertyPath property path, which may be nested
819         * @return a property accessor for the target bean
820         */
821        protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) {
822                int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);
823                // Handle nested properties recursively.
824                if (pos > -1) {
825                        String nestedProperty = propertyPath.substring(0, pos);
826                        String nestedPath = propertyPath.substring(pos + 1);
827                        AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);
828                        return nestedPa.getPropertyAccessorForPropertyPath(nestedPath);
829                }
830                else {
831                        return this;
832                }
833        }
834
835        /**
836         * Retrieve a Property accessor for the given nested property.
837         * Create a new one if not found in the cache.
838         * <p>Note: Caching nested PropertyAccessors is necessary now,
839         * to keep registered custom editors for nested properties.
840         * @param nestedProperty property to create the PropertyAccessor for
841         * @return the PropertyAccessor instance, either cached or newly created
842         */
843        private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nestedProperty) {
844                if (this.nestedPropertyAccessors == null) {
845                        this.nestedPropertyAccessors = new HashMap<String, AbstractNestablePropertyAccessor>();
846                }
847                // Get value of bean property.
848                PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty);
849                String canonicalName = tokens.canonicalName;
850                Object value = getPropertyValue(tokens);
851                if (value == null || (value.getClass() == javaUtilOptionalClass && OptionalUnwrapper.isEmpty(value))) {
852                        if (isAutoGrowNestedPaths()) {
853                                value = setDefaultValue(tokens);
854                        }
855                        else {
856                                throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName);
857                        }
858                }
859
860                // Lookup cached sub-PropertyAccessor, create new one if not found.
861                AbstractNestablePropertyAccessor nestedPa = this.nestedPropertyAccessors.get(canonicalName);
862                if (nestedPa == null || nestedPa.getWrappedInstance() !=
863                                (value.getClass() == javaUtilOptionalClass ? OptionalUnwrapper.unwrap(value) : value)) {
864                        if (logger.isTraceEnabled()) {
865                                logger.trace("Creating new nested " + getClass().getSimpleName() + " for property '" + canonicalName + "'");
866                        }
867                        nestedPa = newNestedPropertyAccessor(value, this.nestedPath + canonicalName + NESTED_PROPERTY_SEPARATOR);
868                        // Inherit all type-specific PropertyEditors.
869                        copyDefaultEditorsTo(nestedPa);
870                        copyCustomEditorsTo(nestedPa, canonicalName);
871                        this.nestedPropertyAccessors.put(canonicalName, nestedPa);
872                }
873                else {
874                        if (logger.isTraceEnabled()) {
875                                logger.trace("Using cached nested property accessor for property '" + canonicalName + "'");
876                        }
877                }
878                return nestedPa;
879        }
880
881        private Object setDefaultValue(String propertyName) {
882                PropertyTokenHolder tokens = new PropertyTokenHolder();
883                tokens.actualName = propertyName;
884                tokens.canonicalName = propertyName;
885                return setDefaultValue(tokens);
886        }
887
888        private Object setDefaultValue(PropertyTokenHolder tokens) {
889                PropertyValue pv = createDefaultPropertyValue(tokens);
890                setPropertyValue(tokens, pv);
891                return getPropertyValue(tokens);
892        }
893
894        private PropertyValue createDefaultPropertyValue(PropertyTokenHolder tokens) {
895                TypeDescriptor desc = getPropertyTypeDescriptor(tokens.canonicalName);
896                Class<?> type = desc.getType();
897                if (type == null) {
898                        throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName,
899                                        "Could not determine property type for auto-growing a default value");
900                }
901                Object defaultValue = newValue(type, desc, tokens.canonicalName);
902                return new PropertyValue(tokens.canonicalName, defaultValue);
903        }
904
905        private Object newValue(Class<?> type, TypeDescriptor desc, String name) {
906                try {
907                        if (type.isArray()) {
908                                Class<?> componentType = type.getComponentType();
909                                // TODO - only handles 2-dimensional arrays
910                                if (componentType.isArray()) {
911                                        Object array = Array.newInstance(componentType, 1);
912                                        Array.set(array, 0, Array.newInstance(componentType.getComponentType(), 0));
913                                        return array;
914                                }
915                                else {
916                                        return Array.newInstance(componentType, 0);
917                                }
918                        }
919                        else if (Collection.class.isAssignableFrom(type)) {
920                                TypeDescriptor elementDesc = (desc != null ? desc.getElementTypeDescriptor() : null);
921                                return CollectionFactory.createCollection(type, (elementDesc != null ? elementDesc.getType() : null), 16);
922                        }
923                        else if (Map.class.isAssignableFrom(type)) {
924                                TypeDescriptor keyDesc = (desc != null ? desc.getMapKeyTypeDescriptor() : null);
925                                return CollectionFactory.createMap(type, (keyDesc != null ? keyDesc.getType() : null), 16);
926                        }
927                        else {
928                                return BeanUtils.instantiate(type);
929                        }
930                }
931                catch (Throwable ex) {
932                        throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + name,
933                                        "Could not instantiate property type [" + type.getName() + "] to auto-grow nested property path", ex);
934                }
935        }
936
937        /**
938         * Parse the given property name into the corresponding property name tokens.
939         * @param propertyName the property name to parse
940         * @return representation of the parsed property tokens
941         */
942        private PropertyTokenHolder getPropertyNameTokens(String propertyName) {
943                PropertyTokenHolder tokens = new PropertyTokenHolder();
944                String actualName = null;
945                List<String> keys = new ArrayList<String>(2);
946                int searchIndex = 0;
947                while (searchIndex != -1) {
948                        int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex);
949                        searchIndex = -1;
950                        if (keyStart != -1) {
951                                int keyEnd = propertyName.indexOf(PROPERTY_KEY_SUFFIX, keyStart + PROPERTY_KEY_PREFIX.length());
952                                if (keyEnd != -1) {
953                                        if (actualName == null) {
954                                                actualName = propertyName.substring(0, keyStart);
955                                        }
956                                        String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd);
957                                        if (key.length() > 1 && (key.startsWith("'") && key.endsWith("'")) ||
958                                                        (key.startsWith("\"") && key.endsWith("\""))) {
959                                                key = key.substring(1, key.length() - 1);
960                                        }
961                                        keys.add(key);
962                                        searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length();
963                                }
964                        }
965                }
966                tokens.actualName = (actualName != null ? actualName : propertyName);
967                tokens.canonicalName = tokens.actualName;
968                if (!keys.isEmpty()) {
969                        tokens.canonicalName += PROPERTY_KEY_PREFIX +
970                                        StringUtils.collectionToDelimitedString(keys, PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX) +
971                                        PROPERTY_KEY_SUFFIX;
972                        tokens.keys = StringUtils.toStringArray(keys);
973                }
974                return tokens;
975        }
976
977
978        @Override
979        public String toString() {
980                StringBuilder sb = new StringBuilder(getClass().getName());
981                if (this.wrappedObject != null) {
982                        sb.append(": wrapping object [").append(ObjectUtils.identityToString(this.wrappedObject)).append("]");
983                }
984                else {
985                        sb.append(": no wrapped object set");
986                }
987                return sb.toString();
988        }
989
990
991        /**
992         * A handler for a specific property.
993         */
994        protected abstract static class PropertyHandler {
995
996                private final Class<?> propertyType;
997
998                private final boolean readable;
999
1000                private final boolean writable;
1001
1002                public PropertyHandler(Class<?> propertyType, boolean readable, boolean writable) {
1003                        this.propertyType = propertyType;
1004                        this.readable = readable;
1005                        this.writable = writable;
1006                }
1007
1008                public Class<?> getPropertyType() {
1009                        return this.propertyType;
1010                }
1011
1012                public boolean isReadable() {
1013                        return this.readable;
1014                }
1015
1016                public boolean isWritable() {
1017                        return this.writable;
1018                }
1019
1020                public abstract TypeDescriptor toTypeDescriptor();
1021
1022                public abstract ResolvableType getResolvableType();
1023
1024                public Class<?> getMapKeyType(int nestingLevel) {
1025                        return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(0);
1026                }
1027
1028                public Class<?> getMapValueType(int nestingLevel) {
1029                        return getResolvableType().getNested(nestingLevel).asMap().resolveGeneric(1);
1030                }
1031
1032                public Class<?> getCollectionType(int nestingLevel) {
1033                        return getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric();
1034                }
1035
1036                public abstract TypeDescriptor nested(int level);
1037
1038                public abstract Object getValue() throws Exception;
1039
1040                public abstract void setValue(Object object, Object value) throws Exception;
1041        }
1042
1043
1044        /**
1045         * Holder class used to store property tokens.
1046         */
1047        protected static class PropertyTokenHolder {
1048
1049                public String canonicalName;
1050
1051                public String actualName;
1052
1053                public String[] keys;
1054        }
1055
1056
1057        /**
1058         * Inner class to avoid a hard dependency on Java 8.
1059         */
1060        @UsesJava8
1061        private static class OptionalUnwrapper {
1062
1063                public static Object unwrap(Object optionalObject) {
1064                        Optional<?> optional = (Optional<?>) optionalObject;
1065                        Assert.isTrue(optional.isPresent(), "Optional value must be present");
1066                        Object result = optional.get();
1067                        Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported");
1068                        return result;
1069                }
1070
1071                public static boolean isEmpty(Object optionalObject) {
1072                        return !((Optional<?>) optionalObject).isPresent();
1073                }
1074        }
1075
1076}