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,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<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)));
}
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<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;
@@ -45,7 +46,7 @@ import org.springframework.util.Assert;
* @since 3.0
*/
public class StandardEvaluationContext implements EvaluationContext {
private TypedValue rootObject;
private List<ConstructorResolver> constructorResolvers;
@@ -65,6 +66,8 @@ public class StandardEvaluationContext implements EvaluationContext {
private OperatorOverloader operatorOverloader = new StandardOperatorOverloader();
private final Map<String, Object> variables = new HashMap<String, Object>();
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<MethodResolver> methodResolvers) {
this.methodResolvers = methodResolvers;
}