SPR-7173, SPR-7100

This commit is contained in:
Andy Clement 2010-05-05 23:52:01 +00:00
parent 42cdfbcd89
commit f53621a86f
10 changed files with 356 additions and 36 deletions

View File

@ -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;
}

View File

@ -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

View File

@ -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;

View File

@ -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();
}
}

View File

@ -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,29 +544,22 @@ 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<SpelNodeImpl> qualifiedIdPieces = new ArrayList<SpelNodeImpl>();
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)));
}
}
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)) {
Token methodOrPropertyName = nextToken();
@ -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<SpelNodeImpl> nodes = new ArrayList<SpelNodeImpl>();
nodes.add(possiblyQualifiedConstructorName);
eatConstructorArgs(nodes);

View File

@ -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;

View File

@ -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) {

View File

@ -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;
@ -66,6 +67,8 @@ public class StandardEvaluationContext implements EvaluationContext {
private final Map<String, Object> variables = new HashMap<String, Object>();
private BeanResolver beanResolver;
public StandardEvaluationContext() {
setRootObject(null);
@ -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<MethodResolver> methodResolvers) {
this.methodResolvers = methodResolvers;
}

View File

@ -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

View File

@ -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;
@ -350,6 +351,47 @@ public class SpringEL300Tests extends ExpressionTestCase {
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
public void testIndexingAsAPropertyAccess_SPR6968_3() {
@ -393,6 +435,36 @@ public class SpringEL300Tests extends ExpressionTestCase {
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<String,String> m;
public String floo ="bar";
public XX() {
m = new HashMap<String,String>();
m.put("$foo","wibble");
m.put("bar","siddle");
}
}
static class Goo {
public static Goo instance = new 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
}