diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java index 8a540a908dc6..593e5164b0ec 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/CompoundExpression.java @@ -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"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; -import org.springframework.expression.spel.SpelNode; /** * Represents a DOT separated expression sequence, such as @@ -120,14 +119,13 @@ public String toStringAST() { for (int i = 0; i < getChildCount(); i++) { sb.append(getChild(i).toStringAST()); if (i < getChildCount() - 1) { - SpelNode nextChild = getChild(i + 1); + SpelNodeImpl nextChild = this.children[i + 1]; + if (nextChild.isNullSafe()) { + sb.append("?."); + } // Don't append a '.' if the next child is an Indexer. // For example, we want 'myVar[0]' instead of 'myVar.[0]'. - if (!(nextChild instanceof Indexer)) { - if ((nextChild instanceof MethodReference methodRef && methodRef.isNullSafe()) || - (nextChild instanceof PropertyOrFieldReference pofRef && pofRef.isNullSafe())) { - sb.append('?'); - } + else if (!(nextChild instanceof Indexer)) { sb.append('.'); } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java index 00be16e8556a..5006a03a4937 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java @@ -77,6 +77,7 @@ public MethodReference(boolean nullSafe, String methodName, int startPos, int en * Does this node represent a null-safe method reference? * @since 6.0.13 */ + @Override public final boolean isNullSafe() { return this.nullSafe; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java index 1460734a6ef8..84acd5ef05a4 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java @@ -76,6 +76,7 @@ public PropertyOrFieldReference(boolean nullSafe, String propertyOrFieldName, in /** * Does this node represent a null-safe property or field reference? */ + @Override public boolean isNullSafe() { return this.nullSafe; } @@ -181,7 +182,7 @@ private TypedValue readProperty(TypedValue contextObject, EvaluationContext eval throws EvaluationException { Object targetObject = contextObject.getValue(); - if (targetObject == null && this.nullSafe) { + if (targetObject == null && isNullSafe()) { return TypedValue.NULL; } @@ -233,7 +234,7 @@ private void writeProperty( TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue) throws EvaluationException { - if (contextObject.getValue() == null && this.nullSafe) { + if (contextObject.getValue() == null && isNullSafe()) { return; } if (contextObject.getValue() == null) { @@ -353,7 +354,7 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { } Label skipIfNull = null; - if (this.nullSafe) { + if (isNullSafe()) { mv.visitInsn(DUP); skipIfNull = new Label(); Label continueLabel = new Label(); @@ -381,7 +382,7 @@ void setExitTypeDescriptor(String descriptor) { // If this property or field access would return a primitive - and yet // it is also marked null safe - then the exit type descriptor must be // promoted to the box type to allow a null value to be passed on - if (this.nullSafe && CodeFlow.isPrimitive(descriptor)) { + if (isNullSafe() && CodeFlow.isPrimitive(descriptor)) { this.originalPrimitiveExitTypeDescriptor = descriptor; this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java index f6cbaa6c97c7..3fb9208bc1b8 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java @@ -181,6 +181,16 @@ public int getEndPosition() { return this.endPos; } + /** + * Determine if this node is the target of a null-safe navigation operation. + *

The default implementation returns {@code false}. + * @return {@code true} if this node is the target of a null-safe operation + * @since 6.1.6 + */ + public boolean isNullSafe() { + return false; + } + /** * Check whether a node can be compiled to bytecode. The reasoning in each node may * be different but will typically involve checking whether the exit type descriptor