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.InvocationTargetException;
020import java.util.ArrayList;
021import java.util.HashMap;
022import java.util.List;
023import java.util.Map;
024
025import org.springframework.asm.Label;
026import org.springframework.asm.MethodVisitor;
027import org.springframework.core.convert.TypeDescriptor;
028import org.springframework.expression.AccessException;
029import org.springframework.expression.EvaluationContext;
030import org.springframework.expression.EvaluationException;
031import org.springframework.expression.PropertyAccessor;
032import org.springframework.expression.TypedValue;
033import org.springframework.expression.spel.CodeFlow;
034import org.springframework.expression.spel.CompilablePropertyAccessor;
035import org.springframework.expression.spel.ExpressionState;
036import org.springframework.expression.spel.SpelEvaluationException;
037import org.springframework.expression.spel.SpelMessage;
038import org.springframework.expression.spel.support.ReflectivePropertyAccessor;
039import org.springframework.lang.Nullable;
040import org.springframework.util.Assert;
041import org.springframework.util.ReflectionUtils;
042
043/**
044 * Represents a simple property or field reference.
045 *
046 * @author Andy Clement
047 * @author Juergen Hoeller
048 * @author Clark Duplichien
049 * @since 3.0
050 */
051public class PropertyOrFieldReference extends SpelNodeImpl {
052
053        private final boolean nullSafe;
054
055        private final String name;
056
057        @Nullable
058        private String originalPrimitiveExitTypeDescriptor;
059
060        @Nullable
061        private volatile PropertyAccessor cachedReadAccessor;
062
063        @Nullable
064        private volatile PropertyAccessor cachedWriteAccessor;
065
066
067        public PropertyOrFieldReference(boolean nullSafe, String propertyOrFieldName, int startPos, int endPos) {
068                super(startPos, endPos);
069                this.nullSafe = nullSafe;
070                this.name = propertyOrFieldName;
071        }
072
073
074        public boolean isNullSafe() {
075                return this.nullSafe;
076        }
077
078        public String getName() {
079                return this.name;
080        }
081
082
083        @Override
084        public ValueRef getValueRef(ExpressionState state) throws EvaluationException {
085                return new AccessorLValue(this, state.getActiveContextObject(), state.getEvaluationContext(),
086                                state.getConfiguration().isAutoGrowNullReferences());
087        }
088
089        @Override
090        public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
091                TypedValue tv = getValueInternal(state.getActiveContextObject(), state.getEvaluationContext(),
092                                state.getConfiguration().isAutoGrowNullReferences());
093                PropertyAccessor accessorToUse = this.cachedReadAccessor;
094                if (accessorToUse instanceof CompilablePropertyAccessor) {
095                        CompilablePropertyAccessor accessor = (CompilablePropertyAccessor) accessorToUse;
096                        setExitTypeDescriptor(CodeFlow.toDescriptor(accessor.getPropertyType()));
097                }
098                return tv;
099        }
100
101        private TypedValue getValueInternal(TypedValue contextObject, EvaluationContext evalContext,
102                        boolean isAutoGrowNullReferences) throws EvaluationException {
103
104                TypedValue result = readProperty(contextObject, evalContext, this.name);
105
106                // Dynamically create the objects if the user has requested that optional behavior
107                if (result.getValue() == null && isAutoGrowNullReferences &&
108                                nextChildIs(Indexer.class, PropertyOrFieldReference.class)) {
109                        TypeDescriptor resultDescriptor = result.getTypeDescriptor();
110                        Assert.state(resultDescriptor != null, "No result type");
111                        // Create a new collection or map ready for the indexer
112                        if (List.class == resultDescriptor.getType()) {
113                                if (isWritableProperty(this.name, contextObject, evalContext)) {
114                                        List<?> newList = new ArrayList<>();
115                                        writeProperty(contextObject, evalContext, this.name, newList);
116                                        result = readProperty(contextObject, evalContext, this.name);
117                                }
118                        }
119                        else if (Map.class == resultDescriptor.getType()) {
120                                if (isWritableProperty(this.name,contextObject, evalContext)) {
121                                        Map<?,?> newMap = new HashMap<>();
122                                        writeProperty(contextObject, evalContext, this.name, newMap);
123                                        result = readProperty(contextObject, evalContext, this.name);
124                                }
125                        }
126                        else {
127                                // 'simple' object
128                                try {
129                                        if (isWritableProperty(this.name,contextObject, evalContext)) {
130                                                Class<?> clazz = result.getTypeDescriptor().getType();
131                                                Object newObject = ReflectionUtils.accessibleConstructor(clazz).newInstance();
132                                                writeProperty(contextObject, evalContext, this.name, newObject);
133                                                result = readProperty(contextObject, evalContext, this.name);
134                                        }
135                                }
136                                catch (InvocationTargetException ex) {
137                                        throw new SpelEvaluationException(getStartPosition(), ex.getTargetException(),
138                                                        SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, result.getTypeDescriptor().getType());
139                                }
140                                catch (Throwable ex) {
141                                        throw new SpelEvaluationException(getStartPosition(), ex,
142                                                        SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, result.getTypeDescriptor().getType());
143                                }
144                        }
145                }
146                return result;
147        }
148
149        @Override
150        public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException {
151                writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, newValue);
152        }
153
154        @Override
155        public boolean isWritable(ExpressionState state) throws EvaluationException {
156                return isWritableProperty(this.name, state.getActiveContextObject(), state.getEvaluationContext());
157        }
158
159        @Override
160        public String toStringAST() {
161                return this.name;
162        }
163
164        /**
165         * Attempt to read the named property from the current context object.
166         * @return the value of the property
167         * @throws EvaluationException if any problem accessing the property or it cannot be found
168         */
169        private TypedValue readProperty(TypedValue contextObject, EvaluationContext evalContext, String name)
170                        throws EvaluationException {
171
172                Object targetObject = contextObject.getValue();
173                if (targetObject == null && this.nullSafe) {
174                        return TypedValue.NULL;
175                }
176
177                PropertyAccessor accessorToUse = this.cachedReadAccessor;
178                if (accessorToUse != null) {
179                        if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
180                                try {
181                                        return accessorToUse.read(evalContext, contextObject.getValue(), name);
182                                }
183                                catch (Exception ex) {
184                                        // This is OK - it may have gone stale due to a class change,
185                                        // let's try to get a new one and call it before giving up...
186                                }
187                        }
188                        this.cachedReadAccessor = null;
189                }
190
191                List<PropertyAccessor> accessorsToTry =
192                                getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors());
193                // Go through the accessors that may be able to resolve it. If they are a cacheable accessor then
194                // get the accessor and use it. If they are not cacheable but report they can read the property
195                // then ask them to read it
196                try {
197                        for (PropertyAccessor accessor : accessorsToTry) {
198                                if (accessor.canRead(evalContext, contextObject.getValue(), name)) {
199                                        if (accessor instanceof ReflectivePropertyAccessor) {
200                                                accessor = ((ReflectivePropertyAccessor) accessor).createOptimalAccessor(
201                                                                evalContext, contextObject.getValue(), name);
202                                        }
203                                        this.cachedReadAccessor = accessor;
204                                        return accessor.read(evalContext, contextObject.getValue(), name);
205                                }
206                        }
207                }
208                catch (Exception ex) {
209                        throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_DURING_PROPERTY_READ, name, ex.getMessage());
210                }
211
212                if (contextObject.getValue() == null) {
213                        throw new SpelEvaluationException(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL, name);
214                }
215                else {
216                        throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, name,
217                                        FormatHelper.formatClassNameForMessage(getObjectClass(contextObject.getValue())));
218                }
219        }
220
221        private void writeProperty(
222                        TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue)
223                        throws EvaluationException {
224
225                if (contextObject.getValue() == null && this.nullSafe) {
226                        return;
227                }
228                if (contextObject.getValue() == null) {
229                        throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL, name);
230                }
231
232                PropertyAccessor accessorToUse = this.cachedWriteAccessor;
233                if (accessorToUse != null) {
234                        if (evalContext.getPropertyAccessors().contains(accessorToUse)) {
235                                try {
236                                        accessorToUse.write(evalContext, contextObject.getValue(), name, newValue);
237                                        return;
238                                }
239                                catch (Exception ex) {
240                                        // This is OK - it may have gone stale due to a class change,
241                                        // let's try to get a new one and call it before giving up...
242                                }
243                        }
244                        this.cachedWriteAccessor = null;
245                }
246
247                List<PropertyAccessor> accessorsToTry =
248                                getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors());
249                try {
250                        for (PropertyAccessor accessor : accessorsToTry) {
251                                if (accessor.canWrite(evalContext, contextObject.getValue(), name)) {
252                                        this.cachedWriteAccessor = accessor;
253                                        accessor.write(evalContext, contextObject.getValue(), name, newValue);
254                                        return;
255                                }
256                        }
257                }
258                catch (AccessException ex) {
259                        throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE,
260                                        name, ex.getMessage());
261                }
262
263                throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE, name,
264                                FormatHelper.formatClassNameForMessage(getObjectClass(contextObject.getValue())));
265        }
266
267        public boolean isWritableProperty(String name, TypedValue contextObject, EvaluationContext evalContext)
268                        throws EvaluationException {
269
270                Object value = contextObject.getValue();
271                if (value != null) {
272                        List<PropertyAccessor> accessorsToTry =
273                                        getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors());
274                        for (PropertyAccessor accessor : accessorsToTry) {
275                                try {
276                                        if (accessor.canWrite(evalContext, value, name)) {
277                                                return true;
278                                        }
279                                }
280                                catch (AccessException ex) {
281                                        // let others try
282                                }
283                        }
284                }
285                return false;
286        }
287
288        /**
289         * Determines the set of property resolvers that should be used to try and access a property
290         * on the specified target type. The resolvers are considered to be in an ordered list,
291         * however in the returned list any that are exact matches for the input target type (as
292         * opposed to 'general' resolvers that could work for any type) are placed at the start of the
293         * list. In addition, there are specific resolvers that exactly name the class in question
294         * and resolvers that name a specific class but it is a supertype of the class we have.
295         * These are put at the end of the specific resolvers set and will be tried after exactly
296         * matching accessors but before generic accessors.
297         * @param contextObject the object upon which property access is being attempted
298         * @return a list of resolvers that should be tried in order to access the property
299         */
300        private List<PropertyAccessor> getPropertyAccessorsToTry(
301                        @Nullable Object contextObject, List<PropertyAccessor> propertyAccessors) {
302
303                Class<?> targetType = (contextObject != null ? contextObject.getClass() : null);
304
305                List<PropertyAccessor> specificAccessors = new ArrayList<>();
306                List<PropertyAccessor> generalAccessors = new ArrayList<>();
307                for (PropertyAccessor resolver : propertyAccessors) {
308                        Class<?>[] targets = resolver.getSpecificTargetClasses();
309                        if (targets == null) {
310                                // generic resolver that says it can be used for any type
311                                generalAccessors.add(resolver);
312                        }
313                        else if (targetType != null) {
314                                for (Class<?> clazz : targets) {
315                                        if (clazz == targetType) {
316                                                specificAccessors.add(resolver);
317                                                break;
318                                        }
319                                        else if (clazz.isAssignableFrom(targetType)) {
320                                                generalAccessors.add(resolver);
321                                        }
322                                }
323                        }
324                }
325                List<PropertyAccessor> resolvers = new ArrayList<>(specificAccessors);
326                generalAccessors.removeAll(specificAccessors);
327                resolvers.addAll(generalAccessors);
328                return resolvers;
329        }
330
331        @Override
332        public boolean isCompilable() {
333                PropertyAccessor accessorToUse = this.cachedReadAccessor;
334                return (accessorToUse instanceof CompilablePropertyAccessor &&
335                                ((CompilablePropertyAccessor) accessorToUse).isCompilable());
336        }
337
338        @Override
339        public void generateCode(MethodVisitor mv, CodeFlow cf) {
340                PropertyAccessor accessorToUse = this.cachedReadAccessor;
341                if (!(accessorToUse instanceof CompilablePropertyAccessor)) {
342                        throw new IllegalStateException("Property accessor is not compilable: " + accessorToUse);
343                }
344
345                Label skipIfNull = null;
346                if (this.nullSafe) {
347                        mv.visitInsn(DUP);
348                        skipIfNull = new Label();
349                        Label continueLabel = new Label();
350                        mv.visitJumpInsn(IFNONNULL, continueLabel);
351                        CodeFlow.insertCheckCast(mv, this.exitTypeDescriptor);
352                        mv.visitJumpInsn(GOTO, skipIfNull);
353                        mv.visitLabel(continueLabel);
354                }
355
356                ((CompilablePropertyAccessor) accessorToUse).generateCode(this.name, mv, cf);
357                cf.pushDescriptor(this.exitTypeDescriptor);
358
359                if (this.originalPrimitiveExitTypeDescriptor != null) {
360                        // The output of the accessor is a primitive but from the block above it might be null,
361                        // so to have a common stack element type at skipIfNull target it is necessary
362                        // to box the primitive
363                        CodeFlow.insertBoxIfNecessary(mv, this.originalPrimitiveExitTypeDescriptor);
364                }
365                if (skipIfNull != null) {
366                        mv.visitLabel(skipIfNull);
367                }
368        }
369
370        void setExitTypeDescriptor(String descriptor) {
371                // If this property or field access would return a primitive - and yet
372                // it is also marked null safe - then the exit type descriptor must be
373                // promoted to the box type to allow a null value to be passed on
374                if (this.nullSafe && CodeFlow.isPrimitive(descriptor)) {
375                        this.originalPrimitiveExitTypeDescriptor = descriptor;
376                        this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor);
377                }
378                else {
379                        this.exitTypeDescriptor = descriptor;
380                }
381        }
382
383
384        private static class AccessorLValue implements ValueRef {
385
386                private final PropertyOrFieldReference ref;
387
388                private final TypedValue contextObject;
389
390                private final EvaluationContext evalContext;
391
392                private final boolean autoGrowNullReferences;
393
394                public AccessorLValue(PropertyOrFieldReference propertyOrFieldReference, TypedValue activeContextObject,
395                                EvaluationContext evalContext, boolean autoGrowNullReferences) {
396
397                        this.ref = propertyOrFieldReference;
398                        this.contextObject = activeContextObject;
399                        this.evalContext = evalContext;
400                        this.autoGrowNullReferences = autoGrowNullReferences;
401                }
402
403                @Override
404                public TypedValue getValue() {
405                        TypedValue value =
406                                        this.ref.getValueInternal(this.contextObject, this.evalContext, this.autoGrowNullReferences);
407                        PropertyAccessor accessorToUse = this.ref.cachedReadAccessor;
408                        if (accessorToUse instanceof CompilablePropertyAccessor) {
409                                this.ref.setExitTypeDescriptor(CodeFlow.toDescriptor(((CompilablePropertyAccessor) accessorToUse).getPropertyType()));
410                        }
411                        return value;
412                }
413
414                @Override
415                public void setValue(@Nullable Object newValue) {
416                        this.ref.writeProperty(this.contextObject, this.evalContext, this.ref.name, newValue);
417                }
418
419                @Override
420                public boolean isWritable() {
421                        return this.ref.isWritableProperty(this.ref.name, this.contextObject, this.evalContext);
422                }
423        }
424
425}