Polishing

This commit is contained in:
Sam Brannen 2024-01-31 16:41:15 +01:00
parent 67958656e4
commit 2b52582dff
6 changed files with 72 additions and 90 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2017 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,9 +19,9 @@ package org.springframework.expression;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
* By default the mathematical operators {@link Operation} support simple types * By default, the mathematical operators defined in {@link Operation} support simple
* like numbers. By providing an implementation of OperatorOverloader, a user * types like numbers. By providing an implementation of {@code OperatorOverloader},
* of the expression language can support these operations on other types. * a user of the expression language can support these operations on other types.
* *
* @author Andy Clement * @author Andy Clement
* @since 3.0 * @since 3.0
@ -29,21 +29,21 @@ import org.springframework.lang.Nullable;
public interface OperatorOverloader { public interface OperatorOverloader {
/** /**
* Return true if the operator overloader supports the specified operation * Return {@code true} if this operator overloader supports the specified
* between the two operands and so should be invoked to handle it. * operation on the two operands and should be invoked to handle it.
* @param operation the operation to be performed * @param operation the operation to be performed
* @param leftOperand the left operand * @param leftOperand the left operand
* @param rightOperand the right operand * @param rightOperand the right operand
* @return true if the OperatorOverloader supports the specified operation * @return true if this {@code OperatorOverloader} supports the specified
* between the two operands * operation between the two operands
* @throws EvaluationException if there is a problem performing the operation * @throws EvaluationException if there is a problem performing the operation
*/ */
boolean overridesOperation(Operation operation, @Nullable Object leftOperand, @Nullable Object rightOperand) boolean overridesOperation(Operation operation, @Nullable Object leftOperand, @Nullable Object rightOperand)
throws EvaluationException; throws EvaluationException;
/** /**
* Execute the specified operation on two operands, returning a result. * Perform the specified operation on the two operands, returning a result.
* See {@link Operation} for supported operations. * <p>See {@link Operation} for supported operations.
* @param operation the operation to be performed * @param operation the operation to be performed
* @param leftOperand the left operand * @param leftOperand the left operand
* @param rightOperand the right operand * @param rightOperand the right operand

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 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.
@ -24,17 +24,19 @@ import org.springframework.expression.TypedValue;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
* Represents a template expression broken into pieces. Each piece will be an Expression * Represents a template expression broken into pieces.
* but pure text parts to the template will be represented as LiteralExpression objects. *
* An example of a template expression might be: * <p>Each piece will be an {@link Expression}, but pure text parts of the
* template will be represented as {@link LiteralExpression} objects. An example
* of a template expression might be:
* *
* <pre class="code"> * <pre class="code">
* &quot;Hello ${getName()}&quot; * &quot;Hello ${getName()}&quot;
* </pre> * </pre>
* *
* which will be represented as a CompositeStringExpression of two parts. The first part * which will be represented as a {@code CompositeStringExpression} of two parts:
* being a LiteralExpression representing 'Hello ' and the second part being a real * the first part being a {@link LiteralExpression} representing 'Hello ' and the
* expression that will call {@code getName()} when invoked. * second part being a real expression that will call {@code getName()} when invoked.
* *
* @author Andy Clement * @author Andy Clement
* @author Juergen Hoeller * @author Juergen Hoeller

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 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.
@ -24,9 +24,11 @@ import org.springframework.expression.TypedValue;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
/** /**
* A very simple hardcoded implementation of the Expression interface that represents a * A very simple, hard-coded implementation of the {@link Expression} interface
* string literal. It is used with CompositeStringExpression when representing a template * that represents a string literal.
* expression which is made up of pieces - some being real expressions to be handled by *
* <p>It is used with {@link CompositeStringExpression} when representing a template
* expression which is made up of pieces, some being real expressions to be handled by
* an EL implementation like SpEL, and some being just textual elements. * an EL implementation like SpEL, and some being just textual elements.
* *
* @author Andy Clement * @author Andy Clement

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.
@ -80,11 +80,11 @@ public abstract class TemplateAwareExpressionParser implements ExpressionParser
* result, evaluating all returned expressions and concatenating the results produces * result, evaluating all returned expressions and concatenating the results produces
* the complete evaluated string. Unwrapping is only done of the outermost delimiters * the complete evaluated string. Unwrapping is only done of the outermost delimiters
* found, so the string 'hello ${foo${abc}}' would break into the pieces 'hello ' and * found, so the string 'hello ${foo${abc}}' would break into the pieces 'hello ' and
* 'foo${abc}'. This means that expression languages that used ${..} as part of their * 'foo${abc}'. This means that expression languages that use ${..} as part of their
* functionality are supported without any problem. The parsing is aware of the * functionality are supported without any problem. The parsing is aware of the
* structure of an embedded expression. It assumes that parentheses '(', square * structure of an embedded expression. It assumes that parentheses '(', square
* brackets '[' and curly brackets '}' must be in pairs within the expression unless * brackets '[', and curly brackets '}' must be in pairs within the expression unless
* they are within a string literal and a string literal starts and terminates with a * they are within a string literal and the string literal starts and terminates with a
* single quote '. * single quote '.
* @param expressionString the expression string * @param expressionString the expression string
* @return the parsed expressions * @return the parsed expressions
@ -238,48 +238,33 @@ public abstract class TemplateAwareExpressionParser implements ExpressionParser
/** /**
* This captures a type of bracket and the position in which it occurs in the * This captures a type of bracket and the position in which it occurs in the
* expression. The positional information is used if an error has to be reported * expression. The positional information is used if an error has to be reported
* because the related end bracket cannot be found. Bracket is used to describe: * because the related end bracket cannot be found. Bracket is used to describe
* square brackets [] round brackets () and curly brackets {} * square brackets [], round brackets (), and curly brackets {}.
*/ */
private static class Bracket { private record Bracket(char bracket, int pos) {
char bracket;
int pos;
Bracket(char bracket, int pos) {
this.bracket = bracket;
this.pos = pos;
}
boolean compatibleWithCloseBracket(char closeBracket) { boolean compatibleWithCloseBracket(char closeBracket) {
if (this.bracket == '{') { return switch (this.bracket) {
return closeBracket == '}'; case '{' -> closeBracket == '}';
} case '[' -> closeBracket == ']';
else if (this.bracket == '[') { default -> closeBracket == ')';
return closeBracket == ']'; };
}
return closeBracket == ')';
} }
static char theOpenBracketFor(char closeBracket) { static char theOpenBracketFor(char closeBracket) {
if (closeBracket == '}') { return switch (closeBracket) {
return '{'; case '}' -> '{';
} case ']' -> '[';
else if (closeBracket == ']') { default -> '(';
return '['; };
}
return '(';
} }
static char theCloseBracketFor(char openBracket) { static char theCloseBracketFor(char openBracket) {
if (openBracket == '{') { return switch (openBracket) {
return '}'; case '{' -> '}';
} case '[' -> ']';
else if (openBracket == '[') { default -> ')';
return ']'; };
}
return ')';
} }
} }

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.
@ -22,7 +22,6 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Operation; import org.springframework.expression.Operation;
import org.springframework.expression.TypedValue; import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext;
@ -223,7 +222,7 @@ class ExpressionStateTests extends AbstractExpressionTests {
} }
@Test @Test
void typeLocator() throws EvaluationException { void typeLocator() {
assertThat(state.getEvaluationContext().getTypeLocator()).isNotNull(); assertThat(state.getEvaluationContext().getTypeLocator()).isNotNull();
assertThat(state.findType("java.lang.Integer")).isEqualTo(Integer.class); assertThat(state.findType("java.lang.Integer")).isEqualTo(Integer.class);
assertThatExceptionOfType(SpelEvaluationException.class) assertThatExceptionOfType(SpelEvaluationException.class)
@ -232,7 +231,7 @@ class ExpressionStateTests extends AbstractExpressionTests {
} }
@Test @Test
void typeConversion() throws EvaluationException { void typeConversion() {
String s = (String) state.convertValue(34, TypeDescriptor.valueOf(String.class)); String s = (String) state.convertValue(34, TypeDescriptor.valueOf(String.class));
assertThat(s).isEqualTo("34"); assertThat(s).isEqualTo("34");

View File

@ -18,56 +18,50 @@ package org.springframework.expression.spel;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Operation; import org.springframework.expression.Operation;
import org.springframework.expression.OperatorOverloader; import org.springframework.expression.OperatorOverloader;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import static org.assertj.core.api.Assertions.assertThat;
/** /**
* Test providing operator support * Tests for custom {@link OperatorOverloader} support.
* *
* @author Andy Clement * @author Andy Clement
* @author Sam Brannen
*/ */
class OperatorOverloaderTests extends AbstractExpressionTests { class OperatorOverloaderTests extends AbstractExpressionTests {
@Test @Test
void testSimpleOperations() { void simpleOperations() {
// no built-in support for this: // default behavior
evaluateAndCheckError("'abc'-true",SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES); evaluate("'abc' + true", "abctrue", String.class);
evaluate("'abc' + null", "abcnull", String.class);
StandardEvaluationContext eContext = TestScenarioCreator.getTestEvaluationContext(); // no built-in support for <string> - <boolean>
eContext.setOperatorOverloader(new StringAndBooleanAddition()); evaluateAndCheckError("'abc' - true", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
SpelExpression expr = (SpelExpression)parser.parseExpression("'abc'+true"); super.context.setOperatorOverloader(new StringAndBooleanOperatorOverloader());
assertThat(expr.getValue(eContext)).isEqualTo("abctrue");
expr = (SpelExpression)parser.parseExpression("'abc'-true"); // unaffected
assertThat(expr.getValue(eContext)).isEqualTo("abc"); evaluate("'abc' + true", "abctrue", String.class);
evaluate("'abc' + null", "abcnull", String.class);
expr = (SpelExpression)parser.parseExpression("'abc'+null"); // <string> - <boolean> has been overloaded
assertThat(expr.getValue(eContext)).isEqualTo("abcnull"); evaluate("'abc' - true", "abcTRUE", String.class);
} }
static class StringAndBooleanAddition implements OperatorOverloader { private static class StringAndBooleanOperatorOverloader implements OperatorOverloader {
@Override @Override
public Object operate(Operation operation, Object leftOperand, Object rightOperand) throws EvaluationException { public boolean overridesOperation(Operation operation, Object leftOperand, Object rightOperand) {
if (operation==Operation.ADD) { return (leftOperand instanceof String && rightOperand instanceof Boolean);
return leftOperand + ((Boolean) rightOperand).toString();
}
else {
return leftOperand;
}
} }
@Override @Override
public boolean overridesOperation(Operation operation, Object leftOperand, Object rightOperand) throws EvaluationException { public Object operate(Operation operation, Object leftOperand, Object rightOperand) {
return leftOperand instanceof String && rightOperand instanceof Boolean; if (operation == Operation.SUBTRACT) {
return leftOperand + ((Boolean) rightOperand).toString().toUpperCase();
}
throw new UnsupportedOperationException(operation.name());
} }
} }