
org.apache.ibatis.ognl.enhance.ExpressionCompiler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mybatis Show documentation
Show all versions of mybatis Show documentation
The MyBatis SQL mapper framework makes it easier to use a relational database with object-oriented
applications. MyBatis couples objects with stored procedures or SQL statements using a XML descriptor or
annotations. Simplicity is the biggest advantage of the MyBatis data mapper over object relational mapping
tools.
The newest version!
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* and/or LICENSE file distributed with this work for additional
* information regarding copyright ownership. The ASF licenses
* this file to you 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.apache.ibatis.ognl.enhance;
import org.apache.ibatis.javassist.CannotCompileException;
import org.apache.ibatis.javassist.ClassPool;
import org.apache.ibatis.javassist.CtClass;
import org.apache.ibatis.javassist.CtField;
import org.apache.ibatis.javassist.CtMethod;
import org.apache.ibatis.javassist.CtNewConstructor;
import org.apache.ibatis.javassist.CtNewMethod;
import org.apache.ibatis.javassist.LoaderClassPath;
import org.apache.ibatis.javassist.NotFoundException;
import org.apache.ibatis.ognl.ASTAnd;
import org.apache.ibatis.ognl.ASTChain;
import org.apache.ibatis.ognl.ASTConst;
import org.apache.ibatis.ognl.ASTCtor;
import org.apache.ibatis.ognl.ASTList;
import org.apache.ibatis.ognl.ASTMethod;
import org.apache.ibatis.ognl.ASTOr;
import org.apache.ibatis.ognl.ASTProperty;
import org.apache.ibatis.ognl.ASTRootVarRef;
import org.apache.ibatis.ognl.ASTStaticField;
import org.apache.ibatis.ognl.ASTStaticMethod;
import org.apache.ibatis.ognl.ASTVarRef;
import org.apache.ibatis.ognl.ClassResolver;
import org.apache.ibatis.ognl.ExpressionNode;
import org.apache.ibatis.ognl.Node;
import org.apache.ibatis.ognl.OgnlContext;
import org.apache.ibatis.ognl.OgnlRuntime;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Responsible for managing/providing functionality related to compiling generated java source
* expressions via bytecode enhancements for a given ognl expression.
*/
public class ExpressionCompiler implements OgnlExpressionCompiler {
/**
* Key used to store any java source string casting statements in the {@link OgnlContext} during
* class compilation.
*/
public static final String PRE_CAST = "_preCast";
/**
* {@link ClassLoader} instances.
*/
protected Map loaders = new HashMap<>();
/**
* Javassist class definition pool.
*/
protected ClassPool classPool;
protected int classCounter = 0;
/**
* Default constructor, does nothing.
*/
public ExpressionCompiler() {
}
/**
* Used by {@link #castExpression(OgnlContext, Node, String)} to store the cast java
* source string in to the current {@link OgnlContext}. This will either add to the existing
* string present if it already exists or create a new instance and store it using the static key
* of {@link #PRE_CAST}.
*
* @param context The current execution context.
* @param cast The java source string to store in to the context.
*/
public static void addCastString(OgnlContext context, String cast) {
String value = (String) context.get(PRE_CAST);
if (value != null)
value = cast + value;
else
value = cast;
context.put(PRE_CAST, value);
}
/**
* Returns the appropriate casting expression (minus parens) for the specified class type.
*
*
* For instance, if given an {@link Integer} object the string "java.lang.Integer"
* would be returned. For an array of primitive ints "int[]"
and so on..
*
*
* @param type The class to cast a string expression for.
* @return The converted raw string version of the class name.
*/
public static String getCastString(Class> type) {
if (type == null)
return null;
return type.isArray() ? type.getComponentType().getName() + "[]" : type.getName();
}
/**
* Convenience method called by many different property/method resolving AST types to get a root expression
* resolving string for the given node. The callers are mostly ignorant and rely on this method to properly
* determine if the expression should be cast at all and take the appropriate actions if it should.
*
* @param expression The node to check and generate a root expression to if necessary.
* @param root The root object for this execution.
* @param context The current execution context.
* @return Either an empty string or a root path java source string compatible with javassist compilations
* from the root object up to the specified {@link Node}.
*/
public static String getRootExpression(Node expression, Object root, OgnlContext context) {
String rootExpr = "";
if (!shouldCast(expression))
return rootExpr;
if ((!(expression instanceof ASTList)
&& !(expression instanceof ASTVarRef)
&& !(expression instanceof ASTStaticMethod)
&& !(expression instanceof ASTStaticField)
&& !(expression instanceof ASTConst)
&& !(expression instanceof ExpressionNode)
&& !(expression instanceof ASTCtor)
&& root != null)
||
(root != null && expression instanceof ASTRootVarRef)) {
Class> castClass = OgnlRuntime.getCompiler().getRootExpressionClass(expression, context);
if (castClass.isArray() || expression instanceof ASTRootVarRef) {
rootExpr = "((" + getCastString(castClass) + ")$2)";
if (expression instanceof ASTProperty && !((ASTProperty) expression).isIndexedAccess()) {
rootExpr += ".";
}
} else if ((expression instanceof ASTProperty && ((ASTProperty) expression).isIndexedAccess()) || expression instanceof ASTChain) {
rootExpr = "((" + getCastString(castClass) + ")$2)";
} else {
rootExpr = "((" + getCastString(castClass) + ")$2).";
}
}
return rootExpr;
}
/**
* Used by {@link #getRootExpression(Node, Object, OgnlContext)} to determine if the expression
* needs to be cast at all.
*
* @param expression The node to check against.
* @return Yes if the node type should be cast - false otherwise.
*/
public static boolean shouldCast(Node expression) {
if (expression instanceof ASTChain) {
Node child = expression.jjtGetChild(0);
if (child instanceof ASTConst
|| child instanceof ASTStaticMethod
|| child instanceof ASTStaticField
|| (child instanceof ASTVarRef && !(child instanceof ASTRootVarRef)))
return false;
}
return !(expression instanceof ASTConst);
}
public String castExpression(OgnlContext context, Node expression, String body) {
// ok - so this looks really f-ed up ...and it is ..eh if you can do it better I'm all for it :)
if (context.getCurrentAccessor() == null
|| context.getPreviousType() == null
|| context.getCurrentAccessor().isAssignableFrom(context.getPreviousType())
|| (context.getCurrentType() != null
&& context.getCurrentObject() != null
&& context.getCurrentType().isAssignableFrom(context.getCurrentObject().getClass())
&& context.getCurrentAccessor().isAssignableFrom(context.getPreviousType()))
|| body == null || body.trim().length() < 1
|| (context.getCurrentType() != null && context.getCurrentType().isArray()
&& (context.getPreviousType() == null || context.getPreviousType() != Object.class))
|| expression instanceof ASTOr
|| expression instanceof ASTAnd
|| expression instanceof ASTRootVarRef
|| context.getCurrentAccessor() == Class.class
|| (context.get(ExpressionCompiler.PRE_CAST) != null && ((String) context.get(ExpressionCompiler.PRE_CAST)).startsWith("new"))
|| expression instanceof ASTStaticField
|| expression instanceof ASTStaticMethod
|| (expression instanceof OrderedReturn && ((OrderedReturn) expression).getLastExpression() != null))
return body;
ExpressionCompiler.addCastString(context, "((" + ExpressionCompiler.getCastString(context.getCurrentAccessor()) + ")");
return ")" + body;
}
public String getClassName(Class> clazz) {
if (clazz.getName().equals("java.util.AbstractList$Itr"))
return Iterator.class.getName();
if (Modifier.isPublic(clazz.getModifiers()) && clazz.isInterface())
return clazz.getName();
return getClassName(clazz, clazz.getInterfaces());
}
private String getClassName(Class> clazz, Class>[] interfaces) {
for (Class> anInterface : interfaces) {
if (anInterface.getName().indexOf("util.List") > 0) {
return anInterface.getName();
} else if (anInterface.getName().indexOf("Iterator") > 0) {
return anInterface.getName();
}
}
final Class> superClazz = clazz.getSuperclass();
if (superClazz != null) {
final Class>[] superClazzInterfaces = superClazz.getInterfaces();
if (superClazzInterfaces.length > 0)
return getClassName(superClazz, superClazzInterfaces);
}
return clazz.getName();
}
public Class> getSuperOrInterfaceClass(Method method, Class> clazz) {
Class>[] interfaces = clazz.getInterfaces();
if (interfaces.length > 0) {
Class> intClass;
for (Class> anInterface : interfaces) {
intClass = getSuperOrInterfaceClass(method, anInterface);
if (intClass != null) {
return intClass;
}
if (Modifier.isPublic(anInterface.getModifiers()) && containsMethod(method, anInterface)) {
return anInterface;
}
}
}
if (clazz.getSuperclass() != null) {
Class> superClass = getSuperOrInterfaceClass(method, clazz.getSuperclass());
if (superClass != null)
return superClass;
}
if (Modifier.isPublic(clazz.getModifiers()) && containsMethod(method, clazz))
return clazz;
return null;
}
/**
* Helper utility method used by compiler to help resolve class->method mappings
* during method calls to {@link OgnlExpressionCompiler#getSuperOrInterfaceClass(java.lang.reflect.Method, Class)}.
*
* @param method The method to check for existance of.
* @param clazz The class to check for the existance of a matching method definition to the method passed in.
* @return True if the class contains the specified method, false otherwise.
*/
public boolean containsMethod(Method method, Class> clazz) {
Method[] methods = clazz.getMethods();
for (Method value : methods) {
if (value.getName().equals(method.getName()) && value.getReturnType() == method.getReturnType()) {
Class>[] parms = method.getParameterTypes();
Class>[] methodParams = value.getParameterTypes();
if (methodParams.length != parms.length)
continue;
boolean parmsMatch = true;
for (int p = 0; p < parms.length; p++) {
if (parms[p] != methodParams[p]) {
parmsMatch = false;
break;
}
}
if (!parmsMatch)
continue;
Class>[] exceptions = method.getExceptionTypes();
Class>[] methodExceptions = value.getExceptionTypes();
if (methodExceptions.length != exceptions.length) {
continue;
}
boolean exceptionsMatch = true;
for (int e = 0; e < exceptions.length; e++) {
if (exceptions[e] != methodExceptions[e]) {
exceptionsMatch = false;
break;
}
}
if (!exceptionsMatch)
continue;
return true;
}
}
return false;
}
public Class> getInterfaceClass(Class> clazz) {
if (clazz.getName().equals("java.util.AbstractList$Itr"))
return Iterator.class;
if (Modifier.isPublic(clazz.getModifiers()) && clazz.isInterface() || clazz.isPrimitive()) {
return clazz;
}
return getInterfaceClass(clazz, clazz.getInterfaces());
}
private Class> getInterfaceClass(Class> clazz, Class>[] interfaces) {
for (Class> anInterface : interfaces) {
if (List.class.isAssignableFrom(anInterface))
return List.class;
else if (Iterator.class.isAssignableFrom(anInterface))
return Iterator.class;
else if (Map.class.isAssignableFrom(anInterface))
return Map.class;
else if (Set.class.isAssignableFrom(anInterface))
return Set.class;
else if (Collection.class.isAssignableFrom(anInterface))
return Collection.class;
}
final Class> superClazz = clazz.getSuperclass();
if (superClazz != null) {
final Class>[] superClazzInterfaces = superClazz.getInterfaces();
if (superClazzInterfaces.length > 0)
return getInterfaceClass(superClazz, superClazzInterfaces);
}
return clazz;
}
public Class> getRootExpressionClass(Node rootNode, OgnlContext context) {
if (context.getRoot() == null) {
return null;
}
Class> ret = context.getRoot().getClass();
if (context.getFirstAccessor() != null && context.getFirstAccessor().isInstance(context.getRoot())) {
ret = context.getFirstAccessor();
}
return ret;
}
public void compileExpression(OgnlContext context, Node expression, Object root) throws Exception {
if (expression.getAccessor() != null) {
return;
}
String getBody, setBody;
EnhancedClassLoader loader = getClassLoader(context);
ClassPool pool = getClassPool(context, loader);
CtClass newClass = pool.makeClass(expression.getClass().getName() + expression.hashCode() + classCounter++ + "Accessor");
newClass.addInterface(getCtClass(ExpressionAccessor.class));
CtClass ognlClass = getCtClass(OgnlContext.class);
CtClass objClass = getCtClass(Object.class);
CtMethod valueGetter = new CtMethod(objClass, "get", new CtClass[]{ognlClass, objClass}, newClass);
CtMethod valueSetter = new CtMethod(CtClass.voidType, "set", new CtClass[]{ognlClass, objClass, objClass}, newClass);
CtField nodeMember = null; // will only be set if uncompilable exception is thrown
CtClass nodeClass = getCtClass(Node.class);
CtMethod setExpression = null;
try {
getBody = generateGetter(context, newClass, pool, valueGetter, expression, root);
} catch (UnsupportedCompilationException uc) {
nodeMember = new CtField(nodeClass, "_node", newClass);
newClass.addField(nodeMember);
getBody = generateOgnlGetter(newClass, valueGetter, nodeMember);
setExpression = CtNewMethod.setter("setExpression", nodeMember);
newClass.addMethod(setExpression);
}
try {
setBody = generateSetter(context, newClass, pool, valueSetter, expression, root);
} catch (UnsupportedCompilationException uc) {
if (nodeMember == null) {
nodeMember = new CtField(nodeClass, "_node", newClass);
newClass.addField(nodeMember);
}
setBody = generateOgnlSetter(newClass, valueSetter, nodeMember);
if (setExpression == null) {
setExpression = CtNewMethod.setter("setExpression", nodeMember);
newClass.addMethod(setExpression);
}
}
try {
newClass.addConstructor(CtNewConstructor.defaultConstructor(newClass));
Class> clazz = instantiateClass(pool, newClass);
newClass.detach();
expression.setAccessor((ExpressionAccessor) clazz.newInstance());
// need to set expression on node if the field was just defined.
if (nodeMember != null) {
expression.getAccessor().setExpression(expression);
}
} catch (Throwable t) {
throw new RuntimeException("Error compiling expression on object " + root
+ " with expression node " + expression + " getter body: " + getBody
+ " setter body: " + setBody, t);
}
}
/**
* Called when newClass
has been fully populated and is ready to be instantiated.
*
* @param pool the javassist ClassPool context
* @param newClass the definition of the new class
* @return The compiled class
* @throws CannotCompileException if thrown by javassist
*/
protected Class> instantiateClass(final ClassPool pool, final CtClass newClass) throws CannotCompileException
{
return pool.toClass(newClass);
}
protected String generateGetter(OgnlContext context, CtClass newClass, ClassPool pool, CtMethod valueGetter, Node expression, Object root) throws Exception {
String pre = "";
String post = "";
String body;
context.setRoot(root);
// the ExpressionAccessor API has to reference the generic Object class for get/set operations, so this sets up that known
// type beforehand
context.remove(PRE_CAST);
// Recursively generate the java source code representation of the top level expression
String getterCode = expression.toGetSourceString(context, root);
if (getterCode == null || getterCode.trim().length() <= 0
&& !ASTVarRef.class.isAssignableFrom(expression.getClass()))
getterCode = "null";
String castExpression = (String) context.get(PRE_CAST);
if (context.getCurrentType() == null
|| context.getCurrentType().isPrimitive()
|| Character.class.isAssignableFrom(context.getCurrentType())
|| Object.class == context.getCurrentType()) {
pre = pre + " ($w) (";
post = post + ")";
}
String rootExpr = !getterCode.equals("null") ? getRootExpression(expression, root, context) : "";
String noRoot = (String) context.remove("_noRoot");
if (noRoot != null) {
rootExpr = "";
}
createLocalReferences(context, pool, newClass, valueGetter.getParameterTypes());
if (expression instanceof OrderedReturn && ((OrderedReturn) expression).getLastExpression() != null) {
body = "{ "
+ (expression instanceof ASTMethod || expression instanceof ASTChain ? rootExpr : "")
+ (castExpression != null ? castExpression : "")
+ ((OrderedReturn) expression).getCoreExpression()
+ " return " + pre + ((OrderedReturn) expression).getLastExpression()
+ post
+ ";}";
} else {
body = "{ return "
+ pre
+ (castExpression != null ? castExpression : "")
+ rootExpr
+ getterCode
+ post
+ ";}";
}
if (body.contains("..")) {
body = body.replaceAll("\\.\\.", ".");
}
valueGetter.setBody(body);
newClass.addMethod(valueGetter);
return body;
}
public String createLocalReference(OgnlContext context, String expression, Class> type) {
String referenceName = "ref" + context.incrementLocalReferenceCounter();
context.addLocalReference(referenceName, new OgnlLocalReference(referenceName, expression, type));
String castString = "";
if (!type.isPrimitive())
castString = "(" + ExpressionCompiler.getCastString(type) + ") ";
return castString + referenceName + "($$)";
}
private void createLocalReferences(OgnlContext context, ClassPool pool, CtClass clazz, CtClass[] params) throws CannotCompileException, NotFoundException {
Map referenceMap = context.getLocalReferences();
if (referenceMap == null || referenceMap.size() < 1) {
return;
}
Iterator it = referenceMap.values().iterator();
while (it.hasNext()) {
LocalReference ref = it.next();
String widener = ref.getType().isPrimitive() ? " " : " ($w) ";
String body = "{";
body += " return " + widener + ref.getExpression() + ";";
body += "}";
if (body.contains("..")) {
body = body.replaceAll("\\.\\.", ".");
}
CtMethod method = new CtMethod(pool.get(getCastString(ref.getType())), ref.getName(), params, clazz);
method.setBody(body);
clazz.addMethod(method);
it.remove();
}
}
protected String generateSetter(OgnlContext context, CtClass newClass, ClassPool pool, CtMethod valueSetter, Node expression, Object root) throws Exception {
if (expression instanceof ExpressionNode || expression instanceof ASTConst) {
throw new UnsupportedCompilationException("Can't compile expression/constant setters.");
}
context.setRoot(root);
context.remove(PRE_CAST);
String body;
String setterCode = expression.toSetSourceString(context, root);
String castExpression = (String) context.get(PRE_CAST);
if (setterCode == null || setterCode.trim().length() < 1)
throw new UnsupportedCompilationException("Can't compile null setter body.");
if (root == null)
throw new UnsupportedCompilationException("Can't compile setters with a null root object.");
String pre = getRootExpression(expression, root, context);
String noRoot = (String) context.remove("_noRoot");
if (noRoot != null)
pre = "";
createLocalReferences(context, pool, newClass, valueSetter.getParameterTypes());
body = "{"
+ (castExpression != null ? castExpression : "")
+ pre
+ setterCode + ";}";
if (body.contains("..")) {
body = body.replaceAll("\\.\\.", ".");
}
valueSetter.setBody(body);
newClass.addMethod(valueSetter);
return body;
}
/**
* Fail safe getter creation when normal compilation fails.
*
* @param clazz The javassist class the new method should be attached to.
* @param valueGetter The method definition the generated code will be contained within.
* @param node The root expression node.
* @return The generated source string for this method, the method will still be
* added via the javassist API either way so this is really a convenience
* for exception reporting / debugging.
* @throws Exception If a javassist error occurs.
*/
protected String generateOgnlGetter(CtClass clazz, CtMethod valueGetter, CtField node) throws Exception {
String body = "return " + node.getName() + ".getValue($1, $2);";
valueGetter.setBody(body);
clazz.addMethod(valueGetter);
return body;
}
/**
* Fail safe setter creation when normal compilation fails.
*
* @param clazz The javassist class the new method should be attached to.
* @param valueSetter The method definition the generated code will be contained within.
* @param node The root expression node.
* @return The generated source string for this method, the method will still be
* added via the javassist API either way so this is really a convenience
* for exception reporting / debugging.
* @throws Exception If a javassist error occurs.
*/
protected String generateOgnlSetter(CtClass clazz, CtMethod valueSetter, CtField node)
throws Exception {
String body = node.getName() + ".setValue($1, $2, $3);";
valueSetter.setBody(body);
clazz.addMethod(valueSetter);
return body;
}
/**
* Creates a {@link ClassLoader} instance compatible with the javassist classloader and normal
* OGNL class resolving semantics.
*
* @param context The current execution context.
* @return The created {@link ClassLoader} instance.
*/
protected EnhancedClassLoader getClassLoader(OgnlContext context) {
EnhancedClassLoader ret = loaders.get(context.getClassResolver());
if (ret != null) {
return ret;
}
ClassLoader classLoader = new ContextClassLoader(OgnlContext.class.getClassLoader(), context);
ret = new EnhancedClassLoader(classLoader);
loaders.put(context.getClassResolver(), ret);
return ret;
}
/**
* Loads a new class definition via javassist for the specified class.
*
* @param searchClass The class to load.
* @return The javassist class equivalent.
* @throws NotFoundException When the class definition can't be found.
*/
protected CtClass getCtClass(Class> searchClass) throws NotFoundException {
return classPool.get(searchClass.getName());
}
/**
* Gets either a new or existing {@link ClassPool} for use in compiling javassist
* classes. A new class path object is inserted in to the returned {@link ClassPool} using
* the passed in loader
instance if a new pool needs to be created.
*
* @param context The current execution context.
* @param loader The {@link ClassLoader} instance to use - as returned by {@link #getClassLoader(OgnlContext)}.
* @return The existing or new {@link ClassPool} instance.
*/
protected ClassPool getClassPool(OgnlContext context, EnhancedClassLoader loader) {
if (classPool != null) {
return classPool;
}
classPool = ClassPool.getDefault();
classPool.insertClassPath(new LoaderClassPath(loader.getParent()));
return classPool;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy