From a7418f480cdcec1817e011689b5df11bf8932199 Mon Sep 17 00:00:00 2001 From: Andy Clement Date: Fri, 3 Aug 2012 10:41:19 -0700 Subject: [PATCH] Support case-insensitive null literals in SpEL Prior to this commit null literals in SpEL expressions had to be specified as "null" (i.e., all lowercase). With this commit null literals in SpEL expressions are interpreted in a case-insensitive manner, analogous to the current support for boolean literals. Issue: SPR-9613 --- .../InternalSpelExpressionParser.java | 2 +- .../expression/spel/EvaluationTests.java | 251 +++++++++--------- src/dist/changelog.txt | 1 + 3 files changed, 134 insertions(+), 120 deletions(-) diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index ec43b2bfab..84c3c27350 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -513,7 +513,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { private boolean maybeEatNullReference() { if (peekToken(TokenKind.IDENTIFIER)) { Token nullToken = peekToken(); - if (!nullToken.stringValue().equals("null")) { + if (!nullToken.stringValue().toLowerCase().equals("null")) { return false; } nextToken(); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java index bb50bfeb0b..30d637ddd8 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/EvaluationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2010 the original author or authors. + * Copyright 2002-2012 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. @@ -16,12 +16,13 @@ package org.springframework.expression.spel; +import static org.junit.Assert.*; + import java.util.List; import java.util.Map; -import junit.framework.Assert; - import org.junit.Test; + import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; @@ -38,6 +39,7 @@ import org.springframework.expression.spel.testresources.TestPerson; * * @author Andy Clement * @author Mark Fisher + * @author Sam Brannen * @since 3.0 */ public class EvaluationTests extends ExpressionTestCase { @@ -49,76 +51,66 @@ public class EvaluationTests extends ExpressionTestCase { TestClass testClass = new TestClass(); Object o = null; o = expression.getValue(new StandardEvaluationContext(testClass)); - Assert.assertEquals("",o); + assertEquals("", o); o = parser.parseExpression("list[3]").getValue(new StandardEvaluationContext(testClass)); - Assert.assertEquals("",o); - Assert.assertEquals(4, testClass.list.size()); + assertEquals("", o); + assertEquals(4, testClass.list.size()); try { o = parser.parseExpression("list2[3]").getValue(new StandardEvaluationContext(testClass)); - Assert.fail(); + fail(); } catch (EvaluationException ee) { ee.printStackTrace(); // success! } o = parser.parseExpression("foo[3]").getValue(new StandardEvaluationContext(testClass)); - Assert.assertEquals("",o); - Assert.assertEquals(4, testClass.getFoo().size()); + assertEquals("", o); + assertEquals(4, testClass.getFoo().size()); } - - @Test - public void testCreateMapsOnAttemptToIndexNull01() throws EvaluationException, ParseException { + + @Test(expected = SpelEvaluationException.class) + public void testCreateMapsOnAttemptToIndexNull01() throws Exception { TestClass testClass = new TestClass(); StandardEvaluationContext ctx = new StandardEvaluationContext(testClass); ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); Object o = null; o = parser.parseExpression("map['a']").getValue(ctx); - Assert.assertNull(o); + assertNull(o); o = parser.parseExpression("map").getValue(ctx); - Assert.assertNotNull(o); - - try { - o = parser.parseExpression("map2['a']").getValue(ctx); - // fail! - Assert.fail("map2 should be null, there is no setter"); - } catch (Exception e) { - // success! - } + assertNotNull(o); + + o = parser.parseExpression("map2['a']").getValue(ctx); + // map2 should be null, there is no setter } - @Test - public void testCreateObjectsOnAttemptToReferenceNull() throws EvaluationException, ParseException { + // wibble2 should be null (cannot be initialized dynamically), there is no setter + @Test(expected = SpelEvaluationException.class) + public void testCreateObjectsOnAttemptToReferenceNull() throws Exception { TestClass testClass = new TestClass(); StandardEvaluationContext ctx = new StandardEvaluationContext(testClass); ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true)); Object o = null; o = parser.parseExpression("wibble.bar").getValue(ctx); - Assert.assertEquals("hello",o); + assertEquals("hello", o); o = parser.parseExpression("wibble").getValue(ctx); - Assert.assertNotNull(o); - - try { - o = parser.parseExpression("wibble2.bar").getValue(ctx); - // fail! - Assert.fail("wibble2 should be null (cannot be initialized dynamically), there is no setter"); - } catch (Exception e) { - // success! - } + assertNotNull(o); + + o = parser.parseExpression("wibble2.bar").getValue(ctx); } - + + static class TestClass { - + public Foo wibble; private Foo wibble2; public Map map; - public Map mapStringToInteger; + public Map mapStringToInteger; public List list; public List list2; private Map map2; - + private List foo; + public Map getMap2() { return this.map2; } public Foo getWibble2() { return this.wibble2; } -// public void setMap2(Map m) { this.map2 = m; } - private List foo; public List getFoo() { return this.foo; } public void setFoo(List newfoo) { this.foo = newfoo; } } @@ -127,18 +119,19 @@ public class EvaluationTests extends ExpressionTestCase { public Foo() {} public String bar = "hello"; } - + + @Test public void testElvis01() { - evaluate("'Andy'?:'Dave'","Andy",String.class); - evaluate("null?:'Dave'","Dave",String.class); + evaluate("'Andy'?:'Dave'", "Andy", String.class); + evaluate("null?:'Dave'", "Dave", String.class); } @Test public void testSafeNavigation() { - evaluate("null?.null?.null",null,null); + evaluate("null?.null?.null", null, null); } - + @Test public void testRelOperatorGT01() { evaluate("3 > 6", "false", Boolean.class); @@ -163,7 +156,7 @@ public class EvaluationTests extends ExpressionTestCase { public void testRelOperatorGE02() { evaluate("3 >= 3", "true", Boolean.class); } - + @Test public void testRelOperatorsInstanceof01() { evaluate("'xyz' instanceof T(int)", "false", Boolean.class); @@ -218,12 +211,12 @@ public class EvaluationTests extends ExpressionTestCase { // property access @Test public void testPropertyField01() { - evaluate("name", "Nikola Tesla", String.class, false); + evaluate("name", "Nikola Tesla", String.class, false); // not writable because (1) name is private (2) there is no setter, only a getter evaluateAndCheckError("madeup", SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, 0, "madeup", - "org.springframework.expression.spel.testresources.Inventor"); + "org.springframework.expression.spel.testresources.Inventor"); } - + @Test public void testPropertyField02_SPR7100() { evaluate("_name", "Nikola Tesla", String.class); @@ -234,15 +227,14 @@ public class EvaluationTests extends ExpressionTestCase { public void testRogueTrailingDotCausesNPE_SPR6866() { try { new SpelExpressionParser().parseExpression("placeOfBirth.foo."); - Assert.fail("Should have failed to parse"); + fail("Should have failed to parse"); } catch (ParseException e) { - Assert.assertTrue(e instanceof SpelParseException); - SpelParseException spe = (SpelParseException)e; - Assert.assertEquals(SpelMessage.OOD,spe.getMessageCode()); - Assert.assertEquals(16,spe.getPosition()); - } + assertTrue(e instanceof SpelParseException); + SpelParseException spe = (SpelParseException) e; + assertEquals(SpelMessage.OOD, spe.getMessageCode()); + assertEquals(16, spe.getPosition()); + } } - // nested properties @Test @@ -259,13 +251,13 @@ public class EvaluationTests extends ExpressionTestCase { public void testPropertiesNested03() throws ParseException { try { new SpelExpressionParser().parseRaw("placeOfBirth.23"); - Assert.fail(); + fail(); } catch (SpelParseException spe) { - Assert.assertEquals(spe.getMessageCode(), SpelMessage.UNEXPECTED_DATA_AFTER_DOT); - Assert.assertEquals("23", spe.getInserts()[0]); + assertEquals(spe.getMessageCode(), SpelMessage.UNEXPECTED_DATA_AFTER_DOT); + assertEquals("23", spe.getInserts()[0]); } } - + // methods @Test public void testMethods01() { @@ -287,21 +279,21 @@ public class EvaluationTests extends ExpressionTestCase { public void testConstructorInvocation05() { evaluate("new java.lang.String('foobar')", "foobar", String.class); } - + @Test public void testConstructorInvocation06() throws Exception { // repeated evaluation to drive use of cached executor - SpelExpression expr = (SpelExpression)parser.parseExpression("new String('wibble')"); + SpelExpression expr = (SpelExpression) parser.parseExpression("new String('wibble')"); String newString = expr.getValue(String.class); - Assert.assertEquals("wibble",newString); + assertEquals("wibble", newString); newString = expr.getValue(String.class); - Assert.assertEquals("wibble",newString); - + assertEquals("wibble", newString); + // not writable - Assert.assertFalse(expr.isWritable(new StandardEvaluationContext())); - + assertFalse(expr.isWritable(new StandardEvaluationContext())); + // ast - Assert.assertEquals("new String('wibble')",expr.toStringAST()); + assertEquals("new String('wibble')", expr.toStringAST()); } // unary expressions @@ -358,38 +350,39 @@ public class EvaluationTests extends ExpressionTestCase { @Test public void testTernaryOperator01() { - evaluate("2>4?1:2",2,Integer.class); + evaluate("2>4?1:2", 2, Integer.class); } @Test public void testTernaryOperator02() { - evaluate("'abc'=='abc'?1:2",1,Integer.class); + evaluate("'abc'=='abc'?1:2", 1, Integer.class); } - + @Test public void testTernaryOperator03() { - evaluateAndCheckError("'hello'?1:2", SpelMessage.TYPE_CONVERSION_ERROR); // cannot convert String to boolean + // cannot convert String to boolean + evaluateAndCheckError("'hello'?1:2", SpelMessage.TYPE_CONVERSION_ERROR); } @Test public void testTernaryOperator04() throws Exception { Expression expr = parser.parseExpression("1>2?3:4"); - Assert.assertFalse(expr.isWritable(eContext)); + assertFalse(expr.isWritable(eContext)); } @Test public void testTernaryOperator05() { - evaluate("1>2?#var=4:#var=5",5,Integer.class); - evaluate("3?:#var=5",3,Integer.class); - evaluate("null?:#var=5",5,Integer.class); - evaluate("2>4?(3>2?true:false):(5<3?true:false)",false,Boolean.class); + evaluate("1>2?#var=4:#var=5", 5, Integer.class); + evaluate("3?:#var=5", 3, Integer.class); + evaluate("null?:#var=5", 5, Integer.class); + evaluate("2>4?(3>2?true:false):(5<3?true:false)", false, Boolean.class); } @Test(expected = EvaluationException.class) public void testTernaryOperatorWithNullValue() { parser.parseExpression("null ? 0 : 1").getValue(); } - + @Test public void methodCallWithRootReferenceThroughParameter() { evaluate("placeOfBirth.doubleIt(inventions.length)", 18, Integer.class); @@ -397,28 +390,29 @@ public class EvaluationTests extends ExpressionTestCase { @Test public void ctorCallWithRootReferenceThroughParameter() { - evaluate("new org.springframework.expression.spel.testresources.PlaceOfBirth(inventions[0].toString()).city", "Telephone repeater", String.class); + evaluate("new org.springframework.expression.spel.testresources.PlaceOfBirth(inventions[0].toString()).city", + "Telephone repeater", String.class); } @Test public void fnCallWithRootReferenceThroughParameter() { evaluate("#reverseInt(inventions.length, inventions.length, inventions.length)", "int[3]{9,9,9}", int[].class); } - + @Test public void methodCallWithRootReferenceThroughParameterThatIsAFunctionCall() { evaluate("placeOfBirth.doubleIt(#reverseInt(inventions.length,2,3)[2])", 18, Integer.class); } - + @Test public void testIndexer03() { evaluate("'christian'[8]", "n", String.class); } - - + @Test public void testIndexerError() { - evaluateAndCheckError("new org.springframework.expression.spel.testresources.Inventor().inventions[1]",SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); + evaluateAndCheckError("new org.springframework.expression.spel.testresources.Inventor().inventions[1]", + SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); } @Test @@ -447,16 +441,16 @@ public class EvaluationTests extends ExpressionTestCase { public void testTypeReferences01() { evaluate("T(java.lang.String)", "class java.lang.String", Class.class); } - + @Test public void testTypeReferencesAndQualifiedIdentifierCaching() throws Exception { - SpelExpression expr = (SpelExpression)parser.parseExpression("T(java.lang.String)"); - Assert.assertFalse(expr.isWritable(new StandardEvaluationContext())); - Assert.assertEquals("T(java.lang.String)",expr.toStringAST()); - Assert.assertEquals(String.class,expr.getValue(Class.class)); + SpelExpression expr = (SpelExpression) parser.parseExpression("T(java.lang.String)"); + assertFalse(expr.isWritable(new StandardEvaluationContext())); + assertEquals("T(java.lang.String)", expr.toStringAST()); + assertEquals(String.class, expr.getValue(Class.class)); // use cached QualifiedIdentifier: - Assert.assertEquals("T(java.lang.String)",expr.toStringAST()); - Assert.assertEquals(String.class,expr.getValue(Class.class)); + assertEquals("T(java.lang.String)", expr.toStringAST()); + assertEquals(String.class, expr.getValue(Class.class)); } @Test @@ -490,49 +484,50 @@ public class EvaluationTests extends ExpressionTestCase { evaluateAndAskForReturnType("3*4+5", "17", String.class); } - @Test public void testAdvancedNumerics() throws Exception { int twentyFour = parser.parseExpression("2.0 * 3e0 * 4").getValue(Integer.class); - Assert.assertEquals(24,twentyFour); + assertEquals(24, twentyFour); double one = parser.parseExpression("8.0 / 5e0 % 2").getValue(Double.class); - Assert.assertEquals(1.6d,one); + assertEquals(1.6d, one, 0); int o = parser.parseExpression("8.0 / 5e0 % 2").getValue(Integer.class); - Assert.assertEquals(1,o); + assertEquals(1, o); int sixteen = parser.parseExpression("-2 ^ 4").getValue(Integer.class); - Assert.assertEquals(16,sixteen); + assertEquals(16, sixteen); int minusFortyFive = parser.parseExpression("1+2-3*8^2/2/2").getValue(Integer.class); - Assert.assertEquals(-45,minusFortyFive); + assertEquals(-45, minusFortyFive); } - + @Test public void testComparison() throws Exception { EvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); - boolean trueValue = parser.parseExpression("T(java.util.Date) == Birthdate.Class").getValue(context, Boolean.class); - Assert.assertTrue(trueValue); + boolean trueValue = parser.parseExpression("T(java.util.Date) == Birthdate.Class").getValue(context, + Boolean.class); + assertTrue(trueValue); } - + @Test public void testResolvingList() throws Exception { StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext(); try { - Assert.assertFalse(parser.parseExpression("T(List)!=null").getValue(context, Boolean.class)); - Assert.fail("should have failed to find List"); + assertFalse(parser.parseExpression("T(List)!=null").getValue(context, Boolean.class)); + fail("should have failed to find List"); } catch (EvaluationException ee) { // success - List not found } - ((StandardTypeLocator)context.getTypeLocator()).registerImport("java.util"); - Assert.assertTrue(parser.parseExpression("T(List)!=null").getValue(context, Boolean.class)); + ((StandardTypeLocator) context.getTypeLocator()).registerImport("java.util"); + assertTrue(parser.parseExpression("T(List)!=null").getValue(context, Boolean.class)); } - + @Test public void testResolvingString() throws Exception { - Class stringClass = parser.parseExpression("T(String)").getValue(Class.class); - Assert.assertEquals(String.class,stringClass); - } - + Class stringClass = parser.parseExpression("T(String)").getValue(Class.class); + assertEquals(String.class, stringClass); + } + /** - * SPR-6984: attempting to index a collection on write using an index that doesn't currently exist in the collection (address.crossStreets[0] below) + * SPR-6984: attempting to index a collection on write using an index that + * doesn't currently exist in the collection (address.crossStreets[0] below) */ @Test public void initializingCollectionElementsOnWrite() throws Exception { @@ -542,20 +537,38 @@ public class EvaluationTests extends ExpressionTestCase { ExpressionParser parser = new SpelExpressionParser(config); Expression expression = parser.parseExpression("name"); expression.setValue(context, "Oleg"); - Assert.assertEquals("Oleg",person.getName()); + assertEquals("Oleg", person.getName()); expression = parser.parseExpression("address.street"); expression.setValue(context, "123 High St"); - Assert.assertEquals("123 High St",person.getAddress().getStreet()); - + assertEquals("123 High St", person.getAddress().getStreet()); + expression = parser.parseExpression("address.crossStreets[0]"); expression.setValue(context, "Blah"); - Assert.assertEquals("Blah",person.getAddress().getCrossStreets().get(0)); - + assertEquals("Blah", person.getAddress().getCrossStreets().get(0)); + expression = parser.parseExpression("address.crossStreets[3]"); expression.setValue(context, "Wibble"); - Assert.assertEquals("Blah",person.getAddress().getCrossStreets().get(0)); - Assert.assertEquals("Wibble",person.getAddress().getCrossStreets().get(3)); + assertEquals("Blah", person.getAddress().getCrossStreets().get(0)); + assertEquals("Wibble", person.getAddress().getCrossStreets().get(3)); } - + + /** + * Verifies behavior requested in SPR-9613. + */ + @Test + public void caseInsensitiveNullLiterals() { + ExpressionParser parser = new SpelExpressionParser(); + Expression exp; + + exp = parser.parseExpression("null"); + assertNull(exp.getValue()); + + exp = parser.parseExpression("NULL"); + assertNull(exp.getValue()); + + exp = parser.parseExpression("NuLl"); + assertNull(exp.getValue()); + } + } diff --git a/src/dist/changelog.txt b/src/dist/changelog.txt index e5a777baa1..9f1766ee2e 100644 --- a/src/dist/changelog.txt +++ b/src/dist/changelog.txt @@ -9,6 +9,7 @@ Changes in version 3.2 M2 (2012-08-xx) * spring-test module now depends on junit:junit-dep (SPR-6966) * now inferring return type of parameterized factory methods (SPR-9493) * SpEL Tokenizer now supports methods on integers (SPR-9612) +* introduced support for case-insensitive null literals in SpEL expressions (SPR-9613) * now using BufferedInputStream in SimpleMetaDataReader to double performance (SPR-9528) * introduced "repeatCount" property in Quartz SimpleTriggerFactoryBean (SPR-9521) * introduced "jtaTransactionManager" property in Hibernate 4 LocalSessionFactoryBean/Builder (SPR-9480)