From 888e50175d6e8b5e65b29d34b97a898fe5d1ae4c Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sat, 10 Feb 2024 15:03:02 +0100 Subject: [PATCH] Polish SpEL Javadocs and internals --- .../common/CompositeStringExpression.java | 8 +- .../expression/common/LiteralExpression.java | 12 +-- .../expression/spel/ast/FormatHelper.java | 6 +- .../spel/ast/FunctionReference.java | 79 ++++++++------- .../expression/spel/ast/Indexer.java | 80 ++++++++-------- .../expression/spel/ast/InlineList.java | 5 +- .../expression/spel/ast/InlineMap.java | 5 +- .../expression/spel/ast/Projection.java | 6 +- .../spel/ast/PropertyOrFieldReference.java | 8 +- .../expression/spel/standard/Token.java | 38 +++++--- .../support/StandardEvaluationContext.java | 6 +- .../expression/spel/ParsingTests.java | 18 ++++ .../spel/support/ReflectionHelperTests.java | 95 +++++++++---------- 13 files changed, 194 insertions(+), 172 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java b/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java index c98a543401..8bb6dcf195 100644 --- a/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java @@ -80,7 +80,7 @@ public class CompositeStringExpression implements Expression { @Override @Nullable public T getValue(@Nullable Class expectedResultType) throws EvaluationException { - Object value = getValue(); + String value = getValue(); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), expectedResultType); } @@ -99,7 +99,7 @@ public class CompositeStringExpression implements Expression { @Override @Nullable public T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { - Object value = getValue(rootObject); + String value = getValue(rootObject); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), desiredResultType); } @@ -120,7 +120,7 @@ public class CompositeStringExpression implements Expression { public T getValue(EvaluationContext context, @Nullable Class expectedResultType) throws EvaluationException { - Object value = getValue(context); + String value = getValue(context); return ExpressionUtils.convertTypedValue(context, new TypedValue(value), expectedResultType); } @@ -141,7 +141,7 @@ public class CompositeStringExpression implements Expression { public T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { - Object value = getValue(context,rootObject); + String value = getValue(context,rootObject); return ExpressionUtils.convertTypedValue(context, new TypedValue(value), desiredResultType); } diff --git a/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java b/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java index dbe3753421..ca9275ad21 100644 --- a/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java @@ -64,7 +64,7 @@ public class LiteralExpression implements Expression { @Override @Nullable public T getValue(@Nullable Class expectedResultType) throws EvaluationException { - Object value = getValue(); + String value = getValue(); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), expectedResultType); } @@ -76,7 +76,7 @@ public class LiteralExpression implements Expression { @Override @Nullable public T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { - Object value = getValue(rootObject); + String value = getValue(rootObject); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), desiredResultType); } @@ -87,10 +87,8 @@ public class LiteralExpression implements Expression { @Override @Nullable - public T getValue(EvaluationContext context, @Nullable Class expectedResultType) - throws EvaluationException { - - Object value = getValue(context); + public T getValue(EvaluationContext context, @Nullable Class expectedResultType) throws EvaluationException { + String value = getValue(context); return ExpressionUtils.convertTypedValue(context, new TypedValue(value), expectedResultType); } @@ -104,7 +102,7 @@ public class LiteralExpression implements Expression { public T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { - Object value = getValue(context, rootObject); + String value = getValue(context, rootObject); return ExpressionUtils.convertTypedValue(context, new TypedValue(value), desiredResultType); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java index f2d7c0a5e9..c05cfad674 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java @@ -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"); * 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.lang.Nullable; -import org.springframework.util.ClassUtils; /** * Utility methods (formatters, etc) used during parsing and evaluation. @@ -51,10 +50,9 @@ abstract class FormatHelper { *

A String array will have the formatted name "java.lang.String[]". * @param clazz the Class whose name is to be formatted * @return a formatted String suitable for message inclusion - * @see ClassUtils#getQualifiedName(Class) */ static String formatClassNameForMessage(@Nullable Class clazz) { - return (clazz != null ? ClassUtils.getQualifiedName(clazz) : "null"); + return (clazz != null ? clazz.getTypeName() : "null"); } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java index 9a97db5c1c..ba7dda43d0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java @@ -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"); * 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; /** - * A function reference is of the form "#someFunction(a,b,c)". Functions may be defined - * 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. + * A function reference is of the form "#someFunction(a,b,c)". * - *

Functions are very simplistic. The arguments are not part of the definition - * (right now), so the names must be unique. + *

Functions can be either a {@link Method} (for static Java methods) or a + * {@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 Juergen Hoeller + * @author Simon Baslé + * @author Sam Brannen * @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 { @@ -72,39 +77,44 @@ public class FunctionReference extends SpelNodeImpl { if (value == TypedValue.NULL) { throw new SpelEvaluationException(getStartPosition(), SpelMessage.FUNCTION_NOT_DEFINED, this.name); } - Object resolvedValue = value.getValue(); - if (resolvedValue instanceof MethodHandle methodHandle) { + Object function = value.getValue(); + + // 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 { - return executeFunctionBoundMethodHandle(state, methodHandle); + return executeFunctionViaMethod(state, javaMethod); } catch (SpelEvaluationException ex) { ex.setPosition(getStartPosition()); throw ex; } } - if (!(resolvedValue instanceof Method function)) { - // Possibly a static Java method registered as a function - throw new SpelEvaluationException( - SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, this.name, value.getClass()); + + // Function registered via a MethodHandle. + if (function instanceof MethodHandle methodHandle) { + try { + return executeFunctionViaMethodHandle(state, methodHandle); + } + catch (SpelEvaluationException ex) { + ex.setPosition(getStartPosition()); + throw ex; + } } - try { - return executeFunctionJLRMethod(state, function); - } - catch (SpelEvaluationException ex) { - ex.setPosition(getStartPosition()); - throw ex; - } + // Neither a Method nor a MethodHandle? + throw new SpelEvaluationException( + SpelMessage.FUNCTION_REFERENCE_CANNOT_BE_INVOKED, this.name, value.getClass()); } /** - * 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 method the method to invoke * @return the return value of the invoked Java 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); if (!method.isVarArgs()) { @@ -151,17 +161,17 @@ public class FunctionReference extends SpelNodeImpl { } /** - * Execute a function represented as {@code java.lang.invoke.MethodHandle}. - * Method types that take no arguments (fully bound handles or static methods - * with no parameters) can use {@code #invoke()} which is the most efficient. - * Otherwise, {@code #invokeWithArguments)} is used. + * Execute a function represented as {@link MethodHandle}. + *

Method types that take no arguments (fully bound handles or static methods + * with no parameters) can use {@link MethodHandle#invoke()} which is the most + * efficient. Otherwise, {@link MethodHandle#invokeWithArguments()} is used. * @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 * @throws EvaluationException if there is any problem invoking the method * @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); MethodType declaredParams = methodHandle.type(); int spelParamCount = functionArgs.length; @@ -169,17 +179,15 @@ public class FunctionReference extends SpelNodeImpl { boolean isSuspectedVarargs = declaredParams.lastParameterType().isArray(); - if (spelParamCount < declaredParamCount || (spelParamCount > declaredParamCount - && !isSuspectedVarargs)) { - //incorrect number, including more arguments and not a vararg + if (spelParamCount < declaredParamCount || (spelParamCount > declaredParamCount && !isSuspectedVarargs)) { + // 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, 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: if (declaredParamCount == 0) { - //note we consider MethodHandles not compilable try { return new TypedValue(methodHandle.invoke()); } @@ -188,6 +196,7 @@ public class FunctionReference extends SpelNodeImpl { this.name, ex.getMessage()); } finally { + // Note: we consider MethodHandles not compilable this.exitTypeDescriptor = null; this.method = null; } @@ -202,12 +211,11 @@ public class FunctionReference extends SpelNodeImpl { ReflectionHelper.convertAllMethodHandleArguments(converter, functionArgs, methodHandle, varArgPosition); 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( methodHandle.type().parameterArray(), functionArgs); } - //note we consider MethodHandles not compilable try { return new TypedValue(methodHandle.invokeWithArguments(functionArgs)); } @@ -216,6 +224,7 @@ public class FunctionReference extends SpelNodeImpl { this.name, ex.getMessage()); } finally { + // Note: we consider MethodHandles not compilable this.exitTypeDescriptor = null; this.method = null; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index e3f703fb0b..aed383a1d0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -180,12 +180,12 @@ public class Indexer extends SpelNodeImpl { } else { 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 - // 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(); if (valueType != null && String.class == valueType.getType()) { this.indexedType = IndexedType.OBJECT; @@ -226,41 +226,42 @@ public class Indexer extends SpelNodeImpl { } if (this.indexedType == IndexedType.ARRAY) { - int insn; - if ("D".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[D"); - insn = DALOAD; - } - else if ("F".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[F"); - insn = FALOAD; - } - else if ("J".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[J"); - insn = LALOAD; - } - else if ("I".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[I"); - insn = IALOAD; - } - else if ("S".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[S"); - insn = SALOAD; - } - else if ("B".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[B"); - insn = BALOAD; - } - else if ("C".equals(this.exitTypeDescriptor)) { - mv.visitTypeInsn(CHECKCAST, "[C"); - insn = CALOAD; - } - else { - mv.visitTypeInsn(CHECKCAST, "["+ this.exitTypeDescriptor + - (CodeFlow.isPrimitiveArray(this.exitTypeDescriptor) ? "" : ";")); - //depthPlusOne(exitTypeDescriptor)+"Ljava/lang/Object;"); - insn = AALOAD; - } + int insn = switch (this.exitTypeDescriptor) { + case "D" -> { + mv.visitTypeInsn(CHECKCAST, "[D"); + yield DALOAD; + } + case "F" -> { + mv.visitTypeInsn(CHECKCAST, "[F"); + yield FALOAD; + } + case "J" -> { + mv.visitTypeInsn(CHECKCAST, "[J"); + yield LALOAD; + } + case "I" -> { + mv.visitTypeInsn(CHECKCAST, "[I"); + yield IALOAD; + } + case "S" -> { + mv.visitTypeInsn(CHECKCAST, "[S"); + yield SALOAD; + } + case "B" -> { + mv.visitTypeInsn(CHECKCAST, "[B"); + yield BALOAD; + } + case "C" -> { + mv.visitTypeInsn(CHECKCAST, "[C"); + yield CALOAD; + } + default -> { + mv.visitTypeInsn(CHECKCAST, "["+ this.exitTypeDescriptor + + (CodeFlow.isPrimitiveArray(this.exitTypeDescriptor) ? "" : ";")); + yield AALOAD; + } + }; + SpelNodeImpl index = this.children[0]; cf.enterCompilationScope(); index.generateCode(mv, cf); @@ -325,6 +326,7 @@ public class Indexer extends SpelNodeImpl { @Override public String toStringAST() { + // TODO Since we do not support multidimensional arrays, we should be able to return: "[" + getChild(0).toStringAST() + "]" StringJoiner sj = new StringJoiner(",", "[", "]"); for (int i = 0; i < getChildCount(); i++) { 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; @@ -752,7 +754,7 @@ public class Indexer extends SpelNodeImpl { private final TypeDescriptor typeDescriptor; - public StringIndexingLValue(String target, int index, TypeDescriptor typeDescriptor) { + public StringIndexingValueRef(String target, int index, TypeDescriptor typeDescriptor) { this.target = target; this.index = index; this.typeDescriptor = typeDescriptor; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java index 41eb8c8aab..339326e977 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java @@ -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"); * you may not use this file except in compliance with the License. @@ -111,8 +111,7 @@ public class InlineList extends SpelNodeImpl { public String toStringAST() { StringJoiner sj = new StringJoiner(",", "{", "}"); // String ast matches input string, not the 'toString()' of the resultant collection, which would use [] - int count = getChildCount(); - for (int c = 0; c < count; c++) { + for (int c = 0; c < getChildCount(); c++) { sj.add(getChild(c).toStringAST()); } return sj.toString(); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java index 29c6e191e4..a08c50217e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java @@ -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"); * you may not use this file except in compliance with the License. @@ -143,8 +143,7 @@ public class InlineMap extends SpelNodeImpl { @Override public String toStringAST() { StringBuilder sb = new StringBuilder("{"); - int count = getChildCount(); - for (int c = 0; c < count; c++) { + for (int c = 0; c < getChildCount(); c++) { if (c > 0) { sb.append(','); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java index b9a9c2cc0b..4c87dd0520 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java @@ -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"); * you may not use this file except in compliance with the License. @@ -60,10 +60,7 @@ public class Projection extends SpelNodeImpl { @Override protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { TypedValue op = state.getActiveContextObject(); - 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 // 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 } + boolean operandIsArray = ObjectUtils.isArray(operand); if (operand instanceof Iterable || operandIsArray) { Iterable data = (operand instanceof Iterable iterable ? iterable : Arrays.asList(ObjectUtils.toObjectArray(operand))); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java index dc891da4a4..b84c782104 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java @@ -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"); * you may not use this file except in compliance with the License. @@ -90,7 +90,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl { @Override 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()); } @@ -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; @@ -401,7 +401,7 @@ public class PropertyOrFieldReference extends SpelNodeImpl { private final boolean autoGrowNullReferences; - public AccessorLValue(PropertyOrFieldReference propertyOrFieldReference, TypedValue activeContextObject, + public AccessorValueRef(PropertyOrFieldReference propertyOrFieldReference, TypedValue activeContextObject, EvaluationContext evalContext, boolean autoGrowNullReferences) { this.ref = propertyOrFieldReference; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java index f00f26a300..257060262b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java @@ -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"); * 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; /** - * Holder for a kind of token, the associated data and its position in the input data - * stream (start/end). + * Holder for a kind of token, the associated data, and its position in the input + * data stream (start/end). * * @author Andy Clement * @since 3.0 */ class Token { - TokenKind kind; + final TokenKind kind; @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 - * (e.g. TRUE or '+') - * @param startPos the exact start - * @param endPos the index to the last character + * (e.g. TRUE or '+'). + * @param tokenKind the kind of token + * @param startPos the exact start position + * @param endPos the index of the last character */ Token(TokenKind tokenKind, int startPos, int endPos) { - this.kind = tokenKind; - this.startPos = startPos; - this.endPos = endPos; + this(tokenKind, null, startPos, 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) { - this(tokenKind, startPos, endPos); - this.data = new String(tokenData); + this.kind = tokenKind; + this.data = (tokenData != null ? new String(tokenData) : null); + this.startPos = startPos; + this.endPos = endPos; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java index 3178f74a7c..012487f0c2 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java @@ -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"); * 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. *

Note: Function names share a namespace with the variables in this * evaluation context, as populated by {@link #setVariable(String, Object)}. * 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. *

Note: Function names share a namespace with the variables in this * evaluation context, as populated by {@link #setVariable(String, Object)}. * Make sure that specified function names and variable names do not overlap. diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java index 1234f62bf1..3f9df2ca89 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ParsingTests.java @@ -112,6 +112,24 @@ class ParsingTests { 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 void projection() { parseCheck("{1,2,3,4,5,6,7,8,9,10}.![#isEven()]"); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/support/ReflectionHelperTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/support/ReflectionHelperTests.java index 6e1b9c08cc..1e990417dd 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/support/ReflectionHelperTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/support/ReflectionHelperTests.java @@ -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.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 Sam Brannen */ class ReflectionHelperTests extends AbstractExpressionTests { + private final StandardTypeConverter tc = new StandardTypeConverter(); + + @Test void utilities() throws ParseException { SpelExpression expr = (SpelExpression)parser.parseExpression("3+4+5+6+7-2"); @@ -100,44 +107,38 @@ class ReflectionHelperTests extends AbstractExpressionTests { @Test void reflectionHelperCompareArguments_ExactMatching() { - StandardTypeConverter tc = new StandardTypeConverter(); - // 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 - 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 void reflectionHelperCompareArguments_CloseMatching() { - StandardTypeConverter tc = new StandardTypeConverter(); - // 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 - 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 - 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 - void reflectionHelperCompareArguments_RequiresConversionMatching() { - StandardTypeConverter tc = new StandardTypeConverter(); - + void reflectionHelperCompareArguments_CloseMatching_WithAutoBoxing() { // 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 - 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 - 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 - // 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 @@ -149,59 +150,56 @@ class ReflectionHelperTests extends AbstractExpressionTests { } @Test - void reflectionHelperCompareArguments_Varargs_ExactMatching() { - StandardTypeConverter tc = new StandardTypeConverter(); - + void reflectionHelperCompareArguments_Varargs() { // 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 - 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 - 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 - 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 - 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 - 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 - 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 - 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) - 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 - 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 - 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[]) ? } @Test void convertArguments() throws Exception { - StandardTypeConverter tc = new StandardTypeConverter(); Method oneArg = TestInterface.class.getMethod("oneArg", String.class); Method twoArg = TestInterface.class.getMethod("twoArg", String.class, String[].class); @@ -227,8 +225,7 @@ class ReflectionHelperTests extends AbstractExpressionTests { } @Test - void convertArguments2() throws Exception { - StandardTypeConverter tc = new StandardTypeConverter(); + void convertAllArguments() throws Exception { Method oneArg = TestInterface.class.getMethod("oneArg", String.class); Method twoArg = TestInterface.class.getMethod("twoArg", String.class, String[].class); @@ -254,7 +251,7 @@ class ReflectionHelperTests extends AbstractExpressionTests { } @Test - void setupArguments() { + void setupArgumentsForVarargsInvocation() { Object[] newArray = ReflectionHelper.setupArgumentsForVarargsInvocation( 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(); } - if (expectedMatchKind == ArgumentsMatchKind.EXACT) { + if (expectedMatchKind == EXACT) { assertThat(matchInfo.isExactMatch()).isTrue(); } - else if (expectedMatchKind == ArgumentsMatchKind.CLOSE) { + else if (expectedMatchKind == CLOSE) { 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(); } } /** - * 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) { ReflectionHelper.ArgumentsMatchInfo matchInfo = @@ -438,14 +435,10 @@ class ReflectionHelperTests extends AbstractExpressionTests { private static void checkArguments(Object[] args, Object... expected) { assertThat(args).hasSize(expected.length); 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 typeDescriptors(Class... types) { return Arrays.stream(types).map(TypeDescriptor::valueOf).toList(); }