From 7879bdfc1d99fa675902783c1cd5bb08ede33855 Mon Sep 17 00:00:00 2001 From: Andy Clement Date: Mon, 6 Feb 2017 18:43:10 +0100 Subject: [PATCH] Fix compilation of SpEL elvis/ternary expressions Without this fix the compiled version of elvis actual behaved differently to the interpreted version if the value being queried was an empty string. This is now fixed. It also now correctly handles the query value being a primitive and addresses the findings of SPR-15192 where some type inferencing logic was trying to be too clever, that code has been deleted. Issue: SPR-15192 (cherry picked from commit d41d28f) --- .../expression/spel/ast/Elvis.java | 19 +- .../expression/spel/ast/Ternary.java | 10 +- .../spel/SpelCompilationCoverageTests.java | 190 +++++++++++++++++- 3 files changed, 200 insertions(+), 19 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java index 77f342ec17..bcf2aea70f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2017 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. @@ -35,7 +35,7 @@ import org.springframework.util.StringUtils; public class Elvis extends SpelNodeImpl { public Elvis(int pos, SpelNodeImpl... args) { - super(pos,args); + super(pos, args); } @@ -49,6 +49,7 @@ public class Elvis extends SpelNodeImpl { @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { TypedValue value = this.children[0].getValueInternal(state); + // If this check is changed, the generateCode method will need changing too if (!StringUtils.isEmpty(value.getValue())) { return value; } @@ -77,11 +78,17 @@ public class Elvis extends SpelNodeImpl { // exit type descriptor can be null if both components are literal expressions computeExitTypeDescriptor(); this.children[0].generateCode(mv, cf); + CodeFlow.insertBoxIfNecessary(mv, cf.lastDescriptor().charAt(0)); Label elseTarget = new Label(); Label endOfIf = new Label(); mv.visitInsn(DUP); mv.visitJumpInsn(IFNULL, elseTarget); - mv.visitJumpInsn(GOTO, endOfIf); + // Also check if empty string, as per the code in the interpreted version + mv.visitInsn(DUP); + mv.visitLdcInsn(""); + mv.visitInsn(SWAP); + mv.visitMethodInsn(INVOKEVIRTUAL, "java/lang/String", "equals", "(Ljava/lang/Object;)Z",false); + mv.visitJumpInsn(IFEQ, endOfIf); // if not empty, drop through to elseTarget mv.visitLabel(elseTarget); mv.visitInsn(POP); this.children[1].generateCode(mv, cf); @@ -100,12 +107,6 @@ public class Elvis extends SpelNodeImpl { if (conditionDescriptor.equals(ifNullValueDescriptor)) { this.exitTypeDescriptor = conditionDescriptor; } - else if (conditionDescriptor.equals("Ljava/lang/Object") && !CodeFlow.isPrimitive(ifNullValueDescriptor)) { - this.exitTypeDescriptor = ifNullValueDescriptor; - } - else if (ifNullValueDescriptor.equals("Ljava/lang/Object") && !CodeFlow.isPrimitive(conditionDescriptor)) { - this.exitTypeDescriptor = conditionDescriptor; - } else { // Use the easiest to compute common super type this.exitTypeDescriptor = "Ljava/lang/Object"; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java index 6855d59c4a..cb86567bc9 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Ternary.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2017 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. @@ -35,7 +35,7 @@ import org.springframework.expression.spel.SpelMessage; public class Ternary extends SpelNodeImpl { public Ternary(int pos, SpelNodeImpl... args) { - super(pos,args); + super(pos, args); } @@ -71,12 +71,6 @@ public class Ternary extends SpelNodeImpl { if (leftDescriptor.equals(rightDescriptor)) { this.exitTypeDescriptor = leftDescriptor; } - else if (leftDescriptor.equals("Ljava/lang/Object") && !CodeFlow.isPrimitive(rightDescriptor)) { - this.exitTypeDescriptor = rightDescriptor; - } - else if (rightDescriptor.equals("Ljava/lang/Object") && !CodeFlow.isPrimitive(leftDescriptor)) { - this.exitTypeDescriptor = leftDescriptor; - } else { // Use the easiest to compute common super type this.exitTypeDescriptor = "Ljava/lang/Object"; diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index 9aef460db0..4d7efd22ef 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2017 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. @@ -17,8 +17,10 @@ package org.springframework.expression.spel; import java.io.IOException; +import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -4626,6 +4628,164 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { assertEquals(3, expression.getValue(root)); assertEquals(3, expression.getValue(root)); } + + @Test + public void elvisOperator_SPR15192() { + SpelParserConfiguration configuration = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null); + Expression exp; + + exp = new SpelExpressionParser(configuration).parseExpression("bar()"); + assertEquals("BAR", exp.getValue(new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("BAR", exp.getValue(new Foo(), String.class)); + assertIsCompiled(exp); + + exp = new SpelExpressionParser(configuration).parseExpression("bar('baz')"); + assertEquals("BAZ", exp.getValue(new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("BAZ", exp.getValue(new Foo(), String.class)); + assertIsCompiled(exp); + + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("map", Collections.singletonMap("foo", "qux")); + + exp = new SpelExpressionParser(configuration).parseExpression("bar(#map['foo'])"); + assertEquals("QUX", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("QUX", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + exp = new SpelExpressionParser(configuration).parseExpression("bar(#map['foo'] ?: 'qux')"); + assertEquals("QUX", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("QUX", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + // When the condition is a primitive + exp = new SpelExpressionParser(configuration).parseExpression("3?:'foo'"); + assertEquals("3", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("3", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + // When the condition is a double slot primitive + exp = new SpelExpressionParser(configuration).parseExpression("3L?:'foo'"); + assertEquals("3", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("3", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + // When the condition is an empty string + exp = new SpelExpressionParser(configuration).parseExpression("''?:4L"); + assertEquals("4", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("4", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + // null condition + exp = new SpelExpressionParser(configuration).parseExpression("null?:4L"); + assertEquals("4", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("4", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + // variable access returning primitive + exp = new SpelExpressionParser(configuration).parseExpression("#x?:'foo'"); + context.setVariable("x",50); + assertEquals("50", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("50", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + exp = new SpelExpressionParser(configuration).parseExpression("#x?:'foo'"); + context.setVariable("x",null); + assertEquals("foo", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("foo", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + // variable access returning array + exp = new SpelExpressionParser(configuration).parseExpression("#x?:'foo'"); + context.setVariable("x",new int[]{1,2,3}); + assertEquals("1,2,3", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("1,2,3", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + } + + @Test + public void ternaryOperator_SPR15192() { + SpelParserConfiguration configuration = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE, null); + Expression exp; + StandardEvaluationContext context = new StandardEvaluationContext(); + context.setVariable("map", Collections.singletonMap("foo", "qux")); + + exp = new SpelExpressionParser(configuration).parseExpression("bar(#map['foo'] != null ? #map['foo'] : 'qux')"); + assertEquals("QUX", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("QUX", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + exp = new SpelExpressionParser(configuration).parseExpression("3==3?3:'foo'"); + assertEquals("3", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("3", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + exp = new SpelExpressionParser(configuration).parseExpression("3!=3?3:'foo'"); + assertEquals("foo", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("foo", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + // When the condition is a double slot primitive + exp = new SpelExpressionParser(configuration).parseExpression("3==3?3L:'foo'"); + assertEquals("3", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("3", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + exp = new SpelExpressionParser(configuration).parseExpression("3!=3?3L:'foo'"); + assertEquals("foo", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("foo", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + // When the condition is an empty string + exp = new SpelExpressionParser(configuration).parseExpression("''==''?'abc':4L"); + assertEquals("abc", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("abc", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + // null condition + exp = new SpelExpressionParser(configuration).parseExpression("3==3?null:4L"); + assertEquals(null, exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals(null, exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + // variable access returning primitive + exp = new SpelExpressionParser(configuration).parseExpression("#x==#x?50:'foo'"); + context.setVariable("x",50); + assertEquals("50", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("50", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + exp = new SpelExpressionParser(configuration).parseExpression("#x!=#x?50:'foo'"); + context.setVariable("x",null); + assertEquals("foo", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("foo", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + + // variable access returning array + exp = new SpelExpressionParser(configuration).parseExpression("#x==#x?'1,2,3':'foo'"); + context.setVariable("x",new int[]{1,2,3}); + assertEquals("1,2,3", exp.getValue(context, new Foo(), String.class)); + assertCanCompile(exp); + assertEquals("1,2,3", exp.getValue(context, new Foo(), String.class)); + assertIsCompiled(exp); + } // helper methods @@ -4687,6 +4847,20 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { } } + private void assertIsCompiled(Expression expression) { + try { + Field field = SpelExpression.class.getDeclaredField("compiledAst"); + field.setAccessible(true); + Object object = field.get(expression); + assertNotNull(object); + } + catch (Exception ex) { + fail(ex.toString()); + } + } + + + // nested types public interface Message { @@ -4803,7 +4977,7 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { try { method = Payload2.class.getDeclaredMethod("getField", String.class); } - catch (Exception e) { + catch (Exception ex) { } } String descriptor = cf.lastDescriptor(); @@ -5661,4 +5835,16 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { } } + + public static class Foo { + + public String bar() { + return "BAR"; + } + + public String bar(String arg) { + return arg.toUpperCase(); + } + } + }