Polish SpEL Javadocs and internals

This commit is contained in:
Sam Brannen 2024-02-10 15:03:02 +01:00
parent 1080c145e3
commit 888e50175d
13 changed files with 194 additions and 172 deletions

View File

@ -80,7 +80,7 @@ public class CompositeStringExpression implements Expression {
@Override @Override
@Nullable @Nullable
public <T> T getValue(@Nullable Class<T> expectedResultType) throws EvaluationException { public <T> T getValue(@Nullable Class<T> expectedResultType) throws EvaluationException {
Object value = getValue(); String value = getValue();
return ExpressionUtils.convertTypedValue(null, new TypedValue(value), expectedResultType); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), expectedResultType);
} }
@ -99,7 +99,7 @@ public class CompositeStringExpression implements Expression {
@Override @Override
@Nullable @Nullable
public <T> T getValue(@Nullable Object rootObject, @Nullable Class<T> desiredResultType) throws EvaluationException { public <T> T getValue(@Nullable Object rootObject, @Nullable Class<T> desiredResultType) throws EvaluationException {
Object value = getValue(rootObject); String value = getValue(rootObject);
return ExpressionUtils.convertTypedValue(null, new TypedValue(value), desiredResultType); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), desiredResultType);
} }
@ -120,7 +120,7 @@ public class CompositeStringExpression implements Expression {
public <T> T getValue(EvaluationContext context, @Nullable Class<T> expectedResultType) public <T> T getValue(EvaluationContext context, @Nullable Class<T> expectedResultType)
throws EvaluationException { throws EvaluationException {
Object value = getValue(context); String value = getValue(context);
return ExpressionUtils.convertTypedValue(context, new TypedValue(value), expectedResultType); return ExpressionUtils.convertTypedValue(context, new TypedValue(value), expectedResultType);
} }
@ -141,7 +141,7 @@ public class CompositeStringExpression implements Expression {
public <T> T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class<T> desiredResultType) public <T> T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class<T> desiredResultType)
throws EvaluationException { throws EvaluationException {
Object value = getValue(context,rootObject); String value = getValue(context,rootObject);
return ExpressionUtils.convertTypedValue(context, new TypedValue(value), desiredResultType); return ExpressionUtils.convertTypedValue(context, new TypedValue(value), desiredResultType);
} }

View File

