001/*
002 * Copyright 2002-2019 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.expression.spel.ast;
018
019import java.lang.reflect.Constructor;
020import java.lang.reflect.Field;
021import java.lang.reflect.Member;
022import java.lang.reflect.Method;
023import java.lang.reflect.Modifier;
024import java.util.Collection;
025import java.util.List;
026import java.util.Map;
027import java.util.StringJoiner;
028
029import org.springframework.asm.MethodVisitor;
030import org.springframework.core.convert.TypeDescriptor;
031import org.springframework.expression.AccessException;
032import org.springframework.expression.EvaluationContext;
033import org.springframework.expression.EvaluationException;
034import org.springframework.expression.PropertyAccessor;
035import org.springframework.expression.TypeConverter;
036import org.springframework.expression.TypedValue;
037import org.springframework.expression.spel.CodeFlow;
038import org.springframework.expression.spel.ExpressionState;
039import org.springframework.expression.spel.SpelEvaluationException;
040import org.springframework.expression.spel.SpelMessage;
041import org.springframework.expression.spel.support.ReflectivePropertyAccessor;
042import org.springframework.lang.Nullable;
043import org.springframework.util.Assert;
044import org.springframework.util.ReflectionUtils;
045
046/**
047 * An Indexer can index into some proceeding structure to access a particular piece of it.
048 * Supported structures are: strings / collections (lists/sets) / arrays.
049 *
050 * @author Andy Clement
051 * @author Phillip Webb
052 * @author Stephane Nicoll
053 * @since 3.0
054 */
055// TODO support multidimensional arrays
056// TODO support correct syntax for multidimensional [][][] and not [,,,]
057public class Indexer extends SpelNodeImpl {
058
059        private enum IndexedType {ARRAY, LIST, MAP, STRING, OBJECT}
060
061
062        // These fields are used when the indexer is being used as a property read accessor.
063        // If the name and target type match these cached values then the cachedReadAccessor
064        // is used to read the property. If they do not match, the correct accessor is
065        // discovered and then cached for later use.
066
067        @Nullable
068        private String cachedReadName;
069
070        @Nullable
071        private Class<?> cachedReadTargetType;
072
073        @Nullable
074        private PropertyAccessor cachedReadAccessor;
075
076        // These fields are used when the indexer is being used as a property write accessor.
077        // If the name and target type match these cached values then the cachedWriteAccessor
078        // is used to write the property. If they do not match, the correct accessor is
079        // discovered and then cached for later use.
080
081        @Nullable
082        private String cachedWriteName;
083
084        @Nullable
085        private Class<?> cachedWriteTargetType;
086
087        @Nullable
088        private PropertyAccessor cachedWriteAccessor;
089
090        @Nullable
091        private IndexedType indexedType;
092
093
094        public Indexer(int startPos, int endPos, SpelNodeImpl expr) {
095                super(startPos, endPos, expr);
096        }
097
098
099        @Override
100        public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
101                return getValueRef(state).getValue();
102        }
103
104        @Override
105        public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException {
106                getValueRef(state).setValue(newValue);
107        }
108
109        @Override
110        public boolean isWritable(ExpressionState expressionState) throws SpelEvaluationException {
111                return true;
112        }
113
114
115        @Override
116        protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
117                TypedValue context = state.getActiveContextObject();
118                Object target = context.getValue();
119                TypeDescriptor targetDescriptor = context.getTypeDescriptor();
120                TypedValue indexValue;
121                Object index;
122
123                // This first part of the if clause prevents a 'double dereference' of the property (SPR-5847)
124                if (target instanceof Map && (this.children[0] instanceof PropertyOrFieldReference)) {
125                        PropertyOrFieldReference reference = (PropertyOrFieldReference) this.children[0];
126                        index = reference.getName();
127                        indexValue = new TypedValue(index);
128                }
129                else {
130                        // In case the map key is unqualified, we want it evaluated against the root object
131                        // so temporarily push that on whilst evaluating the key
132                        try {
133                                state.pushActiveContextObject(state.getRootContextObject());
134                                indexValue = this.children[0].getValueInternal(state);
135                                index = indexValue.getValue();
136                                Assert.state(index != null, "No index");
137                        }
138                        finally {
139                                state.popActiveContextObject();
140                        }
141                }
142
143                // Raise a proper exception in case of a null target
144                if (target == null) {
145                        throw new SpelEvaluationException(getStartPosition(), SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE);
146                }
147                // At this point, we need a TypeDescriptor for a non-null target object
148                Assert.state(targetDescriptor != null, "No type descriptor");
149
150                // Indexing into a Map
151                if (target instanceof Map) {
152                        Object key = index;
153                        if (targetDescriptor.getMapKeyTypeDescriptor() != null) {
154                                key = state.convertValue(key, targetDescriptor.getMapKeyTypeDescriptor());
155                        }
156                        this.indexedType = IndexedType.MAP;
157                        return new MapIndexingValueRef(state.getTypeConverter(), (Map<?, ?>) target, key, targetDescriptor);
158                }
159
160                // If the object is something that looks indexable by an integer,
161                // attempt to treat the index value as a number
162                if (target.getClass().isArray() || target instanceof Collection || target instanceof String) {
163                        int idx = (Integer) state.convertValue(index, TypeDescriptor.valueOf(Integer.class));
164                        if (target.getClass().isArray()) {
165                                this.indexedType = IndexedType.ARRAY;
166                                return new ArrayIndexingValueRef(state.getTypeConverter(), target, idx, targetDescriptor);
167                        }
168                        else if (target instanceof Collection) {
169                                if (target instanceof List) {
170                                        this.indexedType = IndexedType.LIST;
171                                }
172                                return new CollectionIndexingValueRef((Collection<?>) target, idx, targetDescriptor,
173                                                state.getTypeConverter(), state.getConfiguration().isAutoGrowCollections(),
174                                                state.getConfiguration().getMaximumAutoGrowSize());
175                        }
176                        else {
177                                this.indexedType = IndexedType.STRING;
178                                return new StringIndexingLValue((String) target, idx, targetDescriptor);
179                        }
180                }
181
182                // Try and treat the index value as a property of the context object
183                // TODO: could call the conversion service to convert the value to a String
184                TypeDescriptor valueType = indexValue.getTypeDescriptor();
185                if (valueType != null && String.class == valueType.getType()) {
186                        this.indexedType = IndexedType.OBJECT;
187                        return new PropertyIndexingValueRef(
188                                        target, (String) index, state.getEvaluationContext(), targetDescriptor);
189                }
190
191                throw new SpelEvaluationException(
192                                getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, targetDescriptor);
193        }
194
195        @Override
196        public boolean isCompilable() {
197                if (this.indexedType == IndexedType.ARRAY) {
198                        return (this.exitTypeDescriptor != null);
199                }
200                else if (this.indexedType == IndexedType.LIST) {
201                        return this.children[0].isCompilable();
202                }
203                else if (this.indexedType == IndexedType.MAP) {
204                        return (this.children[0] instanceof PropertyOrFieldReference || this.children[0].isCompilable());
205                }
206                else if (this.indexedType == IndexedType.OBJECT) {
207                        // If the string name is changing the accessor is clearly going to change (so no compilation possible)
208                        return (this.cachedReadAccessor != null &&
209                                        this.cachedReadAccessor instanceof ReflectivePropertyAccessor.OptimalPropertyAccessor &&
210                                        getChild(0) instanceof StringLiteral);
211                }
212                return false;
213        }
214
215        @Override
216        public void generateCode(MethodVisitor mv, CodeFlow cf) {
217                String descriptor = cf.lastDescriptor();
218                if (descriptor == null) {
219                        // Stack is empty, should use context object
220                        cf.loadTarget(mv);
221                }
222
223                if (this.indexedType == IndexedType.ARRAY) {
224                        int insn;
225                        if ("D".equals(this.exitTypeDescriptor)) {
226                                mv.visitTypeInsn(CHECKCAST, "[D");
227                                insn = DALOAD;
228                        }
229                        else if ("F".equals(this.exitTypeDescriptor)) {
230                                mv.visitTypeInsn(CHECKCAST, "[F");
231                                insn = FALOAD;
232                        }
233                        else if ("J".equals(this.exitTypeDescriptor)) {
234                                mv.visitTypeInsn(CHECKCAST, "[J");
235                                insn = LALOAD;
236                        }
237                        else if ("I".equals(this.exitTypeDescriptor)) {
238                                mv.visitTypeInsn(CHECKCAST, "[I");
239                                insn = IALOAD;
240                        }
241                        else if ("S".equals(this.exitTypeDescriptor)) {
242                                mv.visitTypeInsn(CHECKCAST, "[S");
243                                insn = SALOAD;
244                        }
245                        else if ("B".equals(this.exitTypeDescriptor)) {
246                                mv.visitTypeInsn(CHECKCAST, "[B");
247                                insn = BALOAD;
248                        }
249                        else if ("C".equals(this.exitTypeDescriptor)) {
250                                mv.visitTypeInsn(CHECKCAST, "[C");
251                                insn = CALOAD;
252                        }
253                        else {
254                                mv.visitTypeInsn(CHECKCAST, "["+ this.exitTypeDescriptor +
255                                                (CodeFlow.isPrimitiveArray(this.exitTypeDescriptor) ? "" : ";"));
256                                                //depthPlusOne(exitTypeDescriptor)+"Ljava/lang/Object;");
257                                insn = AALOAD;
258                        }
259                        SpelNodeImpl index = this.children[0];
260                        cf.enterCompilationScope();
261                        index.generateCode(mv, cf);
262                        cf.exitCompilationScope();
263                        mv.visitInsn(insn);
264                }
265
266                else if (this.indexedType == IndexedType.LIST) {
267                        mv.visitTypeInsn(CHECKCAST, "java/util/List");
268                        cf.enterCompilationScope();
269                        this.children[0].generateCode(mv, cf);
270                        cf.exitCompilationScope();
271                        mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "get", "(I)Ljava/lang/Object;", true);
272                }
273
274                else if (this.indexedType == IndexedType.MAP) {
275                        mv.visitTypeInsn(CHECKCAST, "java/util/Map");
276                        // Special case when the key is an unquoted string literal that will be parsed as
277                        // a property/field reference
278                        if ((this.children[0] instanceof PropertyOrFieldReference)) {
279                                PropertyOrFieldReference reference = (PropertyOrFieldReference) this.children[0];
280                                String mapKeyName = reference.getName();
281                                mv.visitLdcInsn(mapKeyName);
282                        }
283                        else {
284                                cf.enterCompilationScope();
285                                this.children[0].generateCode(mv, cf);
286                                cf.exitCompilationScope();
287                        }
288                        mv.visitMethodInsn(
289                                        INVOKEINTERFACE, "java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", true);
290                }
291
292                else if (this.indexedType == IndexedType.OBJECT) {
293                        ReflectivePropertyAccessor.OptimalPropertyAccessor accessor =
294                                        (ReflectivePropertyAccessor.OptimalPropertyAccessor) this.cachedReadAccessor;
295                        Assert.state(accessor != null, "No cached read accessor");
296                        Member member = accessor.member;
297                        boolean isStatic = Modifier.isStatic(member.getModifiers());
298                        String classDesc = member.getDeclaringClass().getName().replace('.', '/');
299
300                        if (!isStatic) {
301                                if (descriptor == null) {
302                                        cf.loadTarget(mv);
303                                }
304                                if (descriptor == null || !classDesc.equals(descriptor.substring(1))) {
305                                        mv.visitTypeInsn(CHECKCAST, classDesc);
306                                }
307                        }
308
309                        if (member instanceof Method) {
310                                mv.visitMethodInsn((isStatic? INVOKESTATIC : INVOKEVIRTUAL), classDesc, member.getName(),
311                                                CodeFlow.createSignatureDescriptor((Method) member), false);
312                        }
313                        else {
314                                mv.visitFieldInsn((isStatic ? GETSTATIC : GETFIELD), classDesc, member.getName(),
315                                                CodeFlow.toJvmDescriptor(((Field) member).getType()));
316                        }
317                }
318
319                cf.pushDescriptor(this.exitTypeDescriptor);
320        }
321
322        @Override
323        public String toStringAST() {
324                StringJoiner sj = new StringJoiner(",", "[", "]");
325                for (int i = 0; i < getChildCount(); i++) {
326                        sj.add(getChild(i).toStringAST());
327                }
328                return sj.toString();
329        }
330
331
332        private void setArrayElement(TypeConverter converter, Object ctx, int idx, @Nullable Object newValue,
333                        Class<?> arrayComponentType) throws EvaluationException {
334
335                if (arrayComponentType == Boolean.TYPE) {
336                        boolean[] array = (boolean[]) ctx;
337                        checkAccess(array.length, idx);
338                        array[idx] = convertValue(converter, newValue, Boolean.class);
339                }
340                else if (arrayComponentType == Byte.TYPE) {
341                        byte[] array = (byte[]) ctx;
342                        checkAccess(array.length, idx);
343                        array[idx] = convertValue(converter, newValue, Byte.class);
344                }
345                else if (arrayComponentType == Character.TYPE) {
346                        char[] array = (char[]) ctx;
347                        checkAccess(array.length, idx);
348                        array[idx] = convertValue(converter, newValue, Character.class);
349                }
350                else if (arrayComponentType == Double.TYPE) {
351                        double[] array = (double[]) ctx;
352                        checkAccess(array.length, idx);
353                        array[idx] = convertValue(converter, newValue, Double.class);
354                }
355                else if (arrayComponentType == Float.TYPE) {
356                        float[] array = (float[]) ctx;
357                        checkAccess(array.length, idx);
358                        array[idx] = convertValue(converter, newValue, Float.class);
359                }
360                else if (arrayComponentType == Integer.TYPE) {
361                        int[] array = (int[]) ctx;
362                        checkAccess(array.length, idx);
363                        array[idx] = convertValue(converter, newValue, Integer.class);
364                }
365                else if (arrayComponentType == Long.TYPE) {
366                        long[] array = (long[]) ctx;
367                        checkAccess(array.length, idx);
368                        array[idx] = convertValue(converter, newValue, Long.class);
369                }
370                else if (arrayComponentType == Short.TYPE) {
371                        short[] array = (short[]) ctx;
372                        checkAccess(array.length, idx);
373                        array[idx] = convertValue(converter, newValue, Short.class);
374                }
375                else {
376                        Object[] array = (Object[]) ctx;
377                        checkAccess(array.length, idx);
378                        array[idx] = convertValue(converter, newValue, arrayComponentType);
379                }
380        }
381
382        private Object accessArrayElement(Object ctx, int idx) throws SpelEvaluationException {
383                Class<?> arrayComponentType = ctx.getClass().getComponentType();
384                if (arrayComponentType == Boolean.TYPE) {
385                        boolean[] array = (boolean[]) ctx;
386                        checkAccess(array.length, idx);
387                        this.exitTypeDescriptor = "Z";
388                        return array[idx];
389                }
390                else if (arrayComponentType == Byte.TYPE) {
391                        byte[] array = (byte[]) ctx;
392                        checkAccess(array.length, idx);
393                        this.exitTypeDescriptor = "B";
394                        return array[idx];
395                }
396                else if (arrayComponentType == Character.TYPE) {
397                        char[] array = (char[]) ctx;
398                        checkAccess(array.length, idx);
399                        this.exitTypeDescriptor = "C";
400                        return array[idx];
401                }
402                else if (arrayComponentType == Double.TYPE) {
403                        double[] array = (double[]) ctx;
404                        checkAccess(array.length, idx);
405                        this.exitTypeDescriptor = "D";
406                        return array[idx];
407                }
408                else if (arrayComponentType == Float.TYPE) {
409                        float[] array = (float[]) ctx;
410                        checkAccess(array.length, idx);
411                        this.exitTypeDescriptor = "F";
412                        return array[idx];
413                }
414                else if (arrayComponentType == Integer.TYPE) {
415                        int[] array = (int[]) ctx;
416                        checkAccess(array.length, idx);
417                        this.exitTypeDescriptor = "I";
418                        return array[idx];
419                }
420                else if (arrayComponentType == Long.TYPE) {
421                        long[] array = (long[]) ctx;
422                        checkAccess(array.length, idx);
423                        this.exitTypeDescriptor = "J";
424                        return array[idx];
425                }
426                else if (arrayComponentType == Short.TYPE) {
427                        short[] array = (short[]) ctx;
428                        checkAccess(array.length, idx);
429                        this.exitTypeDescriptor = "S";
430                        return array[idx];
431                }
432                else {
433                        Object[] array = (Object[]) ctx;
434                        checkAccess(array.length, idx);
435                        Object retValue = array[idx];
436                        this.exitTypeDescriptor = CodeFlow.toDescriptor(arrayComponentType);
437                        return retValue;
438                }
439        }
440
441        private void checkAccess(int arrayLength, int index) throws SpelEvaluationException {
442                if (index >= arrayLength) {
443                        throw new SpelEvaluationException(getStartPosition(), SpelMessage.ARRAY_INDEX_OUT_OF_BOUNDS,
444                                        arrayLength, index);
445                }
446        }
447
448        @SuppressWarnings("unchecked")
449        private <T> T convertValue(TypeConverter converter, @Nullable Object value, Class<T> targetType) {
450                T result = (T) converter.convertValue(
451                                value, TypeDescriptor.forObject(value), TypeDescriptor.valueOf(targetType));
452                if (result == null) {
453                        throw new IllegalStateException("Null conversion result for index [" + value + "]");
454                }
455                return result;
456        }
457
458
459        private class ArrayIndexingValueRef implements ValueRef {
460
461                private final TypeConverter typeConverter;
462
463                private final Object array;
464
465                private final int index;
466
467                private final TypeDescriptor typeDescriptor;
468
469                ArrayIndexingValueRef(TypeConverter typeConverter, Object array, int index, TypeDescriptor typeDescriptor) {
470                        this.typeConverter = typeConverter;
471                        this.array = array;
472                        this.index = index;
473                        this.typeDescriptor = typeDescriptor;
474                }
475
476                @Override
477                public TypedValue getValue() {
478                        Object arrayElement = accessArrayElement(this.array, this.index);
479                        return new TypedValue(arrayElement, this.typeDescriptor.elementTypeDescriptor(arrayElement));
480                }
481
482                @Override
483                public void setValue(@Nullable Object newValue) {
484                        TypeDescriptor elementType = this.typeDescriptor.getElementTypeDescriptor();
485                        Assert.state(elementType != null, "No element type");
486                        setArrayElement(this.typeConverter, this.array, this.index, newValue, elementType.getType());
487                }
488
489                @Override
490                public boolean isWritable() {
491                        return true;
492                }
493        }
494
495
496        @SuppressWarnings({"rawtypes", "unchecked"})
497        private class MapIndexingValueRef implements ValueRef {
498
499                private final TypeConverter typeConverter;
500
501                private final Map map;
502
503                @Nullable
504                private final Object key;
505
506                private final TypeDescriptor mapEntryDescriptor;
507
508                public MapIndexingValueRef(
509                                TypeConverter typeConverter, Map map, @Nullable Object key, TypeDescriptor mapEntryDescriptor) {
510
511                        this.typeConverter = typeConverter;
512                        this.map = map;
513                        this.key = key;
514                        this.mapEntryDescriptor = mapEntryDescriptor;
515                }
516
517                @Override
518                public TypedValue getValue() {
519                        Object value = this.map.get(this.key);
520                        exitTypeDescriptor = CodeFlow.toDescriptor(Object.class);
521                        return new TypedValue(value, this.mapEntryDescriptor.getMapValueTypeDescriptor(value));
522                }
523
524                @Override
525                public void setValue(@Nullable Object newValue) {
526                        if (this.mapEntryDescriptor.getMapValueTypeDescriptor() != null) {
527                                newValue = this.typeConverter.convertValue(newValue, TypeDescriptor.forObject(newValue),
528                                                this.mapEntryDescriptor.getMapValueTypeDescriptor());
529                        }
530                        this.map.put(this.key, newValue);
531                }
532
533                @Override
534                public boolean isWritable() {
535                        return true;
536                }
537        }
538
539
540        private class PropertyIndexingValueRef implements ValueRef {
541
542                private final Object targetObject;
543
544                private final String name;
545
546                private final EvaluationContext evaluationContext;
547
548                private final TypeDescriptor targetObjectTypeDescriptor;
549
550                public PropertyIndexingValueRef(Object targetObject, String value,
551                                EvaluationContext evaluationContext, TypeDescriptor targetObjectTypeDescriptor) {
552
553                        this.targetObject = targetObject;
554                        this.name = value;
555                        this.evaluationContext = evaluationContext;
556                        this.targetObjectTypeDescriptor = targetObjectTypeDescriptor;
557                }
558
559                @Override
560                public TypedValue getValue() {
561                        Class<?> targetObjectRuntimeClass = getObjectClass(this.targetObject);
562                        try {
563                                if (Indexer.this.cachedReadName != null && Indexer.this.cachedReadName.equals(this.name) &&
564                                                Indexer.this.cachedReadTargetType != null &&
565                                                Indexer.this.cachedReadTargetType.equals(targetObjectRuntimeClass)) {
566                                        // It is OK to use the cached accessor
567                                        PropertyAccessor accessor = Indexer.this.cachedReadAccessor;
568                                        Assert.state(accessor != null, "No cached read accessor");
569                                        return accessor.read(this.evaluationContext, this.targetObject, this.name);
570                                }
571                                List<PropertyAccessor> accessorsToTry = AstUtils.getPropertyAccessorsToTry(
572                                                targetObjectRuntimeClass, this.evaluationContext.getPropertyAccessors());
573                                for (PropertyAccessor accessor : accessorsToTry) {
574                                        if (accessor.canRead(this.evaluationContext, this.targetObject, this.name)) {
575                                                if (accessor instanceof ReflectivePropertyAccessor) {
576                                                        accessor = ((ReflectivePropertyAccessor) accessor).createOptimalAccessor(
577                                                                        this.evaluationContext, this.targetObject, this.name);
578                                                }
579                                                Indexer.this.cachedReadAccessor = accessor;
580                                                Indexer.this.cachedReadName = this.name;
581                                                Indexer.this.cachedReadTargetType = targetObjectRuntimeClass;
582                                                if (accessor instanceof ReflectivePropertyAccessor.OptimalPropertyAccessor) {
583                                                        ReflectivePropertyAccessor.OptimalPropertyAccessor optimalAccessor =
584                                                                        (ReflectivePropertyAccessor.OptimalPropertyAccessor) accessor;
585                                                        Member member = optimalAccessor.member;
586                                                        Indexer.this.exitTypeDescriptor = CodeFlow.toDescriptor(member instanceof Method ?
587                                                                        ((Method) member).getReturnType() : ((Field) member).getType());
588                                                }
589                                                return accessor.read(this.evaluationContext, this.targetObject, this.name);
590                                        }
591                                }
592                        }
593                        catch (AccessException ex) {
594                                throw new SpelEvaluationException(getStartPosition(), ex,
595                                                SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.targetObjectTypeDescriptor.toString());
596                        }
597                        throw new SpelEvaluationException(getStartPosition(),
598                                        SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.targetObjectTypeDescriptor.toString());
599                }
600
601                @Override
602                public void setValue(@Nullable Object newValue) {
603                        Class<?> contextObjectClass = getObjectClass(this.targetObject);
604                        try {
605                                if (Indexer.this.cachedWriteName != null && Indexer.this.cachedWriteName.equals(this.name) &&
606                                                Indexer.this.cachedWriteTargetType != null &&
607                                                Indexer.this.cachedWriteTargetType.equals(contextObjectClass)) {
608                                        // It is OK to use the cached accessor
609                                        PropertyAccessor accessor = Indexer.this.cachedWriteAccessor;
610                                        Assert.state(accessor != null, "No cached write accessor");
611                                        accessor.write(this.evaluationContext, this.targetObject, this.name, newValue);
612                                        return;
613                                }
614                                List<PropertyAccessor> accessorsToTry = AstUtils.getPropertyAccessorsToTry(
615                                                contextObjectClass, this.evaluationContext.getPropertyAccessors());
616                                for (PropertyAccessor accessor : accessorsToTry) {
617                                        if (accessor.canWrite(this.evaluationContext, this.targetObject, this.name)) {
618                                                Indexer.this.cachedWriteName = this.name;
619                                                Indexer.this.cachedWriteTargetType = contextObjectClass;
620                                                Indexer.this.cachedWriteAccessor = accessor;
621                                                accessor.write(this.evaluationContext, this.targetObject, this.name, newValue);
622                                                return;
623                                        }
624                                }
625                        }
626                        catch (AccessException ex) {
627                                throw new SpelEvaluationException(getStartPosition(), ex,
628                                                SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE, this.name, ex.getMessage());
629                        }
630                }
631
632                @Override
633                public boolean isWritable() {
634                        return true;
635                }
636        }
637
638
639        @SuppressWarnings({"rawtypes", "unchecked"})
640        private class CollectionIndexingValueRef implements ValueRef {
641
642                private final Collection collection;
643
644                private final int index;
645
646                private final TypeDescriptor collectionEntryDescriptor;
647
648                private final TypeConverter typeConverter;
649
650                private final boolean growCollection;
651
652                private final int maximumSize;
653
654                public CollectionIndexingValueRef(Collection collection, int index, TypeDescriptor collectionEntryDescriptor,
655                                TypeConverter typeConverter, boolean growCollection, int maximumSize) {
656
657                        this.collection = collection;
658                        this.index = index;
659                        this.collectionEntryDescriptor = collectionEntryDescriptor;
660                        this.typeConverter = typeConverter;
661                        this.growCollection = growCollection;
662                        this.maximumSize = maximumSize;
663                }
664
665                @Override
666                public TypedValue getValue() {
667                        growCollectionIfNecessary();
668                        if (this.collection instanceof List) {
669                                Object o = ((List) this.collection).get(this.index);
670                                exitTypeDescriptor = CodeFlow.toDescriptor(Object.class);
671                                return new TypedValue(o, this.collectionEntryDescriptor.elementTypeDescriptor(o));
672                        }
673                        int pos = 0;
674                        for (Object o : this.collection) {
675                                if (pos == this.index) {
676                                        return new TypedValue(o, this.collectionEntryDescriptor.elementTypeDescriptor(o));
677                                }
678                                pos++;
679                        }
680                        throw new IllegalStateException("Failed to find indexed element " + this.index + ": " + this.collection);
681                }
682
683                @Override
684                public void setValue(@Nullable Object newValue) {
685                        growCollectionIfNecessary();
686                        if (this.collection instanceof List) {
687                                List list = (List) this.collection;
688                                if (this.collectionEntryDescriptor.getElementTypeDescriptor() != null) {
689                                        newValue = this.typeConverter.convertValue(newValue, TypeDescriptor.forObject(newValue),
690                                                        this.collectionEntryDescriptor.getElementTypeDescriptor());
691                                }
692                                list.set(this.index, newValue);
693                        }
694                        else {
695                                throw new SpelEvaluationException(getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE,
696                                                this.collectionEntryDescriptor.toString());
697                        }
698                }
699
700                private void growCollectionIfNecessary() {
701                        if (this.index >= this.collection.size()) {
702                                if (!this.growCollection) {
703                                        throw new SpelEvaluationException(getStartPosition(), SpelMessage.COLLECTION_INDEX_OUT_OF_BOUNDS,
704                                                        this.collection.size(), this.index);
705                                }
706                                if (this.index >= this.maximumSize) {
707                                        throw new SpelEvaluationException(getStartPosition(), SpelMessage.UNABLE_TO_GROW_COLLECTION);
708                                }
709                                if (this.collectionEntryDescriptor.getElementTypeDescriptor() == null) {
710                                        throw new SpelEvaluationException(
711                                                        getStartPosition(), SpelMessage.UNABLE_TO_GROW_COLLECTION_UNKNOWN_ELEMENT_TYPE);
712                                }
713                                TypeDescriptor elementType = this.collectionEntryDescriptor.getElementTypeDescriptor();
714                                try {
715                                        Constructor<?> ctor = ReflectionUtils.accessibleConstructor(elementType.getType());
716                                        int newElements = this.index - this.collection.size();
717                                        while (newElements >= 0) {
718                                                this.collection.add(ctor.newInstance());
719                                                newElements--;
720                                        }
721                                }
722                                catch (Throwable ex) {
723                                        throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.UNABLE_TO_GROW_COLLECTION);
724                                }
725                        }
726                }
727
728                @Override
729                public boolean isWritable() {
730                        return true;
731                }
732        }
733
734
735        private class StringIndexingLValue implements ValueRef {
736
737                private final String target;
738
739                private final int index;
740
741                private final TypeDescriptor typeDescriptor;
742
743                public StringIndexingLValue(String target, int index, TypeDescriptor typeDescriptor) {
744                        this.target = target;
745                        this.index = index;
746                        this.typeDescriptor = typeDescriptor;
747                }
748
749                @Override
750                public TypedValue getValue() {
751                        if (this.index >= this.target.length()) {
752                                throw new SpelEvaluationException(getStartPosition(), SpelMessage.STRING_INDEX_OUT_OF_BOUNDS,
753                                                this.target.length(), this.index);
754                        }
755                        return new TypedValue(String.valueOf(this.target.charAt(this.index)));
756                }
757
758                @Override
759                public void setValue(@Nullable Object newValue) {
760                        throw new SpelEvaluationException(getStartPosition(), SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE,
761                                        this.typeDescriptor.toString());
762                }
763
764                @Override
765                public boolean isWritable() {
766                        return true;
767                }
768        }
769
770}