Skip to content

Commit 79c7bfd

Browse files
committed
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-33321 (cherry picked from commit e74406a)
1 parent f78b09f commit 79c7bfd

File tree

2 files changed

+87
-18
lines changed

2 files changed

+87
-18
lines changed

spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java

+34-18
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,9 @@
6464
* read-only access to properties via {@link DataBindingPropertyAccessor}. Similarly,
6565
* {@link SimpleEvaluationContext#forReadWriteDataBinding()} enables read and write access
6666
* to properties. Alternatively, configure custom accessors via
67-
* {@link SimpleEvaluationContext#forPropertyAccessors} and potentially activate method
68-
* resolution and/or a type converter through the builder.
67+
* {@link SimpleEvaluationContext#forPropertyAccessors}, potentially
68+
* {@linkplain Builder#withAssignmentDisabled() disable assignment}, and optionally
69+
* activate method resolution and/or a type converter through the builder.
6970
*
7071
* <p>Note that {@code SimpleEvaluationContext} is typically not configured
7172
* with a default root object. Instead it is meant to be created once and
@@ -234,9 +235,8 @@ public Object lookupVariable(String name) {
234235
* ({@code ++}), and decrement ({@code --}) operators are disabled.
235236
* @return {@code true} if assignment is enabled; {@code false} otherwise
236237
* @since 5.3.38
237-
* @see #forPropertyAccessors(PropertyAccessor...)
238238
* @see #forReadOnlyDataBinding()
239-
* @see #forReadWriteDataBinding()
239+
* @see Builder#withAssignmentDisabled()
240240
*/
241241
@Override
242242
public boolean isAssignmentEnabled() {
@@ -245,15 +245,18 @@ public boolean isAssignmentEnabled() {
245245

246246
/**
247247
* Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor}
248-
* delegates: typically a custom {@code PropertyAccessor} specific to a use case
249-
* (e.g. attribute resolution in a custom data structure), potentially combined with
250-
* a {@link DataBindingPropertyAccessor} if property dereferences are needed as well.
251-
* <p>Assignment is enabled within expressions evaluated by the context created via
252-
* this factory method.
248+
* delegates: typically a custom {@code PropertyAccessor} specific to a use case &mdash;
249+
* for example, for attribute resolution in a custom data structure &mdash; potentially
250+
* combined with a {@link DataBindingPropertyAccessor} if property dereferences are
251+
* needed as well.
252+
* <p>By default, assignment is enabled within expressions evaluated by the context
253+
* created via this factory method; however, assignment can be disabled via
254+
* {@link Builder#withAssignmentDisabled()}.
253255
* @param accessors the accessor delegates to use
254256
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
255257
* @see DataBindingPropertyAccessor#forReadWriteAccess()
256258
* @see #isAssignmentEnabled()
259+
* @see Builder#withAssignmentDisabled()
257260
*/
258261
public static Builder forPropertyAccessors(PropertyAccessor... accessors) {
259262
for (PropertyAccessor accessor : accessors) {
@@ -262,7 +265,7 @@ public static Builder forPropertyAccessors(PropertyAccessor... accessors) {
262265
"ReflectivePropertyAccessor. Consider using DataBindingPropertyAccessor or a custom subclass.");
263266
}
264267
}
265-
return new Builder(true, accessors);
268+
return new Builder(accessors);
266269
}
267270

268271
/**
@@ -273,22 +276,26 @@ public static Builder forPropertyAccessors(PropertyAccessor... accessors) {
273276
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
274277
* @see #forPropertyAccessors
275278
* @see #isAssignmentEnabled()
279+
* @see Builder#withAssignmentDisabled()
276280
*/
277281
public static Builder forReadOnlyDataBinding() {
278-
return new Builder(false, DataBindingPropertyAccessor.forReadOnlyAccess());
282+
return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess()).withAssignmentDisabled();
279283
}
280284

281285
/**
282286
* Create a {@code SimpleEvaluationContext} for read-write access to
283287
* public properties via {@link DataBindingPropertyAccessor}.
284-
* <p>Assignment is enabled within expressions evaluated by the context created via
285-
* this factory method.
288+
* <p>By default, assignment is enabled within expressions evaluated by the context
289+
* created via this factory method. Assignment can be disabled via
290+
* {@link Builder#withAssignmentDisabled()}; however, it is preferable to use
291+
* {@link #forReadOnlyDataBinding()} if you desire read-only access.
286292
* @see DataBindingPropertyAccessor#forReadWriteAccess()
287293
* @see #forPropertyAccessors
288294
* @see #isAssignmentEnabled()
295+
* @see Builder#withAssignmentDisabled()
289296
*/
290297
public static Builder forReadWriteDataBinding() {
291-
return new Builder(true, DataBindingPropertyAccessor.forReadWriteAccess());
298+
return new Builder(DataBindingPropertyAccessor.forReadWriteAccess());
292299
}
293300

294301

@@ -307,15 +314,24 @@ public static final class Builder {
307314
@Nullable
308315
private TypedValue rootObject;
309316

310-
private final boolean assignmentEnabled;
317+
private boolean assignmentEnabled = true;
311318

312319

313-
private Builder(boolean assignmentEnabled, PropertyAccessor... accessors) {
314-
this.assignmentEnabled = assignmentEnabled;
320+
private Builder(PropertyAccessor... accessors) {
315321
this.accessors = Arrays.asList(accessors);
316322
}
317323

318324

325+
/**
326+
* Disable assignment within expressions evaluated by this evaluation context.
327+
* @since 5.3.38
328+
* @see SimpleEvaluationContext#isAssignmentEnabled()
329+
*/
330+
public Builder withAssignmentDisabled() {
331+
this.assignmentEnabled = false;
332+
return this;
333+
}
334+
319335
/**
320336
* Register the specified {@link MethodResolver} delegates for
321337
* a combination of property access and method resolution.
@@ -347,7 +363,6 @@ public Builder withInstanceMethods() {
347363
return this;
348364
}
349365

350-
351366
/**
352367
* Register a custom {@link ConversionService}.
353368
* <p>By default a {@link StandardTypeConverter} backed by a
@@ -359,6 +374,7 @@ public Builder withConversionService(ConversionService conversionService) {
359374
this.typeConverter = new StandardTypeConverter(conversionService);
360375
return this;
361376
}
377+
362378
/**
363379
* Register a custom {@link TypeConverter}.
364380
* <p>By default a {@link StandardTypeConverter} backed by a

spring-expression/src/test/java/org/springframework/expression/spel/support/SimpleEvaluationContextTests.java

+53
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,59 @@ void forPropertyAccessorsInMixedReadOnlyMode() {
187187
assertIncrementAndDecrementWritesForIndexedStructures(context);
188188
}
189189

190+
@Test
191+
void forPropertyAccessorsWithAssignmentDisabled() {
192+
SimpleEvaluationContext context = SimpleEvaluationContext
193+
.forPropertyAccessors(new CompilableMapAccessor(), DataBindingPropertyAccessor.forReadOnlyAccess())
194+
.withAssignmentDisabled()
195+
.build();
196+
197+
assertCommonReadOnlyModeBehavior(context);
198+
199+
// Map -- with key as property name supported by CompilableMapAccessor
200+
201+
Expression expression;
202+
expression = parser.parseExpression("map.yellow");
203+
// setValue() is supported even though assignment is not.
204+
expression.setValue(context, model, "pineapple");
205+
assertThat(expression.getValue(context, model, String.class)).isEqualTo("pineapple");
206+
207+
// WRITE -- via assignment operator
208+
209+
// Variable
210+
assertAssignmentDisabled(context, "#myVar = 'rejected'");
211+
212+
// Property
213+
assertAssignmentDisabled(context, "name = 'rejected'");
214+
assertAssignmentDisabled(context, "map.yellow = 'rejected'");
215+
assertIncrementDisabled(context, "count++");
216+
assertIncrementDisabled(context, "++count");
217+
assertDecrementDisabled(context, "count--");
218+
assertDecrementDisabled(context, "--count");
219+
220+
// Array Index
221+
assertAssignmentDisabled(context, "array[0] = 'rejected'");
222+
assertIncrementDisabled(context, "numbers[0]++");
223+
assertIncrementDisabled(context, "++numbers[0]");
224+
assertDecrementDisabled(context, "numbers[0]--");
225+
assertDecrementDisabled(context, "--numbers[0]");
226+
227+
// List Index
228+
assertAssignmentDisabled(context, "list[0] = 'rejected'");
229+
230+
// Map Index -- key as String
231+
assertAssignmentDisabled(context, "map['red'] = 'rejected'");
232+
233+
// Map Index -- key as pseudo property name
234+
assertAssignmentDisabled(context, "map[yellow] = 'rejected'");
235+
236+
// String Index
237+
assertAssignmentDisabled(context, "name[0] = 'rejected'");
238+
239+
// Object Index
240+
assertAssignmentDisabled(context, "['name'] = 'rejected'");
241+
}
242+
190243

191244
private void assertReadWriteMode(SimpleEvaluationContext context) {
192245
// Variables can always be set programmatically within an EvaluationContext.

0 commit comments

Comments
 (0)