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 5f0b6f10d8..f55953f521 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 @@ import org.springframework.lang.Nullable; * 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 @@ -268,9 +269,8 @@ public final class SimpleEvaluationContext implements EvaluationContext { * ({@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() { @@ -279,15 +279,18 @@ public final class SimpleEvaluationContext implements EvaluationContext { /** * 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) { @@ -296,7 +299,7 @@ public final class SimpleEvaluationContext implements EvaluationContext { "ReflectivePropertyAccessor. Consider using DataBindingPropertyAccessor or a custom subclass."); } } - return new Builder(true, accessors); + return new Builder(accessors); } /** @@ -307,22 +310,26 @@ public final class SimpleEvaluationContext implements EvaluationContext { * @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()); } @@ -343,15 +350,24 @@ public final class SimpleEvaluationContext implements EvaluationContext { @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.propertyAccessors = 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 IndexAccessor} delegates. * @param indexAccessors the index accessors to use 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 374465246a..1730e83c2a 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 @@ -211,6 +211,52 @@ class SimpleEvaluationContextTests { assertIncrementAndDecrementWritesForIndexedStructures(context); } + @Test + void forPropertyAccessorsWithAssignmentDisabled() { + SimpleEvaluationContext context = SimpleEvaluationContext + .forPropertyAccessors(new CompilableMapAccessor(false), DataBindingPropertyAccessor.forReadOnlyAccess()) + .withIndexAccessors(colorsIndexAccessor) + .withAssignmentDisabled() + .build(); + + assertCommonReadOnlyModeBehavior(context); + + // 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.