@ -64,7 +64,7 @@ public class LiteralExpression implements Expression {
@Override @Override
@Nullable @Nullable
public <T> T getValue(@Nullable Class<T> expectedResultType) throws EvaluationException { public <T> T getValue(@Nullable Class<T> expectedResultType) throws EvaluationException {
Object value = getValue(); String value = getValue();
return ExpressionUtils.convertTypedValue(null, new TypedValue(value), expectedResultType); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), expectedResultType);
} }
@ -76,7 +76,7 @@ public class LiteralExpression implements Expression {
@Override @Override
@Nullable @Nullable
public <T> T getValue(@Nullable Object rootObject, @Nullable Class<T> desiredResultType) throws EvaluationException { public <T> T getValue(@Nullable Object rootObject, @Nullable Class<T> desiredResultType) throws EvaluationException {
Object value = getValue(rootObject); String value = getValue(rootObject);
return ExpressionUtils.convertTypedValue(null, new TypedValue(value), desiredResultType); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), desiredResultType);
} }
@ -87,10 +87,8 @@ public class LiteralExpression implements Expression {
@Override @Override
@Nullable @Nullable
public <T> T getValue(EvaluationContext context, @Nullable Class<T> expectedResultType) public <T> T getValue(EvaluationContext context, @Nullable Class<T> expectedResultType) throws EvaluationException {
throws EvaluationException { String value = getValue(context);
Object value = getValue(context);
return ExpressionUtils.convertTypedValue(context, new TypedValue(value), expectedResultType); return ExpressionUtils.convertTypedValue(context, new TypedValue(value), expectedResultType);
} }
@ -104,7 +102,7 @@ public class LiteralExpression implements Expression {
public <T> T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class<T> desiredResultType) public <T> T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class<T> desiredResultType)
throws EvaluationException { throws EvaluationException {
Object value = getValue(context, rootObject); String value = getValue(context, rootObject);
return ExpressionUtils.convertTypedValue(context, new TypedValue(value), desiredResultType); return ExpressionUtils.convertTypedValue(context, new TypedValue(value), desiredResultType);
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,7 +21,6 @@ import java.util.StringJoiner;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
/** /**
* Utility methods (formatters, etc) used during parsing and evaluation. * Utility methods (formatters, etc) used during parsing and evaluation.
@ -51,10 +50,9 @@ abstract class FormatHelper {
* <p>A String array will have the formatted name "java.lang.String[]". * <p>A String array will have the formatted name "java.lang.String[]".
* @param clazz the Class whose name is to be formatted * @param clazz the Class whose name is to be formatted
* @return a formatted String suitable for message inclusion * @return a formatted String suitable for message inclusion
* @see ClassUtils#getQualifiedName(Class)
*/ */
static String formatClassNameForMessage(@Nullable Class<?> clazz) { static String formatClassNameForMessage(@Nullable Class<?> clazz) {
return (clazz != null ? ClassUtils.getQualifiedName(clazz) : "null"); return (clazz != null ? clazz.getTypeName() : "null");
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -39,16 +39,21 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
/** /**
* A function reference is of the form "#someFunction(a,b,c)". Functions may be defined * A function reference is of the form "#someFunction(a,b,c)".
* in the context prior to the expression being evaluated. Functions may also be static
* Java methods, registered in the context prior to invocation of the expression.
* *
* <p>Functions are very simplistic. The arguments are not part of the definition * <p>Functions can be either a {@link Method} (for static Java methods) or a
* (right now), so the names must be unique. * {@link MethodHandle} and must be registered in the context prior to evaluation
* of the expression. See the {@code registerFunction()} methods in
* {@link org.springframework.expression.spel.support.StandardEvaluationContext}
* for details.
* *
* @author Andy Clement * @author Andy Clement
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Simon Baslé
* @author Sam Brannen
* @since 3.0 * @since 3.0
* @see org.springframework.expression.spel.support.StandardEvaluationContext#registerFunction(String, Method)
* @see org.springframework.expression.spel.support.StandardEvaluationContext#registerFunction(String, MethodHandle)
*/ */
public class FunctionReference extends SpelNodeImpl { public class FunctionReference extends SpelNodeImpl {
@ -72,39 +77,44 @@ public class FunctionReference extends SpelNodeImpl {
if (value == TypedValue.NULL) { if (value == TypedValue.NULL) {
throw new SpelEvaluationException(getStartPosition(), SpelMessage.FUNCTION_NOT_DEFINED, this.name); throw new SpelEvaluationException(getStartPosition(), SpelMessage.FUNCTION_NOT_DEFINED, this.name);
} }
Object resolvedValue = value.getValue(); Object function = value.getValue();
if (resolvedValue instanceof MethodHandle methodHandle) {
// Static Java method registered via a Method.
// Note: "javaMethod" cannot be named "method" due to a bug in Checkstyle.
if (function instanceof Method javaMethod) {
try { try {
return executeFunctionBoundMethodHandle(state, methodHandle); return executeFunctionViaMethod(state, javaMethod);
} }
catch (SpelEvaluationException ex) { catch (SpelEvaluationException ex) {
ex.setPosition(getStartPosition()); ex.setPosition(getStartPosition());
throw ex; throw ex;
} }
} }
if (!(resolvedValue instanceof Method function)) {
// Possibly a static Java method registered as a function // Function registered via a MethodHandle.
throw new SpelEvaluationException( if (function instanceof MethodHandle methodHandle) {
SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, this.name, value.getClass()); try {
return executeFunctionViaMethodHandle(state, methodHandle);
}
catch (SpelEvaluationException ex) {
ex.setPosition(getStartPosition());
throw ex;
}
} }
try { // Neither a Method nor a MethodHandle?
return executeFunctionJLRMethod(state, function); throw new SpelEvaluationException(
} SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, this.name, value.getClass());
catch (SpelEvaluationException ex) {
ex.setPosition(getStartPosition());
throw ex;
}
} }
/** /**
* Execute a function represented as a {@code java.lang.reflect.Method}. * Execute a function represented as a {@link Method}.
* @param state the expression evaluation state * @param state the expression evaluation state
* @param method the method to invoke * @param method the method to invoke
* @return the return value of the invoked Java method * @return the return value of the invoked Java method
* @throws EvaluationException if there is any problem invoking the method * @throws EvaluationException if there is any problem invoking the method
*/ */
private TypedValue executeFunctionJLRMethod(ExpressionState state, Method method) throws EvaluationException { private TypedValue executeFunctionViaMethod(ExpressionState state, Method method) throws EvaluationException {
Object[] functionArgs = getArguments(state); Object[] functionArgs = getArguments(state);
if (!method.isVarArgs()) { if (!method.isVarArgs()) {
@ -151,17 +161,17 @@ public class FunctionReference extends SpelNodeImpl {
} }
/** /**
* Execute a function represented as {@code java.lang.invoke.MethodHandle}. * Execute a function represented as {@link MethodHandle}.
* Method types that take no arguments (fully bound handles or static methods * <p>Method types that take no arguments (fully bound handles or static methods
* with no parameters) can use {@code #invoke()} which is the most efficient. * with no parameters) can use {@link MethodHandle#invoke()} which is the most
* Otherwise, {@code #invokeWithArguments)} is used. * efficient. Otherwise, {@link MethodHandle#invokeWithArguments()} is used.
* @param state the expression evaluation state * @param state the expression evaluation state
* @param methodHandle the method to invoke * @param methodHandle the method handle to invoke
* @return the return value of the invoked Java method * @return the return value of the invoked Java method
* @throws EvaluationException if there is any problem invoking the method * @throws EvaluationException if there is any problem invoking the method
* @since 6.1 * @since 6.1
*/ */
private TypedValue executeFunctionBoundMethodHandle(ExpressionState state, MethodHandle methodHandle) throws EvaluationException { private TypedValue executeFunctionViaMethodHandle(ExpressionState state, MethodHandle methodHandle) throws EvaluationException {
Object[] functionArgs = getArguments(state); Object[] functionArgs = getArguments(state);
MethodType declaredParams = methodHandle.type(); MethodType declaredParams = methodHandle.type();
int spelParamCount = functionArgs.length; int spelParamCount = functionArgs.length;
@ -169,17 +179,15 @@ public class FunctionReference extends SpelNodeImpl {
boolean isSuspectedVarargs = declaredParams.lastParameterType().isArray(); boolean isSuspectedVarargs = declaredParams.lastParameterType().isArray();
if (spelParamCount < declaredParamCount || (spelParamCount > declaredParamCount if (spelParamCount < declaredParamCount || (spelParamCount > declaredParamCount && !isSuspectedVarargs)) {
&& !isSuspectedVarargs)) { // incorrect number, including more arguments and not a vararg
//incorrect number, including more arguments and not a vararg // perhaps a subset of arguments was provided but the MethodHandle wasn't bound?
throw new SpelEvaluationException(SpelMessage.INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION, throw new SpelEvaluationException(SpelMessage.INCORRECT_NUMBER_OF_ARGUMENTS_TO_FUNCTION,
functionArgs.length, declaredParamCount); functionArgs.length, declaredParamCount);
//perhaps a subset of arguments was provided but the MethodHandle wasn't bound?
} }
// simplest case: the MethodHandle is fully bound or represents a static method with no params: // simplest case: the MethodHandle is fully bound or represents a static method with no params:
if (declaredParamCount == 0) { if (declaredParamCount == 0) {
//note we consider MethodHandles not compilable
try { try {
return new TypedValue(methodHandle.invoke()); return new TypedValue(methodHandle.invoke());
} }
@ -188,6 +196,7 @@ public class FunctionReference extends SpelNodeImpl {
this.name, ex.getMessage()); this.name, ex.getMessage());
} }
finally { finally {
// Note: we consider MethodHandles not compilable
this.exitTypeDescriptor = null; this.exitTypeDescriptor = null;
this.method = null; this.method = null;
} }
@ -202,12 +211,11 @@ public class FunctionReference extends SpelNodeImpl {
ReflectionHelper.convertAllMethodHandleArguments(converter, functionArgs, methodHandle, varArgPosition); ReflectionHelper.convertAllMethodHandleArguments(converter, functionArgs, methodHandle, varArgPosition);
if (isSuspectedVarargs && declaredParamCount == 1) { if (isSuspectedVarargs && declaredParamCount == 1) {
//we only repack the varargs if it is the ONLY argument // we only repack the varargs if it is the ONLY argument
functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation( functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation(
methodHandle.type().parameterArray(), functionArgs); methodHandle.type().parameterArray(), functionArgs);
} }
//note we consider MethodHandles not compilable
try { try {
return new TypedValue(methodHandle.invokeWithArguments(functionArgs)); return new TypedValue(methodHandle.invokeWithArguments(functionArgs));
} }
@ -216,6 +224,7 @@ public class FunctionReference extends SpelNodeImpl {
this.name, ex.getMessage()); this.name, ex.getMessage());
} }
finally { finally {
// Note: we consider MethodHandles not compilable
this.exitTypeDescriptor = null; this.exitTypeDescriptor = null;
this.method = null; this.method = null;
} }

View File

@ -180,12 +180,12 @@ public class Indexer extends SpelNodeImpl {
} }
else { else {
this.indexedType = IndexedType.STRING; this.indexedType = IndexedType.STRING;
return new StringIndexingLValue((String) target, idx, targetDescriptor); return new StringIndexingValueRef((String) target, idx, targetDescriptor);
} }
} }
// Try and treat the index value as a property of the context object // Try and treat the index value as a property of the context object
// TODO: could call the conversion service to convert the value to a String // TODO Could call the conversion service to convert the value to a String
TypeDescriptor valueType = indexValue.getTypeDescriptor(); TypeDescriptor valueType = indexValue.getTypeDescriptor();
if (valueType != null && String.class == valueType.getType()) { if (valueType != null && String.class == valueType.getType()) {
this.indexedType = IndexedType.OBJECT; this.indexedType = IndexedType.OBJECT;
@ -226,41 +226,42 @@ public class Indexer extends SpelNodeImpl {
} }
if (this.indexedType == IndexedType.ARRAY) { if (this.indexedType == IndexedType.ARRAY) {
int insn; int insn = switch (this.exitTypeDescriptor) {
if ("D".equals(this.exitTypeDescriptor)) { case "D" -> {
mv.visitTypeInsn(CHECKCAST, "[D"); mv.visitTypeInsn(CHECKCAST, "[D");
insn = DALOAD; yield DALOAD;
} }
else if ("F".equals(this.exitTypeDescriptor)) { case "F" -> {
mv.visitTypeInsn(CHECKCAST, "[F"); mv.visitTypeInsn(CHECKCAST, "[F");
insn = FALOAD; yield FALOAD;
} }
else if ("J".equals(this.exitTypeDescriptor)) { case "J" -> {
mv.visitTypeInsn(CHECKCAST, "[J"); mv.visitTypeInsn(CHECKCAST, "[J");
insn = LALOAD; yield LALOAD;
} }
else if ("I".equals(this.exitTypeDescriptor)) { case "I" -> {
mv.visitTypeInsn(CHECKCAST, "[I"); mv.visitTypeInsn(CHECKCAST, "[I");
insn = IALOAD; yield IALOAD;
} }
else if ("S".equals(this.exitTypeDescriptor)) { case "S" -> {
mv.visitTypeInsn(CHECKCAST, "[S"); mv.visitTypeInsn(CHECKCAST, "[S");
insn = SALOAD; yield SALOAD;
} }
else if ("B".equals(this.exitTypeDescriptor)) { case "B" -> {
mv.visitTypeInsn(CHECKCAST, "[B"); mv.visitTypeInsn(CHECKCAST, "[B");
insn = BALOAD; yield BALOAD;
} }
else if ("C".equals(this.exitTypeDescriptor)) { case "C" -> {
mv.visitTypeInsn(CHECKCAST, "[C"); mv.visitTypeInsn(CHECKCAST, "[C");
insn = CALOAD; yield CALOAD;
} }
else { default -> {
mv.visitTypeInsn(CHECKCAST, "["+ this.exitTypeDescriptor + mv.visitTypeInsn(CHECKCAST, "["+ this.exitTypeDescriptor +
(CodeFlow.isPrimitiveArray(this.exitTypeDescriptor) ? "" : ";")); (CodeFlow.isPrimitiveArray(this.exitTypeDescriptor) ? "" : ";"));
//depthPlusOne(exitTypeDescriptor)+"Ljava/lang/Object;"); yield AALOAD;
insn = AALOAD; }
} };
SpelNodeImpl index = this.children[0]; SpelNodeImpl index = this.children[0];
cf.enterCompilationScope(); cf.enterCompilationScope();
index.generateCode(mv, cf); index.generateCode(mv, cf);
@ -325,6 +326,7 @@ public class Indexer extends SpelNodeImpl {
@Override @Override
public String toStringAST() { public String toStringAST() {
// TODO Since we do not support multidimensional arrays, we should be able to return: "[" + getChild(0).toStringAST() + "]"
StringJoiner sj = new StringJoiner(",", "[", "]"); StringJoiner sj = new StringJoiner(",", "[", "]");
for (int i = 0; i < getChildCount(); i++) { for (int i = 0; i < getChildCount(); i++) {
sj.add(getChild(i).toStringAST()); sj.add(getChild(i).toStringAST());
@ -744,7 +746,7 @@ public class Indexer extends SpelNodeImpl {
} }
private class StringIndexingLValue implements ValueRef { private class StringIndexingValueRef implements ValueRef {
private final String target; private final String target;
@ -752,7 +754,7 @@ public class Indexer extends SpelNodeImpl {
private final TypeDescriptor typeDescriptor; private final TypeDescriptor typeDescriptor;
public StringIndexingLValue(String target, int index, TypeDescriptor typeDescriptor) { public StringIndexingValueRef(String target, int index, TypeDescriptor typeDescriptor) {
this.target = target; this.target = target;
this.index = index; this.index = index;
this.typeDescriptor = typeDescriptor; this.typeDescriptor = typeDescriptor;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -111,8 +111,7 @@ public class InlineList extends SpelNodeImpl {
public String toStringAST() { public String toStringAST() {
StringJoiner sj = new StringJoiner(",", "{", "}"); StringJoiner sj = new StringJoiner(",", "{", "}");
// String ast matches input string, not the 'toString()' of the resultant collection, which would use [] // String ast matches input string, not the 'toString()' of the resultant collection, which would use []
int count = getChildCount(); for (int c = 0; c < getChildCount(); c++) {
for (int c = 0; c < count; c++) {
sj.add(getChild(c).toStringAST()); sj.add(getChild(c).toStringAST());
} }
return sj.toString(); return sj.toString();

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -143,8 +143,7 @@ public class InlineMap extends SpelNodeImpl {
@Override @Override
public String toStringAST() { public String toStringAST() {
StringBuilder sb = new StringBuilder("{"); StringBuilder sb = new StringBuilder("{");
int count = getChildCount(); for (int c = 0; c < getChildCount(); c++) {
for (int c = 0; c < count; c++) {
if (c > 0) { if (c > 0) {
sb.append(','); sb.append(',');
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2022 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -60,10 +60,7 @@ public class Projection extends SpelNodeImpl {
@Override @Override
protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { protected ValueRef getValueRef(ExpressionState state) throws EvaluationException {
TypedValue op = state.getActiveContextObject(); TypedValue op = state.getActiveContextObject();
Object operand = op.getValue(); Object operand = op.getValue();
boolean operandIsArray = ObjectUtils.isArray(operand);
// TypeDescriptor operandTypeDescriptor = op.getTypeDescriptor();
// When the input is a map, we push a special context object on the stack // When the input is a map, we push a special context object on the stack
// before calling the specified operation. This special context object // before calling the specified operation. This special context object
@ -86,6 +83,7 @@ public class Projection extends SpelNodeImpl {
return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this); // TODO unable to build correct type descriptor return new ValueRef.TypedValueHolderValueRef(new TypedValue(result), this); // TODO unable to build correct type descriptor
} }
boolean operandIsArray = ObjectUtils.isArray(operand);
if (operand instanceof Iterable || operandIsArray) { if (operand instanceof Iterable || operandIsArray) {
Iterable<?> data = (operand instanceof Iterable<?> iterable ? Iterable<?> data = (operand instanceof Iterable<?> iterable ?
iterable : Arrays.asList(ObjectUtils.toObjectArray(operand))); iterable : Arrays.asList(ObjectUtils.toObjectArray(operand)));

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -90,7 +90,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
@Override @Override
public ValueRef getValueRef(ExpressionState state) throws EvaluationException { public ValueRef getValueRef(ExpressionState state) throws EvaluationException {
return new AccessorLValue(this, state.getActiveContextObject(), state.getEvaluationContext(), return new AccessorValueRef(this, state.getActiveContextObject(), state.getEvaluationContext(),
state.getConfiguration().isAutoGrowNullReferences()); state.getConfiguration().isAutoGrowNullReferences());
} }
@ -391,7 +391,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
} }
private static class AccessorLValue implements ValueRef { private static class AccessorValueRef implements ValueRef {
private final PropertyOrFieldReference ref; private final PropertyOrFieldReference ref;
@ -401,7 +401,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
private final boolean autoGrowNullReferences; private final boolean autoGrowNullReferences;
public AccessorLValue(PropertyOrFieldReference propertyOrFieldReference, TypedValue activeContextObject, public AccessorValueRef(PropertyOrFieldReference propertyOrFieldReference, TypedValue activeContextObject,
EvaluationContext evalContext, boolean autoGrowNullReferences) { EvaluationContext evalContext, boolean autoGrowNullReferences) {
this.ref = propertyOrFieldReference; this.ref = propertyOrFieldReference;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,39 +19,47 @@ package org.springframework.expression.spel.standard;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
* Holder for a kind of token, the associated data and its position in the input data * Holder for a kind of token, the associated data, and its position in the input
* stream (start/end). * data stream (start/end).
* *
* @author Andy Clement * @author Andy Clement
* @since 3.0 * @since 3.0
*/ */
class Token { class Token {
TokenKind kind; final TokenKind kind;
@Nullable @Nullable
String data; final String data;
int startPos; // index of first character final int startPos;
int endPos; // index of char after the last character final int endPos;
/** /**
* Constructor for use when there is no particular data for the token * Constructor for use when there is no particular data for the token
* (e.g. TRUE or '+') * (e.g. TRUE or '+').
* @param startPos the exact start * @param tokenKind the kind of token
* @param endPos the index to the last character * @param startPos the exact start position
* @param endPos the index of the last character
*/ */
Token(TokenKind tokenKind, int startPos, int endPos) { Token(TokenKind tokenKind, int startPos, int endPos) {
this.kind = tokenKind; this(tokenKind, null, startPos, endPos);
this.startPos = startPos;
this.endPos = endPos;
} }
/**
* Constructor for use when there is data for the token.
* @param tokenKind the kind of token
* @param tokenData the data for the token
* @param startPos the exact start position
* @param endPos the index of the last character
*/
Token(TokenKind tokenKind, char[] tokenData, int startPos, int endPos) { Token(TokenKind tokenKind, char[] tokenData, int startPos, int endPos) {
this(tokenKind, startPos, endPos); this.kind = tokenKind;
this.data = new String(tokenData); this.data = (tokenData != null ? new String(tokenData) : null);
this.startPos = startPos;
this.endPos = endPos;
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -279,7 +279,7 @@ public class StandardEvaluationContext implements EvaluationContext {
} }
/** /**
* Register the specified Method as a SpEL function. * Register the specified {@link Method} as a SpEL function.
* <p>Note: Function names share a namespace with the variables in this * <p>Note: Function names share a namespace with the variables in this
* evaluation context, as populated by {@link #setVariable(String, Object)}. * evaluation context, as populated by {@link #setVariable(String, Object)}.
* Make sure that specified function names and variable names do not overlap. * Make sure that specified function names and variable names do not overlap.
@ -292,7 +292,7 @@ public class StandardEvaluationContext implements EvaluationContext {
} }
/** /**
* Register the specified MethodHandle as a SpEL function. * Register the specified {@link MethodHandle} as a SpEL function.
* <p>Note: Function names share a namespace with the variables in this * <p>Note: Function names share a namespace with the variables in this
* evaluation context, as populated by {@link #setVariable(String, Object)}. * evaluation context, as populated by {@link #setVariable(String, Object)}.
* Make sure that specified function names and variable names do not overlap. * Make sure that specified function names and variable names do not overlap.

View File

@ -112,6 +112,24 @@ class ParsingTests {
parseCheck("#var1='value1'"); parseCheck("#var1='value1'");
} }
@Test
void indexing() {
parseCheck("#var[2]");
parseCheck("person['name']");
parseCheck("person[name]");
parseCheck("array[2]");
parseCheck("array[2][3]");
parseCheck("func()[2]");
parseCheck("#func()[2]");
parseCheck("'abc'[2]");
parseCheck("\"abc\"[2]", "'abc'[2]");
parseCheck("{1,2,3}[2]");
parseCheck("{'k':'v'}['k']");
parseCheck("{'k':'v'}[k]");
parseCheck("{'k1':'v1','k2':'v2'}['k2']");
parseCheck("{'k1':'v1','k2':'v2'}[k2]");
}
@Test @Test
void projection() { void projection() {
parseCheck("{1,2,3,4,5,6,7,8,9,10}.![#isEven()]"); parseCheck("{1,2,3,4,5,6,7,8,9,10}.![#isEven()]");

View File

@ -40,15 +40,22 @@ import org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMat
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMatchKind.CLOSE;
import static org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMatchKind.EXACT;
import static org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMatchKind.REQUIRES_CONVERSION;
/** /**
* Tests for reflection helper code. * Tests for {@link ReflectionHelper}, {@link SpelUtilities}, {@link TypedValue},
* {@link ReflectivePropertyAccessor}, ...
* *
* @author Andy Clement * @author Andy Clement
* @author Sam Brannen * @author Sam Brannen
*/ */
class ReflectionHelperTests extends AbstractExpressionTests { class ReflectionHelperTests extends AbstractExpressionTests {
private final StandardTypeConverter tc = new StandardTypeConverter();
@Test @Test
void utilities() throws ParseException { void utilities() throws ParseException {
SpelExpression expr = (SpelExpression)parser.parseExpression("3+4+5+6+7-2"); SpelExpression expr = (SpelExpression)parser.parseExpression("3+4+5+6+7-2");
@ -100,44 +107,38 @@ class ReflectionHelperTests extends AbstractExpressionTests {
@Test @Test
void reflectionHelperCompareArguments_ExactMatching() { void reflectionHelperCompareArguments_ExactMatching() {
StandardTypeConverter tc = new StandardTypeConverter();
// Calling foo(String) with (String) is exact match // Calling foo(String) with (String) is exact match
checkMatch(new Class<?>[] {String.class}, new Class<?>[] {String.class}, tc, ReflectionHelper.ArgumentsMatchKind.EXACT); checkMatch(new Class<?>[] {String.class}, new Class<?>[] {String.class}, tc, EXACT);
// Calling foo(String,Integer) with (String,Integer) is exact match // Calling foo(String,Integer) with (String,Integer) is exact match
checkMatch(new Class<?>[] {String.class, Integer.class}, new Class<?>[] {String.class, Integer.class}, tc, ArgumentsMatchKind.EXACT); checkMatch(new Class<?>[] {String.class, Integer.class}, new Class<?>[] {String.class, Integer.class}, tc, EXACT);
} }
@Test @Test
void reflectionHelperCompareArguments_CloseMatching() { void reflectionHelperCompareArguments_CloseMatching() {
StandardTypeConverter tc = new StandardTypeConverter();
// Calling foo(List) with (ArrayList) is close match (no conversion required) // Calling foo(List) with (ArrayList) is close match (no conversion required)
checkMatch(new Class<?>[] {ArrayList.class}, new Class<?>[] {List.class}, tc, ArgumentsMatchKind.CLOSE); checkMatch(new Class<?>[] {ArrayList.class}, new Class<?>[] {List.class}, tc, CLOSE);
// Passing (Sub,String) on call to foo(Super,String) is close match // Passing (Sub,String) on call to foo(Super,String) is close match
checkMatch(new Class<?>[] {Sub.class, String.class}, new Class<?>[] {Super.class, String.class}, tc, ArgumentsMatchKind.CLOSE); checkMatch(new Class<?>[] {Sub.class, String.class}, new Class<?>[] {Super.class, String.class}, tc, CLOSE);
// Passing (String,Sub) on call to foo(String,Super) is close match // Passing (String,Sub) on call to foo(String,Super) is close match
checkMatch(new Class<?>[] {String.class, Sub.class}, new Class<?>[] {String.class, Super.class}, tc, ArgumentsMatchKind.CLOSE); checkMatch(new Class<?>[] {String.class, Sub.class}, new Class<?>[] {String.class, Super.class}, tc, CLOSE);
} }
@Test @Test
void reflectionHelperCompareArguments_RequiresConversionMatching() { void reflectionHelperCompareArguments_CloseMatching_WithAutoBoxing() {
StandardTypeConverter tc = new StandardTypeConverter();
// Calling foo(String,int) with (String,Integer) requires boxing conversion of argument one // Calling foo(String,int) with (String,Integer) requires boxing conversion of argument one
checkMatch(new Class<?>[] {String.class, int.class}, new Class<?>[] {String.class,Integer.class},tc, ArgumentsMatchKind.CLOSE); checkMatch(new Class<?>[] {String.class, int.class}, new Class<?>[] {String.class, Integer.class},tc, CLOSE);
// Passing (int,String) on call to foo(Integer,String) requires boxing conversion of argument zero // Passing (int,String) on call to foo(Integer,String) requires boxing conversion of argument zero
checkMatch(new Class<?>[] {int.class, String.class}, new Class<?>[] {Integer.class, String.class},tc, ArgumentsMatchKind.CLOSE); checkMatch(new Class<?>[] {int.class, String.class}, new Class<?>[] {Integer.class, String.class},tc, CLOSE);
// Passing (int,Sub) on call to foo(Integer,Super) requires boxing conversion of argument zero // Passing (int,Sub) on call to foo(Integer,Super) requires boxing conversion of argument zero
checkMatch(new Class<?>[] {int.class, Sub.class}, new Class<?>[] {Integer.class, Super.class}, tc, ArgumentsMatchKind.CLOSE); checkMatch(new Class<?>[] {int.class, Sub.class}, new Class<?>[] {Integer.class, Super.class}, tc, CLOSE);
// Passing (int,Sub,boolean) on call to foo(Integer,Super,Boolean) requires boxing conversion of arguments zero and two // Passing (int,Sub,boolean) on call to foo(Integer,Super,Boolean) requires boxing conversion of arguments zero and two
// TODO checkMatch(new Class<?>[] {int.class, Sub.class, boolean.class}, new Class<?>[] {Integer.class, Super.class, Boolean.class}, tc, ArgsMatchKind.REQUIRES_CONVERSION); checkMatch(new Class<?>[] {int.class, Sub.class, boolean.class}, new Class<?>[] {Integer.class, Super.class, Boolean.class}, tc, CLOSE);
} }
@Test @Test
@ -149,59 +150,56 @@ class ReflectionHelperTests extends AbstractExpressionTests {
} }
@Test @Test
void reflectionHelperCompareArguments_Varargs_ExactMatching() { void reflectionHelperCompareArguments_Varargs() {
StandardTypeConverter tc = new StandardTypeConverter();
// Passing (String[]) on call to (String[]) is exact match // Passing (String[]) on call to (String[]) is exact match
checkMatch2(new Class<?>[] {String[].class}, new Class<?>[] {String[].class}, tc, ArgumentsMatchKind.EXACT); checkMatchVarargs(new Class<?>[] {String[].class}, new Class<?>[] {String[].class}, tc, EXACT);
// Passing (Integer, String[]) on call to (Integer, String[]) is exact match // Passing (Integer, String[]) on call to (Integer, String[]) is exact match
checkMatch2(new Class<?>[] {Integer.class, String[].class}, new Class<?>[] {Integer.class, String[].class}, tc, ArgumentsMatchKind.EXACT); checkMatchVarargs(new Class<?>[] {Integer.class, String[].class}, new Class<?>[] {Integer.class, String[].class}, tc, EXACT);
// Passing (String, Integer, String[]) on call to (String, String, String[]) is exact match // Passing (String, Integer, String[]) on call to (String, String, String[]) is exact match
checkMatch2(new Class<?>[] {String.class, Integer.class, String[].class}, new Class<?>[] {String.class,Integer.class, String[].class}, tc, ArgumentsMatchKind.EXACT); checkMatchVarargs(new Class<?>[] {String.class, Integer.class, String[].class}, new Class<?>[] {String.class,Integer.class, String[].class}, tc, EXACT);
// Passing (Sub, String[]) on call to (Super, String[]) is exact match // Passing (Sub, String[]) on call to (Super, String[]) is exact match
checkMatch2(new Class<?>[] {Sub.class, String[].class}, new Class<?>[] {Super.class,String[].class}, tc, ArgumentsMatchKind.CLOSE); checkMatchVarargs(new Class<?>[] {Sub.class, String[].class}, new Class<?>[] {Super.class,String[].class}, tc, CLOSE);
// Passing (Integer, String[]) on call to (String, String[]) is exact match // Passing (Integer, String[]) on call to (String, String[]) is exact match
checkMatch2(new Class<?>[] {Integer.class, String[].class}, new Class<?>[] {String.class, String[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION); checkMatchVarargs(new Class<?>[] {Integer.class, String[].class}, new Class<?>[] {String.class, String[].class}, tc, REQUIRES_CONVERSION);
// Passing (Integer, Sub, String[]) on call to (String, Super, String[]) is exact match // Passing (Integer, Sub, String[]) on call to (String, Super, String[]) is exact match
checkMatch2(new Class<?>[] {Integer.class, Sub.class, String[].class}, new Class<?>[] {String.class, Super.class, String[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION); checkMatchVarargs(new Class<?>[] {Integer.class, Sub.class, String[].class}, new Class<?>[] {String.class, Super.class, String[].class}, tc, REQUIRES_CONVERSION);
// Passing (String) on call to (String[]) is exact match // Passing (String) on call to (String[]) is exact match
checkMatch2(new Class<?>[] {String.class}, new Class<?>[] {String[].class}, tc, ArgumentsMatchKind.EXACT); checkMatchVarargs(new Class<?>[] {String.class}, new Class<?>[] {String[].class}, tc, EXACT);
// Passing (Integer,String) on call to (Integer,String[]) is exact match // Passing (Integer,String) on call to (Integer,String[]) is exact match
checkMatch2(new Class<?>[] {Integer.class, String.class}, new Class<?>[] {Integer.class, String[].class}, tc, ArgumentsMatchKind.EXACT); checkMatchVarargs(new Class<?>[] {Integer.class, String.class}, new Class<?>[] {Integer.class, String[].class}, tc, EXACT);
// Passing (String) on call to (Integer[]) is conversion match (String to Integer) // Passing (String) on call to (Integer[]) is conversion match (String to Integer)
checkMatch2(new Class<?>[] {String.class}, new Class<?>[] {Integer[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION); checkMatchVarargs(new Class<?>[] {String.class}, new Class<?>[] {Integer[].class}, tc, REQUIRES_CONVERSION);
// Passing (Sub) on call to (Super[]) is close match // Passing (Sub) on call to (Super[]) is close match
checkMatch2(new Class<?>[] {Sub.class}, new Class<?>[] {Super[].class}, tc, ArgumentsMatchKind.CLOSE); checkMatchVarargs(new Class<?>[] {Sub.class}, new Class<?>[] {Super[].class}, tc, CLOSE);
// Passing (Super) on call to (Sub[]) is not a match // Passing (Super) on call to (Sub[]) is not a match
checkMatch2(new Class<?>[] {Super.class}, new Class<?>[] {Sub[].class}, tc, null); checkMatchVarargs(new Class<?>[] {Super.class}, new Class<?>[] {Sub[].class}, tc, null);
checkMatch2(new Class<?>[] {Unconvertable.class, String.class}, new Class<?>[] {Sub.class, Super[].class}, tc, null); checkMatchVarargs(new Class<?>[] {Unconvertable.class, String.class}, new Class<?>[] {Sub.class, Super[].class}, tc, null);
checkMatch2(new Class<?>[] {Integer.class, Integer.class, String.class}, new Class<?>[] {String.class, String.class, Super[].class}, tc, null); checkMatchVarargs(new Class<?>[] {Integer.class, Integer.class, String.class}, new Class<?>[] {String.class, String.class, Super[].class}, tc, null);
checkMatch2(new Class<?>[] {Unconvertable.class, String.class}, new Class<?>[] {Sub.class, Super[].class}, tc, null); checkMatchVarargs(new Class<?>[] {Unconvertable.class, String.class}, new Class<?>[] {Sub.class, Super[].class}, tc, null);
checkMatch2(new Class<?>[] {Integer.class, Integer.class, String.class}, new Class<?>[] {String.class, String.class, Super[].class}, tc, null); checkMatchVarargs(new Class<?>[] {Integer.class, Integer.class, String.class}, new Class<?>[] {String.class, String.class, Super[].class}, tc, null);
checkMatch2(new Class<?>[] {Integer.class, Integer.class, Sub.class}, new Class<?>[] {String.class, String.class, Super[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION); checkMatchVarargs(new Class<?>[] {Integer.class, Integer.class, Sub.class}, new Class<?>[] {String.class, String.class, Super[].class}, tc, REQUIRES_CONVERSION);
checkMatch2(new Class<?>[] {Integer.class, Integer.class, Integer.class}, new Class<?>[] {Integer.class, String[].class}, tc, ArgumentsMatchKind.REQUIRES_CONVERSION); checkMatchVarargs(new Class<?>[] {Integer.class, Integer.class, Integer.class}, new Class<?>[] {Integer.class, String[].class}, tc, REQUIRES_CONVERSION);
// what happens on (Integer,String) passed to (Integer[]) ? // what happens on (Integer,String) passed to (Integer[]) ?
} }
@Test @Test
void convertArguments() throws Exception { void convertArguments() throws Exception {
StandardTypeConverter tc = new StandardTypeConverter();
Method oneArg = TestInterface.class.getMethod("oneArg", String.class); Method oneArg = TestInterface.class.getMethod("oneArg", String.class);
Method twoArg = TestInterface.class.getMethod("twoArg", String.class, String[].class); Method twoArg = TestInterface.class.getMethod("twoArg", String.class, String[].class);
@ -227,8 +225,7 @@ class ReflectionHelperTests extends AbstractExpressionTests {
} }
@Test @Test
void convertArguments2() throws Exception { void convertAllArguments() throws Exception {
StandardTypeConverter tc = new StandardTypeConverter();
Method oneArg = TestInterface.class.getMethod("oneArg", String.class); Method oneArg = TestInterface.class.getMethod("oneArg", String.class);
Method twoArg = TestInterface.class.getMethod("twoArg", String.class, String[].class); Method twoArg = TestInterface.class.getMethod("twoArg", String.class, String[].class);
@ -254,7 +251,7 @@ class ReflectionHelperTests extends AbstractExpressionTests {
} }
@Test @Test
void setupArguments() { void setupArgumentsForVarargsInvocation() {
Object[] newArray = ReflectionHelper.setupArgumentsForVarargsInvocation( Object[] newArray = ReflectionHelper.setupArgumentsForVarargsInvocation(
new Class<?>[] {String[].class}, "a", "b", "c"); new Class<?>[] {String[].class}, "a", "b", "c");
@ -402,21 +399,21 @@ class ReflectionHelperTests extends AbstractExpressionTests {
assertThat(matchInfo).as("Should not be a null match").isNotNull(); assertThat(matchInfo).as("Should not be a null match").isNotNull();
} }
if (expectedMatchKind == ArgumentsMatchKind.EXACT) { if (expectedMatchKind == EXACT) {
assertThat(matchInfo.isExactMatch()).isTrue(); assertThat(matchInfo.isExactMatch()).isTrue();
} }
else if (expectedMatchKind == ArgumentsMatchKind.CLOSE) { else if (expectedMatchKind == CLOSE) {
assertThat(matchInfo.isCloseMatch()).isTrue(); assertThat(matchInfo.isCloseMatch()).isTrue();
} }
else if (expectedMatchKind == ArgumentsMatchKind.REQUIRES_CONVERSION) { else if (expectedMatchKind == REQUIRES_CONVERSION) {
assertThat(matchInfo.isMatchRequiringConversion()).as("expected to be a match requiring conversion, but was " + matchInfo).isTrue(); assertThat(matchInfo.isMatchRequiringConversion()).as("expected to be a match requiring conversion, but was " + matchInfo).isTrue();
} }
} }
/** /**
* Used to validate the match returned from a compareArguments call. * Used to validate the match returned from a compareArgumentsVarargs call.
*/ */
private static void checkMatch2(Class<?>[] inputTypes, Class<?>[] expectedTypes, private static void checkMatchVarargs(Class<?>[] inputTypes, Class<?>[] expectedTypes,
StandardTypeConverter typeConverter, ArgumentsMatchKind expectedMatchKind) { StandardTypeConverter typeConverter, ArgumentsMatchKind expectedMatchKind) {
ReflectionHelper.ArgumentsMatchInfo matchInfo = ReflectionHelper.ArgumentsMatchInfo matchInfo =
@ -438,14 +435,10 @@ class ReflectionHelperTests extends AbstractExpressionTests {
private static void checkArguments(Object[] args, Object... expected) { private static void checkArguments(Object[] args, Object... expected) {
assertThat(args).hasSize(expected.length); assertThat(args).hasSize(expected.length);
for (int i = 0; i < expected.length; i++) { for (int i = 0; i < expected.length; i++) {
checkArgument(expected[i], args[i]); assertThat(args[i]).isEqualTo(expected[i]);
} }
} }
private static void checkArgument(Object expected, Object actual) {
assertThat(actual).isEqualTo(expected);
}
private static List<TypeDescriptor> typeDescriptors(Class<?>... types) { private static List<TypeDescriptor> typeDescriptors(Class<?>... types) {
return Arrays.stream(types).map(TypeDescriptor::valueOf).toList(); return Arrays.stream(types).map(TypeDescriptor::valueOf).toList();
} }