From f9c3d00af07e3b5e634f9161f86a2daacc64d682 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:26:34 +0300 Subject: [PATCH] Introduce withAssignmentDisabled() option for SimpleEvaluationContext To support additional use cases, this commit introduces a withAssignmentDisabled() method in the Builder for SimpleEvaluationContext. See gh-33319 Closes gh-33320 (cherry picked from commit 79c7bfdbadb73bdabf5412d477454bdeddb67aa5) --- .../spel/support/SimpleEvaluationContext.java | 50 +++++++++++------ .../support/SimpleEvaluationContextTests.java | 53 +++++++++++++++++++ 2 files changed, 86 insertions(+), 17 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java index 9ac59f3b8b6b..f10dfa3718d0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java @@ -64,8 +64,9 @@ * read-only access to properties via {@link DataBindingPropertyAccessor}. Similarly, * {@link SimpleEvaluationContext#forReadWriteDataBinding()} enables read and write access * to properties. Alternatively, configure custom accessors via - * {@link SimpleEvaluationContext#forPropertyAccessors} and potentially activate method - * resolution and/or a type converter through the builder. + * {@link SimpleEvaluationContext#forPropertyAccessors}, potentially + * {@linkplain Builder#withAssignmentDisabled() disable assignment}, and optionally + * activate method resolution and/or a type converter through the builder. * *

Note that {@code SimpleEvaluationContext} is typically not configured * with a default root object. Instead it is meant to be created once and @@ -234,9 +235,8 @@ public Object lookupVariable(String name) { * ({@code ++}), and decrement ({@code --}) operators are disabled. * @return {@code true} if assignment is enabled; {@code false} otherwise * @since 5.3.38 - * @see #forPropertyAccessors(PropertyAccessor...) * @see #forReadOnlyDataBinding() - * @see #forReadWriteDataBinding() + * @see Builder#withAssignmentDisabled() */ @Override public boolean isAssignmentEnabled() { @@ -245,15 +245,18 @@ public boolean isAssignmentEnabled() { /** * Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor} - * delegates: typically a custom {@code PropertyAccessor} specific to a use case - * (e.g. attribute resolution in a custom data structure), potentially combined with - * a {@link DataBindingPropertyAccessor} if property dereferences are needed as well. - *

Assignment is enabled within expressions evaluated by the context created via - * this factory method. + * delegates: typically a custom {@code PropertyAccessor} specific to a use case — + * for example, for attribute resolution in a custom data structure — potentially + * combined with a {@link DataBindingPropertyAccessor} if property dereferences are + * needed as well. + *

By default, assignment is enabled within expressions evaluated by the context + * created via this factory method; however, assignment can be disabled via + * {@link Builder#withAssignmentDisabled()}. * @param accessors the accessor delegates to use * @see DataBindingPropertyAccessor#forReadOnlyAccess() * @see DataBindingPropertyAccessor#forReadWriteAccess() * @see #isAssignmentEnabled() + * @see Builder#withAssignmentDisabled() */ public static Builder forPropertyAccessors(PropertyAccessor... accessors) { for (PropertyAccessor accessor : accessors) { @@ -262,7 +265,7 @@ public static Builder forPropertyAccessors(PropertyAccessor... accessors) { "ReflectivePropertyAccessor. Consider using DataBindingPropertyAccessor or a custom subclass."); } } - return new Builder(true, accessors); + return new Builder(accessors); } /** @@ -273,22 +276,26 @@ public static Builder forPropertyAccessors(PropertyAccessor... accessors) { * @see DataBindingPropertyAccessor#forReadOnlyAccess() * @see #forPropertyAccessors * @see #isAssignmentEnabled() + * @see Builder#withAssignmentDisabled() */ public static Builder forReadOnlyDataBinding() { - return new Builder(false, DataBindingPropertyAccessor.forReadOnlyAccess()); + return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess()).withAssignmentDisabled(); } /** * Create a {@code SimpleEvaluationContext} for read-write access to * public properties via {@link DataBindingPropertyAccessor}. - *

Assignment is enabled within expressions evaluated by the context created via - * this factory method. + *

By default, assignment is enabled within expressions evaluated by the context + * created via this factory method. Assignment can be disabled via + * {@link Builder#withAssignmentDisabled()}; however, it is preferable to use + * {@link #forReadOnlyDataBinding()} if you desire read-only access. * @see DataBindingPropertyAccessor#forReadWriteAccess() * @see #forPropertyAccessors * @see #isAssignmentEnabled() + * @see Builder#withAssignmentDisabled() */ public static Builder forReadWriteDataBinding() { - return new Builder(true, DataBindingPropertyAccessor.forReadWriteAccess()); + return new Builder(DataBindingPropertyAccessor.forReadWriteAccess()); } @@ -307,15 +314,24 @@ public static final class Builder { @Nullable private TypedValue rootObject; - private final boolean assignmentEnabled; + private boolean assignmentEnabled = true; - private Builder(boolean assignmentEnabled, PropertyAccessor... accessors) { - this.assignmentEnabled = assignmentEnabled; + private Builder(PropertyAccessor... accessors) { this.accessors = Arrays.asList(accessors); } + /** + * Disable assignment within expressions evaluated by this evaluation context. + * @since 5.3.38 + * @see SimpleEvaluationContext#isAssignmentEnabled() + */ + public Builder withAssignmentDisabled() { + this.assignmentEnabled = false; + return this; + } + /** * Register the specified {@link MethodResolver} delegates for * a combination of property access and method resolution. diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java index 7ac2132883c3..e2106ac25a35 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java @@ -187,6 +187,59 @@ void forPropertyAccessorsInMixedReadOnlyMode() { assertIncrementAndDecrementWritesForIndexedStructures(context); } + @Test + void forPropertyAccessorsWithAssignmentDisabled() { + SimpleEvaluationContext context = SimpleEvaluationContext + .forPropertyAccessors(new CompilableMapAccessor(), DataBindingPropertyAccessor.forReadOnlyAccess()) + .withAssignmentDisabled() + .build(); + + assertCommonReadOnlyModeBehavior(context); + + // Map -- with key as property name supported by CompilableMapAccessor + + Expression expression; + expression = parser.parseExpression("map.yellow"); + // setValue() is supported even though assignment is not. + expression.setValue(context, model, "pineapple"); + assertThat(expression.getValue(context, model, String.class)).isEqualTo("pineapple"); + + // WRITE -- via assignment operator + + // Variable + assertAssignmentDisabled(context, "#myVar = 'rejected'"); + + // Property + assertAssignmentDisabled(context, "name = 'rejected'"); + assertAssignmentDisabled(context, "map.yellow = 'rejected'"); + assertIncrementDisabled(context, "count++"); + assertIncrementDisabled(context, "++count"); + assertDecrementDisabled(context, "count--"); + assertDecrementDisabled(context, "--count"); + + // Array Index + assertAssignmentDisabled(context, "array[0] = 'rejected'"); + assertIncrementDisabled(context, "numbers[0]++"); + assertIncrementDisabled(context, "++numbers[0]"); + assertDecrementDisabled(context, "numbers[0]--"); + assertDecrementDisabled(context, "--numbers[0]"); + + // List Index + assertAssignmentDisabled(context, "list[0] = 'rejected'"); + + // Map Index -- key as String + assertAssignmentDisabled(context, "map['red'] = 'rejected'"); + + // Map Index -- key as pseudo property name + assertAssignmentDisabled(context, "map[yellow] = 'rejected'"); + + // String Index + assertAssignmentDisabled(context, "name[0] = 'rejected'"); + + // Object Index + assertAssignmentDisabled(context, "['name'] = 'rejected'"); + } + private void assertReadWriteMode(SimpleEvaluationContext context) { // Variables can always be set programmatically within an EvaluationContext.