diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/BeanResolver.java b/org.springframework.expression/src/main/java/org/springframework/expression/BeanResolver.java new file mode 100644 index 00000000000..396ee2b3c08 --- /dev/null +++ b/org.springframework.expression/src/main/java/org/springframework/expression/BeanResolver.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.expression; + +/** + * A bean resolver can be registered with the evaluation context and will + * @author Andy Clement + * @since 3.0.3 + */ +public interface BeanResolver { + + /** + * Lookup the named bean and return it. + * @param context the current evaluation context + * @param beanname the name of the bean to lookup + * @return a object representing the bean + * @throws AccessException if there is an unexpected problem resolving the named bean + */ + Object resolve(EvaluationContext context, String beanname) throws AccessException; + +} + \ No newline at end of file diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/EvaluationContext.java b/org.springframework.expression/src/main/java/org/springframework/expression/EvaluationContext.java index 80f28197417..eed6c8c6b1d 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/EvaluationContext.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/EvaluationContext.java @@ -73,6 +73,11 @@ public interface EvaluationContext { */ OperatorOverloader getOperatorOverloader(); + /** + * @return a bean resolver that can lookup named beans + */ + BeanResolver getBeanResolver(); + /** * Set a named variable within this evaluation context to a specified value. * @param name variable to set diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelMessage.java index c9788fed57a..4cfe5d3a7dd 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelMessage.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/SpelMessage.java @@ -93,6 +93,9 @@ public enum SpelMessage { UNABLE_TO_CREATE_LIST_FOR_INDEXING(Kind.ERROR,1054,"Unable to dynamically create a List to replace a null value"),// UNABLE_TO_CREATE_MAP_FOR_INDEXING(Kind.ERROR,1055,"Unable to dynamically create a Map to replace a null value"),// UNABLE_TO_DYNAMICALLY_CREATE_OBJECT(Kind.ERROR,1056,"Unable to dynamically create instance of ''{0}'' to replace a null value"),// + NO_BEAN_RESOLVER_REGISTERED(Kind.ERROR,1057,"No bean resolver registered in the context to resolve access to bean ''{0}''"),// + EXCEPTION_DURING_BEAN_RESOLUTION(Kind.ERROR, 1058, "A problem occurred when trying to resolve bean ''{0}'':''{1}''"), // + INVALID_BEAN_REFERENCE(Kind.ERROR,1059,"@ can only be followed by an identifier or a quoted name"),// ; private Kind kind; diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java new file mode 100644 index 00000000000..e7c7eb850b4 --- /dev/null +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/ast/BeanReference.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2010 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.expression.spel.ast; + +import org.springframework.expression.AccessException; +import org.springframework.expression.BeanResolver; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.TypedValue; +import org.springframework.expression.spel.ExpressionState; +import org.springframework.expression.spel.SpelEvaluationException; +import org.springframework.expression.spel.SpelMessage; + +/** + * Represents a bean reference to a type, for example "@foo" or "@'foo.bar'" + * + * @author Andy Clement + */ +public class BeanReference extends SpelNodeImpl { + + private String beanname; + + public BeanReference(int pos,String beanname) { + super(pos); + this.beanname = beanname; + } + + @Override + public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { + BeanResolver beanResolver = state.getEvaluationContext().getBeanResolver(); + if (beanResolver==null) { + throw new SpelEvaluationException(getStartPosition(),SpelMessage.NO_BEAN_RESOLVER_REGISTERED, beanname); + } + try { + TypedValue bean = new TypedValue(beanResolver.resolve(state.getEvaluationContext(),beanname)); + return bean; + } catch (AccessException ae) { + throw new SpelEvaluationException( getStartPosition(), ae, SpelMessage.EXCEPTION_DURING_BEAN_RESOLUTION, + beanname, ae.getMessage()); + } + } + + @Override + public String toStringAST() { + StringBuilder sb = new StringBuilder(); + sb.append("@"); + if (beanname.indexOf('.')==-1) { + sb.append(beanname); + } else { + sb.append("'").append(beanname).append("'"); + } + return sb.toString(); + } + +} diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index eb86e5bea8d..8a0784409c5 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -28,6 +28,7 @@ import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.SpelParseException; import org.springframework.expression.spel.SpelParserConfiguration; import org.springframework.expression.spel.ast.Assign; +import org.springframework.expression.spel.ast.BeanReference; import org.springframework.expression.spel.ast.BooleanLiteral; import org.springframework.expression.spel.ast.CompoundExpression; import org.springframework.expression.spel.ast.ConstructorReference; @@ -437,6 +438,8 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { return pop(); } else if (maybeEatTypeReference() || maybeEatNullReference() || maybeEatConstructorReference() || maybeEatMethodOrProperty(false) || maybeEatFunctionOrVar()) { return pop(); + } else if (maybeEatBeanReference()) { + return pop(); } else if (maybeEatProjection(false) || maybeEatSelection(false) || maybeEatIndexer()) { return pop(); } else { @@ -444,7 +447,30 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { } } - + // parse: @beanname @'bean.name' + // quoted if dotted + private boolean maybeEatBeanReference() { + if (peekToken(TokenKind.BEAN_REF)) { + Token beanRefToken = nextToken(); + Token beanNameToken = null; + String beanname = null; + if (peekToken(TokenKind.IDENTIFIER)) { + beanNameToken = eatToken(TokenKind.IDENTIFIER); + beanname = beanNameToken.data; + } else if (peekToken(TokenKind.LITERAL_STRING)) { + beanNameToken = eatToken(TokenKind.LITERAL_STRING); + beanname = beanNameToken.stringValue(); + beanname = beanname.substring(1, beanname.length() - 1); + } else { + raiseInternalException(beanRefToken.startpos,SpelMessage.INVALID_BEAN_REFERENCE); + } + + BeanReference beanReference = new BeanReference(toPos(beanNameToken),beanname); + constructedNodes.push(beanReference); + return true; + } + return false; + } private boolean maybeEatTypeReference() { if (peekToken(TokenKind.IDENTIFIER)) { @@ -454,7 +480,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { } nextToken(); eatToken(TokenKind.LPAREN); - SpelNodeImpl node = eatPossiblyQualifiedId(true); + SpelNodeImpl node = eatPossiblyQualifiedId(); // dotted qualified id eatToken(TokenKind.RPAREN); constructedNodes.push(new TypeReference(toPos(typeName),node)); @@ -518,31 +544,24 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { } /** - * Eat an identifier, possibly qualified (meaning that it is dotted). If the dollarAllowed parameter is true then - * it will process any dollar characters found between names, and this allows it to support inner type references - * correctly. For example 'com.foo.bar.Outer$Inner' will produce the identifier sequence com, foo, bar, Outer, $Inner, - * note that the $ has been prefixed onto the Inner identifier. The code in TypeReference which reforms this into - * a typename copes with the $ prefixed identifiers. + * Eat an identifier, possibly qualified (meaning that it is dotted). * TODO AndyC Could create complete identifiers (a.b.c) here rather than a sequence of them? (a, b, c) */ - private SpelNodeImpl eatPossiblyQualifiedId(boolean dollarAllowed) { + private SpelNodeImpl eatPossiblyQualifiedId() { List qualifiedIdPieces = new ArrayList(); Token startnode = eatToken(TokenKind.IDENTIFIER); qualifiedIdPieces.add(new Identifier(startnode.stringValue(),toPos(startnode))); - boolean dollar = false; - while (peekToken(TokenKind.DOT,true) || (dollarAllowed && (dollar = peekToken(TokenKind.DOLLAR,true)))) { + while (peekToken(TokenKind.DOT,true)) { Token node = eatToken(TokenKind.IDENTIFIER); - if (dollar) { - qualifiedIdPieces.add(new Identifier("$"+node.stringValue(),((node.startpos-1)<<16)+node.endpos)); - } else { - qualifiedIdPieces.add(new Identifier(node.stringValue(),toPos(node))); - } + qualifiedIdPieces.add(new Identifier(node.stringValue(),toPos(node))); } return new QualifiedIdentifier(toPos(startnode.startpos,qualifiedIdPieces.get(qualifiedIdPieces.size()-1).getEndPosition()),qualifiedIdPieces.toArray(new SpelNodeImpl[qualifiedIdPieces.size()])); } + // This is complicated due to the support for dollars in identifiers. Dollars are normally separate tokens but + // there we want to combine a series of identifiers and dollars into a single identifier private boolean maybeEatMethodOrProperty(boolean nullSafeNavigation) { - if (peekToken(TokenKind.IDENTIFIER)) { + if (peekToken(TokenKind.IDENTIFIER)) { Token methodOrPropertyName = nextToken(); SpelNodeImpl[] args = maybeEatMethodArgs(); if (args==null) { @@ -557,6 +576,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { } } return false; + } //constructor @@ -564,7 +584,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser { private boolean maybeEatConstructorReference() { if (peekIdentifierToken("new")) { Token newToken = nextToken(); - SpelNodeImpl possiblyQualifiedConstructorName = eatPossiblyQualifiedId(true); + SpelNodeImpl possiblyQualifiedConstructorName = eatPossiblyQualifiedId(); List nodes = new ArrayList(); nodes.add(possiblyQualifiedConstructorName); eatConstructorArgs(nodes); diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java index 116be5d7cc4..b7d89a14568 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/TokenKind.java @@ -26,8 +26,8 @@ enum TokenKind { COLON(":"),HASH("#"),RSQUARE("]"), LSQUARE("["), DOT("."), PLUS("+"), STAR("*"), DIV("/"), NOT("!"), MINUS("-"), SELECT_FIRST("^["), SELECT_LAST("$["), QMARK("?"), PROJECT("!["), GE(">="),GT(">"),LE("<="),LT("<"),EQ("=="),NE("!="),ASSIGN("="), INSTANCEOF("instanceof"), MATCHES("matches"), BETWEEN("between"), - SELECT("?["), MOD("%"), POWER("^"), DOLLAR("$"), - ELVIS("?:"), SAFE_NAVI("?."); + SELECT("?["), MOD("%"), POWER("^"), + ELVIS("?:"), SAFE_NAVI("?."), BEAN_REF("@") ; char[] tokenChars; diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java index 5126222876a..ac8eafd2f94 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/standard/Tokenizer.java @@ -96,6 +96,9 @@ class Tokenizer { case ']': pushCharToken(TokenKind.RSQUARE); break; + case '@': + pushCharToken(TokenKind.BEAN_REF); + break; case '^': if (isTwoCharToken(TokenKind.SELECT_FIRST)) { pushPairToken(TokenKind.SELECT_FIRST); @@ -134,7 +137,7 @@ class Tokenizer { if (isTwoCharToken(TokenKind.SELECT_LAST)) { pushPairToken(TokenKind.SELECT_LAST); } else { - pushCharToken(TokenKind.DOLLAR); + lexIdentifier(); } break; case '>': @@ -424,9 +427,9 @@ class Tokenizer { tokens.add(new Token(kind,pos,pos+kind.getLength())); } - // ID: ('a'..'z'|'A'..'Z'|'_') ('a'..'z'|'A'..'Z'|'_'|'0'..'9'|DOT_ESCAPED)*; + // ID: ('a'..'z'|'A'..'Z'|'_'|'$') ('a'..'z'|'A'..'Z'|'_'|'$'|'0'..'9'|DOT_ESCAPED)*; private boolean isIdentifier(char ch) { - return isAlphabetic(ch) || isDigit(ch) || ch=='_'; + return isAlphabetic(ch) || isDigit(ch) || ch=='_' || ch=='$'; } private boolean isChar(char a,char b) { diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java index 9f56baa947c..087fbabce2c 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import org.springframework.core.convert.TypeDescriptor; +import org.springframework.expression.BeanResolver; import org.springframework.expression.ConstructorResolver; import org.springframework.expression.EvaluationContext; import org.springframework.expression.MethodFilter; @@ -45,7 +46,7 @@ import org.springframework.util.Assert; * @since 3.0 */ public class StandardEvaluationContext implements EvaluationContext { - + private TypedValue rootObject; private List constructorResolvers; @@ -65,6 +66,8 @@ public class StandardEvaluationContext implements EvaluationContext { private OperatorOverloader operatorOverloader = new StandardOperatorOverloader(); private final Map variables = new HashMap(); + + private BeanResolver beanResolver; public StandardEvaluationContext() { @@ -134,6 +137,14 @@ public class StandardEvaluationContext implements EvaluationContext { return this.methodResolvers; } + public void setBeanResolver(BeanResolver beanResolver) { + this.beanResolver = beanResolver; + } + + public BeanResolver getBeanResolver() { + return this.beanResolver; + } + public void setMethodResolvers(List methodResolvers) { this.methodResolvers = methodResolvers; } diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ParsingTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ParsingTests.java index 5c0e5a321d6..110d55b1ea3 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/ParsingTests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/ParsingTests.java @@ -20,8 +20,8 @@ import junit.framework.Assert; import org.junit.Test; import org.springframework.expression.ParseException; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; /** * Parse some expressions and check we get the AST we expect. Rather than inspecting each node in the AST, we ask it to @@ -267,18 +267,18 @@ public class ParsingTests { // parseCheck("{'a','b','a','d','e'}.distinct()"); // } - // // references - // public void testReferences01() { - // parseCheck("@(foo)"); - // } - // - // public void testReferences02() { - // parseCheck("@(p:foo)"); - // } - // - // public void testReferences04() { - // parseCheck("@(a/b/c:foo)", "@(a.b.c:foo)"); - // }// normalized to '.' for separator in QualifiedIdentifier + // references + @Test + public void testReferences01() { + parseCheck("@foo"); + parseCheck("@'foo.bar'"); + parseCheck("@\"foo.bar.goo\"","@'foo.bar.goo'"); + } + + @Test + public void testReferences03() { + parseCheck("@$$foo"); + } // properties @Test diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/SpringEL300Tests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/SpringEL300Tests.java index 54dd02fd80b..36f5cc34856 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/SpringEL300Tests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/SpringEL300Tests.java @@ -24,6 +24,7 @@ import junit.framework.Assert; import org.junit.Test; import org.springframework.expression.AccessException; +import org.springframework.expression.BeanResolver; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; @@ -349,6 +350,47 @@ public class SpringEL300Tests extends ExpressionTestCase { name = expr.getValue(eContext,String.class); // will be using the cached accessor this time Assert.assertEquals("hello",name); } + + /** $ related identifiers */ + @SuppressWarnings("unchecked") + @Test + public void testDollarPrefixedIdentifier_SPR7100() { + Holder h = new Holder(); + StandardEvaluationContext eContext = new StandardEvaluationContext(h); + eContext.addPropertyAccessor(new MapAccessor()); + h.map.put("$foo","wibble"); + h.map.put("foo$bar","wobble"); + h.map.put("foobar$$","wabble"); + h.map.put("$","wubble"); + h.map.put("$$","webble"); + h.map.put("$_$","tribble"); + String name = null; + Expression expr = null; + + expr = new SpelExpressionParser().parseRaw("map.$foo"); + name = expr.getValue(eContext,String.class); + Assert.assertEquals("wibble",name); + + expr = new SpelExpressionParser().parseRaw("map.foo$bar"); + name = expr.getValue(eContext,String.class); + Assert.assertEquals("wobble",name); + + expr = new SpelExpressionParser().parseRaw("map.foobar$$"); + name = expr.getValue(eContext,String.class); + Assert.assertEquals("wabble",name); + + expr = new SpelExpressionParser().parseRaw("map.$"); + name = expr.getValue(eContext,String.class); + Assert.assertEquals("wubble",name); + + expr = new SpelExpressionParser().parseRaw("map.$$"); + name = expr.getValue(eContext,String.class); + Assert.assertEquals("webble",name); + + expr = new SpelExpressionParser().parseRaw("map.$_$"); + name = expr.getValue(eContext,String.class); + Assert.assertEquals("tribble",name); + } /** Should be accessing Goo.wibble field because 'bar' variable evaluates to "wibble" */ @Test @@ -392,6 +434,36 @@ public class SpringEL300Tests extends ExpressionTestCase { expr.getValue(eContext,String.class); // will be using the cached accessor this time Assert.assertEquals("world",g.value); } + + @Test + public void testDollars() { + StandardEvaluationContext eContext = new StandardEvaluationContext(new XX()); + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("m['$foo']"); + eContext.setVariable("file_name","$foo"); + Assert.assertEquals("wibble",expr.getValue(eContext,String.class)); + } + + @Test + public void testDollars2() { + StandardEvaluationContext eContext = new StandardEvaluationContext(new XX()); + Expression expr = null; + expr = new SpelExpressionParser().parseRaw("m[$foo]"); + eContext.setVariable("file_name","$foo"); + Assert.assertEquals("wibble",expr.getValue(eContext,String.class)); + } + + static class XX { + public Map m; + + public String floo ="bar"; + + public XX() { + m = new HashMap(); + m.put("$foo","wibble"); + m.put("bar","siddle"); + } + } static class Goo { @@ -411,6 +483,11 @@ public class SpringEL300Tests extends ExpressionTestCase { } + static class Holder { + + public Map map = new HashMap(); + } + // --- private void checkTemplateParsing(String expression, String expectedValue) throws Exception { @@ -452,5 +529,102 @@ public class SpringEL300Tests extends ExpressionTestCase { } }; +// @Test +// public void testFails() { +// +// StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); +// evaluationContext.setVariable("target", new Foo2()); +// for (int i = 0; i < 300000; i++) { +// evaluationContext.addPropertyAccessor(new MapAccessor()); +// ExpressionParser parser = new SpelExpressionParser(); +// Expression expression = parser.parseExpression("#target.execute(payload)"); +// Message message = new Message(); +// message.setPayload(i+""); +// expression.getValue(evaluationContext, message); +// } +// } + + static class Foo2 { + public void execute(String str){ + System.out.println("Value: " + str); + } + } + + static class Message{ + private String payload; + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + } + + // bean resolver tests + + @Test + public void beanResolution() { + StandardEvaluationContext eContext = new StandardEvaluationContext(new XX()); + Expression expr = null; + + // no resolver registered == exception + try { + expr = new SpelExpressionParser().parseRaw("@foo"); + Assert.assertEquals("custard",expr.getValue(eContext,String.class)); + } catch (SpelEvaluationException see) { + Assert.assertEquals(SpelMessage.NO_BEAN_RESOLVER_REGISTERED,see.getMessageCode()); + Assert.assertEquals("foo",see.getInserts()[0]); + } + + eContext.setBeanResolver(new MyBeanResolver()); + + // bean exists + expr = new SpelExpressionParser().parseRaw("@foo"); + Assert.assertEquals("custard",expr.getValue(eContext,String.class)); + + // bean does not exist + expr = new SpelExpressionParser().parseRaw("@bar"); + Assert.assertEquals(null,expr.getValue(eContext,String.class)); + + // bean name will cause AccessException + expr = new SpelExpressionParser().parseRaw("@goo"); + try { + Assert.assertEquals(null,expr.getValue(eContext,String.class)); + } catch (SpelEvaluationException see) { + Assert.assertEquals(SpelMessage.EXCEPTION_DURING_BEAN_RESOLUTION,see.getMessageCode()); + Assert.assertEquals("goo",see.getInserts()[0]); + Assert.assertTrue(see.getCause() instanceof AccessException); + Assert.assertTrue(((AccessException)see.getCause()).getMessage().startsWith("DONT")); + } + + // bean exists + expr = new SpelExpressionParser().parseRaw("@'foo.bar'"); + Assert.assertEquals("trouble",expr.getValue(eContext,String.class)); + + // bean exists + try { + expr = new SpelExpressionParser().parseRaw("@378"); + Assert.assertEquals("trouble",expr.getValue(eContext,String.class)); + } catch (SpelParseException spe) { + Assert.assertEquals(SpelMessage.INVALID_BEAN_REFERENCE,spe.getMessageCode()); + } + } + + static class MyBeanResolver implements BeanResolver { + public Object resolve(EvaluationContext context, String beanname) throws AccessException { + if (beanname.equals("foo")) { + return "custard"; + } else if (beanname.equals("foo.bar")) { + return "trouble"; + } else if (beanname.equals("goo")) { + throw new AccessException("DONT ASK ME ABOUT GOO"); + } + return null; + } + } + + // end bean resolver tests }