Skip to content

Commit 22bfe7d

Browse files
committed
Introduce proper error handling for IndexAccessor support in SpEL
See gh-26409 See gh-26478
1 parent 5c6b82a commit 22bfe7d

File tree

3 files changed

+140
-10
lines changed

3 files changed

+140
-10
lines changed

spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java

+10-1
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,16 @@ public enum SpelMessage {
291291

292292
/** @since 6.0.13 */
293293
NEGATIVE_REPEATED_TEXT_COUNT(Kind.ERROR, 1081,
294-
"Repeat count ''{0}'' must not be negative");
294+
"Repeat count ''{0}'' must not be negative"),
295+
296+
/** @since 6.2 */
297+
EXCEPTION_DURING_INDEX_READ(Kind.ERROR, 1082,
298+
"A problem occurred while attempting to read index ''{0}'' in ''{1}''"),
299+
300+
/** @since 6.2 */
301+
EXCEPTION_DURING_INDEX_WRITE(Kind.ERROR, 1083,
302+
"A problem occurred while attempting to write index ''{0}'' in ''{1}''");
303+
295304

296305

297306
private final Kind kind;

spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java

+11-8
Original file line numberDiff line numberDiff line change
@@ -259,8 +259,9 @@ else if (target instanceof Collection<?> collection) {
259259
}
260260
}
261261
catch (Exception ex) {
262-
// TODO throw SpelEvaluationException for "exception during index access",
263-
// analogous to SpelMessage.EXCEPTION_DURING_PROPERTY_READ.
262+
throw new SpelEvaluationException(
263+
getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_INDEX_READ, index,
264+
target.getClass().getTypeName());
264265
}
265266

