/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.expression.spel.ast;

import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.asm.Label;
import org.springframework.asm.MethodVisitor;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.CompilablePropertyAccessor;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.support.ReflectivePropertyAccessor;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

Represents a simple property or field reference.
Author:Andy Clement, Juergen Hoeller, Clark Duplichien
Since:3.0
/** * Represents a simple property or field reference. * * @author Andy Clement * @author Juergen Hoeller * @author Clark Duplichien * @since 3.0 */
public class PropertyOrFieldReference extends SpelNodeImpl { private final boolean nullSafe; private final String name; @Nullable private String originalPrimitiveExitTypeDescriptor; @Nullable private volatile PropertyAccessor cachedReadAccessor; @Nullable private volatile PropertyAccessor cachedWriteAccessor; public PropertyOrFieldReference(boolean nullSafe, String propertyOrFieldName, int pos) { super(pos); this.nullSafe = nullSafe; this.name = propertyOrFieldName; } public boolean isNullSafe() { return this.nullSafe; } public String getName() { return this.name; } @Override public ValueRef getValueRef(ExpressionState state) throws EvaluationException { return new AccessorLValue(this, state.getActiveContextObject(), state.getEvaluationContext(), state.getConfiguration().isAutoGrowNullReferences()); } @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { TypedValue tv = getValueInternal(state.getActiveContextObject(), state.getEvaluationContext(), state.getConfiguration().isAutoGrowNullReferences()); PropertyAccessor accessorToUse = this.cachedReadAccessor; if (accessorToUse instanceof CompilablePropertyAccessor) { CompilablePropertyAccessor accessor = (CompilablePropertyAccessor) accessorToUse; setExitTypeDescriptor(CodeFlow.toDescriptor(accessor.getPropertyType())); } return tv; } private TypedValue getValueInternal(TypedValue contextObject, EvaluationContext evalContext, boolean isAutoGrowNullReferences) throws EvaluationException { TypedValue result = readProperty(contextObject, evalContext, this.name); // Dynamically create the objects if the user has requested that optional behavior if (result.getValue() == null && isAutoGrowNullReferences && nextChildIs(Indexer.class, PropertyOrFieldReference.class)) { TypeDescriptor resultDescriptor = result.getTypeDescriptor(); Assert.state(resultDescriptor != null, "No result type"); // Create a new collection or map ready for the indexer if (List.class == resultDescriptor.getType()) { if (isWritableProperty(this.name, contextObject, evalContext)) { List<?> newList = new ArrayList<>(); writeProperty(contextObject, evalContext, this.name, newList); result = readProperty(contextObject, evalContext, this.name); } } else if (Map.class == resultDescriptor.getType()) { if (isWritableProperty(this.name,contextObject, evalContext)) { Map<?,?> newMap = new HashMap<>(); writeProperty(contextObject, evalContext, this.name, newMap); result = readProperty(contextObject, evalContext, this.name); } } else { // 'simple' object try { if (isWritableProperty(this.name,contextObject, evalContext)) { Class<?> clazz = result.getTypeDescriptor().getType(); Object newObject = ReflectionUtils.accessibleConstructor(clazz).newInstance(); writeProperty(contextObject, evalContext, this.name, newObject); result = readProperty(contextObject, evalContext, this.name); } } catch (InvocationTargetException ex) { throw new SpelEvaluationException(getStartPosition(), ex.getTargetException(), SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, result.getTypeDescriptor().getType()); } catch (Throwable ex) { throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.UNABLE_TO_DYNAMICALLY_CREATE_OBJECT, result.getTypeDescriptor().getType()); } } } return result; } @Override public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException { writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, newValue); } @Override public boolean isWritable(ExpressionState state) throws EvaluationException { return isWritableProperty(this.name, state.getActiveContextObject(), state.getEvaluationContext()); } @Override public String toStringAST() { return this.name; }
Attempt to read the named property from the current context object.
Throws:
Returns:the value of the property
/** * Attempt to read the named property from the current context object. * @return the value of the property * @throws EvaluationException if any problem accessing the property or it cannot be found */
private TypedValue readProperty(TypedValue contextObject, EvaluationContext evalContext, String name) throws EvaluationException { Object targetObject = contextObject.getValue(); if (targetObject == null && this.nullSafe) { return TypedValue.NULL; } PropertyAccessor accessorToUse = this.cachedReadAccessor; if (accessorToUse != null) { if (evalContext.getPropertyAccessors().contains(accessorToUse)) { try { return accessorToUse.read(evalContext, contextObject.getValue(), name); } catch (Exception ex) { // This is OK - it may have gone stale due to a class change, // let's try to get a new one and call it before giving up... } } this.cachedReadAccessor = null; } List<PropertyAccessor> accessorsToTry = getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors()); // Go through the accessors that may be able to resolve it. If they are a cacheable accessor then // get the accessor and use it. If they are not cacheable but report they can read the property // then ask them to read it try { for (PropertyAccessor accessor : accessorsToTry) { if (accessor.canRead(evalContext, contextObject.getValue(), name)) { if (accessor instanceof ReflectivePropertyAccessor) { accessor = ((ReflectivePropertyAccessor) accessor).createOptimalAccessor( evalContext, contextObject.getValue(), name); } this.cachedReadAccessor = accessor; return accessor.read(evalContext, contextObject.getValue(), name); } } } catch (Exception ex) { throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_DURING_PROPERTY_READ, name, ex.getMessage()); } if (contextObject.getValue() == null) { throw new SpelEvaluationException(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL, name); } else { throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, name, FormatHelper.formatClassNameForMessage(getObjectClass(contextObject.getValue()))); } } private void writeProperty( TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue) throws EvaluationException { if (contextObject.getValue() == null && this.nullSafe) { return; } if (contextObject.getValue() == null) { throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL, name); } PropertyAccessor accessorToUse = this.cachedWriteAccessor; if (accessorToUse != null) { if (evalContext.getPropertyAccessors().contains(accessorToUse)) { try { accessorToUse.write(evalContext, contextObject.getValue(), name, newValue); return; } catch (Exception ex) { // This is OK - it may have gone stale due to a class change, // let's try to get a new one and call it before giving up... } } this.cachedWriteAccessor = null; } List<PropertyAccessor> accessorsToTry = getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors()); try { for (PropertyAccessor accessor : accessorsToTry) { if (accessor.canWrite(evalContext, contextObject.getValue(), name)) { this.cachedWriteAccessor = accessor; accessor.write(evalContext, contextObject.getValue(), name, newValue); return; } } } catch (AccessException ex) { throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_PROPERTY_WRITE, name, ex.getMessage()); } throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE, name, FormatHelper.formatClassNameForMessage(getObjectClass(contextObject.getValue()))); } public boolean isWritableProperty(String name, TypedValue contextObject, EvaluationContext evalContext) throws EvaluationException { Object value = contextObject.getValue(); if (value != null) { List<PropertyAccessor> accessorsToTry = getPropertyAccessorsToTry(contextObject.getValue(), evalContext.getPropertyAccessors()); for (PropertyAccessor accessor : accessorsToTry) { try { if (accessor.canWrite(evalContext, value, name)) { return true; } } catch (AccessException ex) { // let others try } } } return false; }
Determines the set of property resolvers that should be used to try and access a property on the specified target type. The resolvers are considered to be in an ordered list, however in the returned list any that are exact matches for the input target type (as opposed to 'general' resolvers that could work for any type) are placed at the start of the list. In addition, there are specific resolvers that exactly name the class in question and resolvers that name a specific class but it is a supertype of the class we have. These are put at the end of the specific resolvers set and will be tried after exactly matching accessors but before generic accessors.
Params:
  • contextObject – the object upon which property access is being attempted
Returns:a list of resolvers that should be tried in order to access the property
/** * Determines the set of property resolvers that should be used to try and access a property * on the specified target type. The resolvers are considered to be in an ordered list, * however in the returned list any that are exact matches for the input target type (as * opposed to 'general' resolvers that could work for any type) are placed at the start of the * list. In addition, there are specific resolvers that exactly name the class in question * and resolvers that name a specific class but it is a supertype of the class we have. * These are put at the end of the specific resolvers set and will be tried after exactly * matching accessors but before generic accessors. * @param contextObject the object upon which property access is being attempted * @return a list of resolvers that should be tried in order to access the property */
private List<PropertyAccessor> getPropertyAccessorsToTry( @Nullable Object contextObject, List<PropertyAccessor> propertyAccessors) { Class<?> targetType = (contextObject != null ? contextObject.getClass() : null); List<PropertyAccessor> specificAccessors = new ArrayList<>(); List<PropertyAccessor> generalAccessors = new ArrayList<>(); for (PropertyAccessor resolver : propertyAccessors) { Class<?>[] targets = resolver.getSpecificTargetClasses(); if (targets == null) { // generic resolver that says it can be used for any type generalAccessors.add(resolver); } else if (targetType != null) { for (Class<?> clazz : targets) { if (clazz == targetType) { specificAccessors.add(resolver); break; } else if (clazz.isAssignableFrom(targetType)) { generalAccessors.add(resolver); } } } } List<PropertyAccessor> resolvers = new ArrayList<>(specificAccessors); generalAccessors.removeAll(specificAccessors); resolvers.addAll(generalAccessors); return resolvers; } @Override public boolean isCompilable() { PropertyAccessor accessorToUse = this.cachedReadAccessor; return (accessorToUse instanceof CompilablePropertyAccessor && ((CompilablePropertyAccessor) accessorToUse).isCompilable()); } @Override public void generateCode(MethodVisitor mv, CodeFlow cf) { PropertyAccessor accessorToUse = this.cachedReadAccessor; if (!(accessorToUse instanceof CompilablePropertyAccessor)) { throw new IllegalStateException("Property accessor is not compilable: " + accessorToUse); } Label skipIfNull = null; if (this.nullSafe) { mv.visitInsn(DUP); skipIfNull = new Label(); Label continueLabel = new Label(); mv.visitJumpInsn(IFNONNULL, continueLabel); CodeFlow.insertCheckCast(mv, this.exitTypeDescriptor); mv.visitJumpInsn(GOTO, skipIfNull); mv.visitLabel(continueLabel); } ((CompilablePropertyAccessor) accessorToUse).generateCode(this.name, mv, cf); cf.pushDescriptor(this.exitTypeDescriptor); if (this.originalPrimitiveExitTypeDescriptor != null) { // The output of the accessor is a primitive but from the block above it might be null, // so to have a common stack element type at skipIfNull target it is necessary // to box the primitive CodeFlow.insertBoxIfNecessary(mv, this.originalPrimitiveExitTypeDescriptor); } if (skipIfNull != null) { mv.visitLabel(skipIfNull); } } void setExitTypeDescriptor(String descriptor) { // If this property or field access would return a primitive - and yet // it is also marked null safe - then the exit type descriptor must be // promoted to the box type to allow a null value to be passed on if (this.nullSafe && CodeFlow.isPrimitive(descriptor)) { this.originalPrimitiveExitTypeDescriptor = descriptor; this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor); } else { this.exitTypeDescriptor = descriptor; } } private static class AccessorLValue implements ValueRef { private final PropertyOrFieldReference ref; private final TypedValue contextObject; private final EvaluationContext evalContext; private final boolean autoGrowNullReferences; public AccessorLValue(PropertyOrFieldReference propertyOrFieldReference, TypedValue activeContextObject, EvaluationContext evalContext, boolean autoGrowNullReferences) { this.ref = propertyOrFieldReference; this.contextObject = activeContextObject; this.evalContext = evalContext; this.autoGrowNullReferences = autoGrowNullReferences; } @Override public TypedValue getValue() { TypedValue value = this.ref.getValueInternal(this.contextObject, this.evalContext, this.autoGrowNullReferences); PropertyAccessor accessorToUse = this.ref.cachedReadAccessor; if (accessorToUse instanceof CompilablePropertyAccessor) { this.ref.setExitTypeDescriptor(CodeFlow.toDescriptor(((CompilablePropertyAccessor) accessorToUse).getPropertyType())); } return value; } @Override public void setValue(@Nullable Object newValue) { this.ref.writeProperty(this.contextObject, this.evalContext, this.ref.name, newValue); } @Override public boolean isWritable() { return this.ref.isWritableProperty(this.ref.name, this.contextObject, this.evalContext); } } }