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