266267
throw new SpelEvaluationException(
@@ -904,9 +905,10 @@ public TypedValue getValue() {
904905
try {
905906
return this.indexAccessor.read(this.evaluationContext, this.target, this.index);
906907
}
907-
catch (AccessException ex) {
908-
throw new SpelEvaluationException(getStartPosition(), ex,
909-
SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.typeDescriptor.toString());
908+
catch (Exception ex) {
909+
throw new SpelEvaluationException(
910+
getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_INDEX_READ, this.index,
911+
this.typeDescriptor.toString());
910912
}
911913
}
912914

@@ -915,9 +917,10 @@ public void setValue(@Nullable Object newValue) {
915917
try {
916918
this.indexAccessor.write(this.evaluationContext, this.target, this.index, newValue);
917919
}
918-
catch (AccessException ex) {
919-
throw new SpelEvaluationException(getStartPosition(), ex,
920-
SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE, this.typeDescriptor.toString());
920+
catch (Exception ex) {
921+
throw new SpelEvaluationException(
922+
getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_INDEX_WRITE, this.index,
923+
this.typeDescriptor.toString());
921924
}
922925
}
923926

spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java

+119-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import com.fasterxml.jackson.databind.node.ArrayNode;
3434
import com.fasterxml.jackson.databind.node.NullNode;
3535
import com.fasterxml.jackson.databind.node.TextNode;
36+
import org.junit.jupiter.api.Disabled;
3637
import org.junit.jupiter.api.Nested;
3738
import org.junit.jupiter.api.Test;
3839

@@ -48,6 +49,15 @@
4849

4950
import static org.assertj.core.api.Assertions.assertThat;
5051
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
52+
import static org.mockito.ArgumentMatchers.any;
53+
import static org.mockito.ArgumentMatchers.eq;
54+
import static org.mockito.BDDMockito.doThrow;
55+
import static org.mockito.BDDMockito.given;
56+
import static org.mockito.Mockito.mock;
57+
import static org.mockito.Mockito.times;
58+
import static org.mockito.Mockito.verify;
59+
import static org.springframework.expression.spel.SpelMessage.EXCEPTION_DURING_INDEX_READ;
60+
import static org.springframework.expression.spel.SpelMessage.EXCEPTION_DURING_INDEX_WRITE;
5161
import static org.springframework.expression.spel.SpelMessage.INDEXING_NOT_SUPPORTED_FOR_TYPE;
5262
import static org.springframework.expression.spel.SpelMessage.UNABLE_TO_GROW_COLLECTION_UNKNOWN_ELEMENT_TYPE;
5363

@@ -485,7 +495,115 @@ void addingAndRemovingIndexAccessors() {
485495
}
486496

487497
@Test
488-
void indexReadWrite() {
498+
void noSuitableIndexAccessorResultsInException() {
499+
StandardEvaluationContext context = new StandardEvaluationContext();
500+
assertThat(context.getIndexAccessors()).isEmpty();
501+
502+
SpelExpressionParser parser = new SpelExpressionParser();
503+
Expression expr = parser.parseExpression("[0]");
504+
assertThatExceptionOfType(SpelEvaluationException.class)
505+
.isThrownBy(() -> expr.getValue(context, this))
506+
.withMessageEndingWith("Indexing into type '%s' is not supported", getClass().getName())
507+
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(INDEXING_NOT_SUPPORTED_FOR_TYPE);
508+
}
509+
510+
@Test
511+
void canReadThrowsException() throws Exception {
512+
StandardEvaluationContext context = new StandardEvaluationContext();
513+
RuntimeException exception = new RuntimeException("Boom!");
514+
515+
IndexAccessor mock = mock();
516+
given(mock.canRead(any(), eq(this), any())).willThrow(exception);
517+
context.addIndexAccessor(mock);
518+
519+
SpelExpressionParser parser = new SpelExpressionParser();
520+
Expression expr = parser.parseExpression("[0]");
521+
assertThatExceptionOfType(SpelEvaluationException.class)
522+
.isThrownBy(() -> expr.getValue(context, this))
523+
.withMessageEndingWith("A problem occurred while attempting to read index '%d' in '%s'",
524+
0, getClass().getName())
525+
.withCause(exception)
526+
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(EXCEPTION_DURING_INDEX_READ);
527+
528+
verify(mock, times(1)).canRead(any(), any(), any());
529+
}
530+
531+
@Test
532+
void readThrowsException() throws Exception {
533+
StandardEvaluationContext context = new StandardEvaluationContext();
534+
RuntimeException exception = new RuntimeException("Boom!");
535+
536+
IndexAccessor mock = mock();
537+
given(mock.canRead(any(), eq(this), any())).willReturn(true);
538+
given(mock.read(any(), eq(this), any())).willThrow(exception);
539+
context.addIndexAccessor(mock);
540+
541+
SpelExpressionParser parser = new SpelExpressionParser();
542+
Expression expr = parser.parseExpression("[0]");
543+
assertThatExceptionOfType(SpelEvaluationException.class)
544+
.isThrownBy(() -> expr.getValue(context, this))
545+
.withMessageEndingWith("A problem occurred while attempting to read index '%d' in '%s'",
546+
0, getClass().getName())
547+
.withCause(exception)
548+
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(EXCEPTION_DURING_INDEX_READ);
549+
550+
verify(mock, times(1)).canRead(any(), any(), any());
551+
verify(mock, times(1)).read(any(), any(), any());
552+
}
553+
554+
@Disabled("Disabled until IndexAccessor#canWrite is honored")
555+
@Test
556+
void canWriteThrowsException() throws Exception {
557+
StandardEvaluationContext context = new StandardEvaluationContext();
558+
RuntimeException exception = new RuntimeException("Boom!");
559+
560+
IndexAccessor mock = mock();
561+
// TODO canRead() should not be invoked for this use case.
562+
given(mock.canRead(any(), eq(this), any())).willReturn(true);
563+
// TODO canWrite() should be invoked for this use case, but it is currently not.
564+
given(mock.canWrite(eq(context), eq(this), eq(0))).willThrow(exception);
565+
context.addIndexAccessor(mock);
566+
567+
SpelExpressionParser parser = new SpelExpressionParser();
568+
Expression expr = parser.parseExpression("[0]");
569+
assertThatExceptionOfType(SpelEvaluationException.class)
570+
.isThrownBy(() -> expr.setValue(context, this, 999))
571+
.withMessageEndingWith("A problem occurred while attempting to write index '%d' in '%s'",
572+
0, getClass().getName())
573+
.withCause(exception)
574+
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(EXCEPTION_DURING_INDEX_WRITE);
575+
576+
verify(mock, times(1)).canWrite(any(), any(), any());
577+
}
578+
579+
@Test
580+
void writeThrowsException() throws Exception {
581+
StandardEvaluationContext context = new StandardEvaluationContext();
582+
RuntimeException exception = new RuntimeException("Boom!");
583+
584+
IndexAccessor mock = mock();
585+
// TODO canRead() should not be invoked for this use case.
586+
given(mock.canRead(any(), eq(this), any())).willReturn(true);
587+
given(mock.canWrite(eq(context), eq(this), eq(0))).willReturn(true);
588+
doThrow(exception).when(mock).write(any(), any(), any(), any());
589+
context.addIndexAccessor(mock);
590+
591+
SpelExpressionParser parser = new SpelExpressionParser();
592+
Expression expr = parser.parseExpression("[0]");
593+
assertThatExceptionOfType(SpelEvaluationException.class)
594+
.isThrownBy(() -> expr.setValue(context, this, 999))
595+
.withMessageEndingWith("A problem occurred while attempting to write index '%d' in '%s'",
596+
0, getClass().getName())
597+
.withCause(exception)
598+
.extracting(SpelEvaluationException::getMessageCode).isEqualTo(EXCEPTION_DURING_INDEX_WRITE);
599+
600+
// TODO canWrite() should be invoked for this use case, but it is currently not.
601+
// verify(mock, times(1)).canWrite(any(), any(), any());
602+
verify(mock, times(1)).write(any(), any(), any(), any());
603+
}
604+
605+
@Test
606+
void readAndWriteIndex() {
489607
StandardEvaluationContext context = new StandardEvaluationContext();
490608

491609
ObjectMapper objectMapper = new ObjectMapper();

0 commit comments

Comments
 (0)