/**
* MVEL 2.0
* Copyright (C) 2007 The Codehaus
* Mike Brock, Dhanji Prasanna, John Graham, Mark Proctor
*
* 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.mvel2.compiler;
import java.lang.reflect.Field;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.mvel2.CompileException;
import org.mvel2.ErrorDetail;
import org.mvel2.MVEL;
import org.mvel2.ParserContext;
import org.mvel2.ast.Function;
import org.mvel2.optimizers.AbstractOptimizer;
import org.mvel2.optimizers.impl.refl.nodes.WithAccessor;
import org.mvel2.util.ErrorUtil;
import org.mvel2.util.NullType;
import org.mvel2.util.ParseTools;
import org.mvel2.util.StringAppender;
import static org.mvel2.util.ParseTools.*;
import static org.mvel2.util.PropertyTools.getFieldOrAccessor;
/**
* This verifier is used by the compiler to enforce rules such as type strictness. It is, as side-effect, also
* responsible for extracting type information.
*
* @author Mike Brock
* @author Dhanji Prasanna
*/
public class PropertyVerifier extends AbstractOptimizer {
private static final int DONE = -1;
private static final int NORM = 0;
private static final int METH = 1;
private static final int COL = 2;
private static final int WITH = 3;
private List<String> inputs = new LinkedList<String>();
private boolean first = false;
private boolean classLiteral = false;
private boolean resolvedExternally;
private boolean methodCall = false;
private boolean deepProperty = false;
private boolean fqcn = false;
private Map<String, Type> paramTypes;
private Class ctx = null;
public PropertyVerifier(char[] property, ParserContext parserContext) {
this.length = end = (this.expr = property).length;
this.pCtx = parserContext;
}
public PropertyVerifier(char[] property, int start, int offset, ParserContext parserContext) {
this.expr = property;
this.start = start;
this.length = offset;
this.end = start + offset;
this.pCtx = parserContext;
}
public PropertyVerifier(String property, ParserContext parserContext) {
this.length = end = (this.expr = property.toCharArray()).length;
this.pCtx = parserContext;
}
public PropertyVerifier(String property, ParserContext parserContext, Class root) {
this.end = this.length = (this.expr = property.toCharArray()).length;
if (property.length() > 0 && property.charAt(0) == '.') {
this.cursor = this.st = this.start = 1;
}
this.pCtx = parserContext;
this.ctx = root;
}
public List<String> getInputs() {
return inputs;
}
public void setInputs(List<String> inputs) {
this.inputs = inputs;
}
/**
* Analyze the statement and return the known egress type.
*
* @return known engress type
*/
public Class analyze() {
cursor = start;
resolvedExternally = true;
if (ctx == null) {
ctx = Object.class;
first = true;
}
while (cursor < end) {
classLiteral = false;
switch (nextSubToken()) {
case NORM:
ctx = getBeanProperty(ctx, capture());
break;
case METH:
ctx = getMethod(ctx, capture());
break;
case COL:
ctx = getCollectionProperty(ctx, capture());
break;
case WITH:
ctx = getWithProperty(ctx);
break;
case DONE:
break;
}
if (cursor < length && !first) deepProperty = true;
first = false;
}
return ctx;
}
private void recordTypeParmsForProperty(String property) {
if (pCtx.isStrictTypeEnforcement()) {
if ((paramTypes = pCtx.getTypeParameters(property)) == null) {
pCtx.addTypeParameters(property, pCtx.getVarOrInputType(property));
}
pCtx.setLastTypeParameters(pCtx.getTypeParametersAsArray(property));
}
}
/**
* Process bean property
*
* @param ctx - the ingress type
* @param property - the property component
* @return known egress type.
*/
private Class getBeanProperty(Class ctx, String property) {
if (first) {
if (pCtx.hasVarOrInput(property)) {
if (pCtx.isStrictTypeEnforcement()) {
recordTypeParmsForProperty(property);
}
return pCtx.getVarOrInputType(property);
}
else if (pCtx.hasImport(property)) {
resolvedExternally = false;
return pCtx.getImport(property);
}
else if (!pCtx.isStrongTyping()) {
return Object.class;
}
else if (pCtx.hasVarOrInput("this")) {
if (pCtx.isStrictTypeEnforcement()) {
recordTypeParmsForProperty("this");
}
ctx = pCtx.getVarOrInputType("this");
resolvedExternally = false;
}
}
st = cursor;
boolean switchStateReg;
Member member = ctx != null ? getFieldOrAccessor(ctx, property) : null;
if (MVEL.COMPILER_OPT_SUPPORT_JAVA_STYLE_CLASS_LITERALS) {
if ("class".equals(property)) {
return Class.class;
}
}
if (member instanceof Field) {
if (pCtx.isStrictTypeEnforcement()) {
Field f = ((Field) member);
if (f.getGenericType() != null) {
if (f.getGenericType() instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) f.getGenericType();
pCtx.setLastTypeParameters(pt.getActualTypeArguments());
Type[] gpt = pt.getActualTypeArguments();
Type[] classArgs = type2Class(pt.getRawType()).getTypeParameters();
if (gpt.length > 0 && paramTypes == null) paramTypes = new HashMap<String, Type>();
for (int i = 0; i < gpt.length; i++) {
paramTypes.put(classArgs[i].toString(), gpt[i]);
}
} else if (f.getGenericType() instanceof TypeVariable) {
TypeVariable tv = (TypeVariable) f.getGenericType();
Type paramType = paramTypes.remove(tv.getName());
if (paramType != null && paramType instanceof Class) {
return (Class) paramType;
}
}
}
return f.getType();
}
else {
return ((Field) member).getType();
}
}
if (member != null) {
return getReturnType(ctx, (Method) member);
}
if (pCtx != null && first && pCtx.hasImport(property)) {
Class<?> importedClass = pCtx.getImport(property);
if (importedClass != null) return pCtx.getImport(property);
}
if (pCtx != null && pCtx.getLastTypeParameters() != null && pCtx.getLastTypeParameters().length != 0
&& ((Collection.class.isAssignableFrom(ctx) && !(switchStateReg = false))
|| (Map.class.isAssignableFrom(ctx) && (switchStateReg = true)))) {
Type parm = pCtx.getLastTypeParameters()[switchStateReg ? 1 : 0];
pCtx.setLastTypeParameters(null);
return parm instanceof ParameterizedType ? Object.class : (Class) parm;
}
if (pCtx != null && "length".equals(property) && ctx.isArray()) {
return Integer.class;
}
Object tryStaticMethodRef = tryStaticAccess();
if (tryStaticMethodRef != null) {
fqcn = true;
resolvedExternally = false;
if (tryStaticMethodRef instanceof Class) {
classLiteral = !(MVEL.COMPILER_OPT_SUPPORT_JAVA_STYLE_CLASS_LITERALS &&
new String(expr, end - 6, 6).equals(".class"));
return classLiteral ? (Class) tryStaticMethodRef : Class.class;
}
else if (tryStaticMethodRef instanceof Field) {
try {
return ((Field) tryStaticMethodRef).get(null).getClass();
}
catch (Exception e) {
throw new CompileException("in verifier: ", expr, start, e);
}
}
else {
try {
return ((Method) tryStaticMethodRef).getReturnType();
}
catch (Exception e) {
throw new CompileException("in verifier: ", expr, start, e);
}
}
}
if (ctx != null) {
try {
return findClass(variableFactory, ctx.getName() + "$" + property, pCtx);
}
catch (ClassNotFoundException cnfe) {
// fall through.
}
}
if (pCtx!=null&& pCtx.getParserConfiguration()!=null?pCtx.getParserConfiguration().isAllowNakedMethCall():MVEL.COMPILER_OPT_ALLOW_NAKED_METH_CALL) {
Class cls = getMethod(ctx, property);
if (cls != Object.class) {
return cls;
}
}
if (pCtx.isStrictTypeEnforcement()) {
throw new CompileException("unqualified type in strict mode for: " + property, expr, tkStart);
}
return Object.class;
}
private Class getReturnType(Class context, Method m) {
Class declaringClass = m.getDeclaringClass();
if (context == declaringClass) {
return returnGenericType(m);
}
Type returnType = m.getGenericReturnType();
if (returnType instanceof TypeVariable) {
String typeName = ((TypeVariable)returnType).getName();
Type superType = context.getGenericSuperclass();
Class superClass = context.getSuperclass();
while (superClass != null && superClass != declaringClass) {
superType = superClass.getGenericSuperclass();
superClass = superClass.getSuperclass();
}
if (superClass == null) {
return returnGenericType(m);
}
if (superType instanceof ParameterizedType) {
TypeVariable[] typeParams = superClass.getTypeParameters();
int typePos = -1;
for (int i = 0; i < typeParams.length; i++) {
if (typeParams[i].getName().equals(typeName)) {
typePos = i;
break;
}
}
if (typePos < 0) {
return returnGenericType(m);
}
Type actualType = ((ParameterizedType)superType).getActualTypeArguments()[typePos];
return actualType instanceof Class ? (Class)actualType : returnGenericType(m);
}
}
return returnGenericType(m);
}
private void recordParametricReturnedType(Type parametricReturnType) {
//push return type parameters onto parser context, only if this is a parametric type
if (parametricReturnType instanceof ParameterizedType) {
pCtx.setLastTypeParameters(((ParameterizedType) parametricReturnType).getActualTypeArguments());
ParameterizedType pt = (ParameterizedType) parametricReturnType;
Type[] gpt = pt.getActualTypeArguments();
Type[] classArgs = type2Class(pt.getRawType()).getTypeParameters();
if (gpt.length > 0 && paramTypes == null) paramTypes = new HashMap<String, Type>();
for (int i = 0; i < gpt.length; i++) {
paramTypes.put(classArgs[i].toString(), gpt[i]);
}
}
}
private Class<?> returnGenericType(Method m) {
Type parametricReturnType = m.getGenericReturnType();
recordParametricReturnedType(parametricReturnType);
String returnTypeArg = parametricReturnType.toString();
//push return type parameters onto parser context, only if this is a parametric type
if (parametricReturnType instanceof ParameterizedType) {
pCtx.setLastTypeParameters(((ParameterizedType) parametricReturnType).getActualTypeArguments());
}
if (paramTypes != null && paramTypes.containsKey(returnTypeArg)) {
/**
* If the paramTypes Map contains the known type, return that type.
*/
return type2Class( paramTypes.get(returnTypeArg) );
}
return m.getReturnType();
}
/**
* Process collection property
*
* @param ctx - the ingress type
* @param property - the property component
* @return known egress type
*/
private Class getCollectionProperty(Class ctx, String property) {
if (first) {
if (pCtx.hasVarOrInput(property)) {
ctx = getSubComponentType(pCtx.getVarOrInputType(property));
}
else if (pCtx.hasImport(property)) {
resolvedExternally = false;
ctx = getSubComponentType(pCtx.getImport(property));
}
else {
ctx = Object.class;
}
}
if (pCtx.isStrictTypeEnforcement()) {
if (Map.class.isAssignableFrom(property.length() != 0 ? ctx = getBeanProperty(ctx, property) : ctx)) {
ctx = type2Class(pCtx.getLastTypeParameters().length != 0 ? pCtx.getLastTypeParameters()[1] : Object.class);
}
else if (Collection.class.isAssignableFrom(ctx)) {
ctx = pCtx.getLastTypeParameters().length == 0 ? Object.class : type2Class(pCtx.getLastTypeParameters()[0]);
}
else if (ctx.isArray()) {
ctx = ctx.getComponentType();
}
else if (pCtx.isStrongTyping()) {
throw new CompileException("unknown collection type: " + ctx + "; property=" + property, expr, start);
}
}
else {
ctx = Object.class;
}
++cursor;
skipWhitespace();
int start = cursor;
if (scanTo(']')) {
addFatalError("unterminated [ in token");
}
MVEL.analysisCompile(new String(expr, start, cursor - start), pCtx);
++cursor;
return ctx;
}
/**
* Process method
*
* @param ctx - the ingress type
* @param name - the property component
* @return known egress type.
*/
private Class getMethod(Class ctx, String name) {
int st = cursor;
/**
* Check to see if this is the first element in the statement.
*/
if (first) {
first = false;
methodCall = true;
/**
* It's the first element in the statement, therefore we check to see if there is a static import of a
* native Java method or an MVEL function.
*/
if (pCtx.hasImport(name)) {
Method m = pCtx.getStaticImport(name).getMethod();
/**
* Replace the method parameters.
*/
ctx = m.getDeclaringClass();
name = m.getName();
}
else {
Function f = pCtx.getFunction(name);
if (f != null && f.getEgressType() != null) {
resolvedExternally = false;
f.checkArgumentCount(
parseParameterList(
(((cursor = balancedCapture(expr, cursor, end, '(')) - st) > 1 ?
ParseTools.subset(expr, st + 1, cursor - st - 1) : new char[0]), 0, -1).size());
return f.getEgressType();
}
else if (pCtx.hasVarOrInput("this")) {
if (pCtx.isStrictTypeEnforcement()) {
recordTypeParmsForProperty("this");
}
ctx = pCtx.getVarOrInputType("this");
resolvedExternally = false;
}
}
}
/**
* Get the arguments for the method.
*/
String tk;
if (cursor < end && expr[cursor] == '(' && ((cursor = balancedCapture(expr, cursor, end, '(')) - st) > 1) {
tk = new String(expr, st + 1, cursor - st - 1);
}
else {
tk = "";
}
cursor++;
/**
* Parse out the arguments list.
*/
Class[] args;
List<char[]> subtokens = parseParameterList(tk.toCharArray(), 0, -1);
if (subtokens.size() == 0) {
args = new Class[0];
subtokens = Collections.emptyList();
}
else {
// ParserContext subCtx = pCtx.createSubcontext();
args = new Class[subtokens.size()];
/**
* Subcompile all the arguments to determine their known types.
*/
// ExpressionCompiler compiler;
List<ErrorDetail> errors = pCtx.getErrorList().isEmpty() ?
pCtx.getErrorList() : new ArrayList<ErrorDetail>(pCtx.getErrorList());
CompileException rethrow = null;
for (int i = 0; i < subtokens.size(); i++) {
try {
args[i] = MVEL.analyze(subtokens.get(i), pCtx);
if ("null".equals(String.valueOf(subtokens.get(i)))) {
args[i] = NullType.class;
}
}
catch (CompileException e) {
rethrow = ErrorUtil.rewriteIfNeeded(e, expr, this.st);
}
if (errors.size() < pCtx.getErrorList().size()) {
for (ErrorDetail detail : pCtx.getErrorList()) {
if (!errors.contains(detail)) {
detail.setExpr(expr);
detail.setCursor(new String(expr).substring(this.st).indexOf(new String(subtokens.get(i))) + this.st);
detail.setColumn(0);
detail.setLineNumber(0);
detail.calcRowAndColumn();
}
}
}
if (rethrow != null) {
throw rethrow;
}
}
}
/**
* If the target object is an instance of java.lang.Class itself then do not
* adjust the Class scope target.
*/
Method m;
/**
* If we have not cached the method then we need to go ahead and try to resolve it.
*/
if ((m = getBestCandidate(args, name, ctx, ctx.getMethods(), pCtx.isStrongTyping())) == null) {
if ((m = getBestCandidate(args, name, ctx, ctx.getDeclaredMethods(), pCtx.isStrongTyping())) == null) {
StringAppender errorBuild = new StringAppender();
for (int i = 0; i < args.length; i++) {
errorBuild.append(args[i] != null ? args[i].getName() : null);
if (i < args.length - 1) errorBuild.append(", ");
}
if (("size".equals(name) || "length".equals(name)) && args.length == 0 && ctx.isArray()) {
return Integer.class;
}
if (pCtx.isStrictTypeEnforcement()) {
throw new CompileException("unable to resolve method using strict-mode: "
+ ctx.getName() + "." + name + "(" + errorBuild.toString() + ")", expr, tkStart);
}
return Object.class;
}
}
/**
* If we're in strict mode, we look for generic type information.
*/
if (pCtx.isStrictTypeEnforcement() && m.getGenericReturnType() != null) {
Map<String, Class> typeArgs = new HashMap<String, Class>();
Type[] gpt = m.getGenericParameterTypes();
Class z;
ParameterizedType pt;
for (int i = 0; i < gpt.length; i++) {
if (gpt[i] instanceof ParameterizedType) {
pt = (ParameterizedType) gpt[i];
if ((z = pCtx.getImport(new String(subtokens.get(i)))) != null) {
/**
* We record the value of the type parameter to our typeArgs Map.
*/
if (pt.getRawType().equals(Class.class)) {
/**
* If this is an instance of Class, we deal with the special parameterization case.
*/
typeArgs.put(pt.getActualTypeArguments()[0].toString(), z);
}
else {
typeArgs.put(gpt[i].toString(), z);
}
}
}
}
if (pCtx.isStrictTypeEnforcement() && ctx.getTypeParameters().length != 0 && pCtx.getLastTypeParameters() !=
null && pCtx.getLastTypeParameters().length == ctx.getTypeParameters().length) {
TypeVariable[] typeVariables = ctx.getTypeParameters();
for (int i = 0; i < typeVariables.length; i++) {
Type typeArg = pCtx.getLastTypeParameters()[i];
typeArgs.put(typeVariables[i].getName(), typeArg instanceof Class ? type2Class( pCtx.getLastTypeParameters()[i] ) : Object.class);
}
}
/**
* Get the return type argument
*/
Type parametricReturnType = m.getGenericReturnType();
String returnTypeArg = parametricReturnType.toString();
//push return type parameters onto parser context, only if this is a parametric type
if (parametricReturnType instanceof ParameterizedType) {
pCtx.setLastTypeParameters(((ParameterizedType) parametricReturnType).getActualTypeArguments());
}
if (paramTypes != null && paramTypes.containsKey(returnTypeArg)) {
/**
* If the paramTypes Map contains the known type, return that type.
*/
return type2Class(paramTypes.get(returnTypeArg));
}
else if (typeArgs.containsKey(returnTypeArg)) {
/**
* If the generic type was declared as part of the method, it will be in this
* Map.
*/
return typeArgs.get(returnTypeArg);
}
}
if (!Modifier.isPublic(m.getModifiers()) && pCtx.isStrictTypeEnforcement()) {
StringAppender errorBuild = new StringAppender();
for (int i = 0; i < args.length; i++) {
errorBuild.append(args[i] != null ? args[i].getName() : null);
if (i < args.length - 1) errorBuild.append(", ");
}
String scope = Modifier.toString(m.getModifiers());
if (scope.trim().equals("")) scope = "<package local>";
addFatalError("the referenced method is not accessible: "
+ ctx.getName() + "." + name + "(" + errorBuild.toString() + ")"
+ " (scope: " + scope + "; required: public", this.tkStart);
}
return getReturnType(ctx, m);
}
private static Class type2Class(Type type) {
return type instanceof Class ? (Class) type : (Class) ((ParameterizedType) type).getRawType();
}
private Class getWithProperty(Class ctx) {
String root = new String(expr, 0, cursor - 1).trim();
int start = cursor + 1;
cursor = balancedCaptureWithLineAccounting(expr, cursor, end, '{', pCtx);
new WithAccessor(pCtx, root, expr, start, cursor++ - start, ctx);
return ctx;
}
public boolean isResolvedExternally() {
return resolvedExternally;
}
public boolean isClassLiteral() {
return classLiteral;
}
public boolean isDeepProperty() {
return deepProperty;
}
public boolean isInput() {
return resolvedExternally && !methodCall;
}
public boolean isMethodCall() {
return methodCall;
}
public boolean isFqcn() {
return fqcn;
}
public Class getCtx() {
return ctx;
}
public void setCtx(Class ctx) {
this.ctx = ctx;
}
}