blob: e75eadc49f4f5b2176cc985137e6fc08d19f849b [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE 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.commons.ognl.enhance;
import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.CtMethod;
import javassist.CtNewConstructor;
import javassist.CtNewMethod;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
import org.apache.commons.ognl.ASTAnd;
import org.apache.commons.ognl.ASTChain;
import org.apache.commons.ognl.ASTConst;
import org.apache.commons.ognl.ASTCtor;
import org.apache.commons.ognl.ASTList;
import org.apache.commons.ognl.ASTMethod;
import org.apache.commons.ognl.ASTOr;
import org.apache.commons.ognl.ASTProperty;
import org.apache.commons.ognl.ASTRootVarRef;
import org.apache.commons.ognl.ASTStaticField;
import org.apache.commons.ognl.ASTStaticMethod;
import org.apache.commons.ognl.ASTThisVarRef;
import org.apache.commons.ognl.ASTVarRef;
import org.apache.commons.ognl.ClassResolver;
import org.apache.commons.ognl.ExpressionNode;
import org.apache.commons.ognl.Node;
import org.apache.commons.ognl.OgnlContext;
import org.apache.commons.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;
import static java.lang.String.format;
/**
* 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<ClassResolver, EnhancedClassLoader> loaders = new HashMap<ClassResolver, EnhancedClassLoader>();
/**
* Javassist class definition poool.
*/
protected ClassPool pool;
protected int classCounter = 0;
/**
* Used by {@link #castExpression(org.apache.commons.ognl.OgnlContext, org.apache.commons.ognl.Node, String)} to
* store the cast java source string in to the current {@link org.apache.commons.ognl.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.
* <p/>
* For instance, if given an {@link Integer} object the string <code>"java.lang.Integer"</code> would be returned.
* For an array of primitive ints <code>"int[]"</code> and so on..
* </p>
*
* @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) && !(expression instanceof ASTStaticMethod)
&& root != null ) || ( root != null && expression instanceof ASTRootVarRef) )
{
Class<?> castClass = OgnlRuntime.getCompiler( context ).getRootExpressionClass( expression, context );
if ( castClass.isArray() || expression instanceof ASTRootVarRef || expression instanceof ASTThisVarRef)
{
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(org.apache.commons.ognl.Node, Object, org.apache.commons.ognl.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);
}
/**
* {@inheritDoc}
*/
public String castExpression( OgnlContext context, Node expression, String body )
{
//TODO: 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;
}
/*
* System.out.println("castExpression() with expression " + expression + " expr class: " + expression.getClass()
* + " currentType is: " + context.getCurrentType() + " previousType: " + context.getPreviousType() +
* "\n current Accessor: " + context.getCurrentAccessor() + " previous Accessor: " +
* context.getPreviousAccessor() + " current object " + context.getCurrentObject());
*/
ExpressionCompiler.addCastString( context,
"((" + ExpressionCompiler.getCastString( context.getCurrentAccessor() )
+ ")" );
return ")" + body;
}
/**
* {@inheritDoc}
*/
public String getClassName( Class<?> clazz )
{
if ( "java.util.AbstractList$Itr".equals( clazz.getName() ) )
{
return Iterator.class.getName();
}
if ( Modifier.isPublic( clazz.getModifiers() ) && clazz.isInterface() )
{
return clazz.getName();
}
Class<?>[] interfaces = clazz.getInterfaces();
for ( Class<?> intface : interfaces )
{
if ( intface.getName().indexOf( "util.List" ) > 0 )
{
return intface.getName();
}
if ( intface.getName().indexOf( "Iterator" ) > 0 )
{
return intface.getName();
}
}
if ( clazz.getSuperclass() != null && clazz.getSuperclass().getInterfaces().length > 0 )
{
return getClassName( clazz.getSuperclass() );
}
return clazz.getName();
}
/**
* {@inheritDoc}
*/
public Class<?> getSuperOrInterfaceClass( Method m, Class<?> clazz )
{
if ( clazz.getInterfaces() != null && clazz.getInterfaces().length > 0 )
{
Class<?>[] intfs = clazz.getInterfaces();
Class<?> intClass;
for ( Class<?> intf : intfs )
{
intClass = getSuperOrInterfaceClass( m, intf );
if ( intClass != null )
{
return intClass;
}
if ( Modifier.isPublic( intf.getModifiers() ) && containsMethod( m, intf ) )
{
return intf;
}
}
}
if ( clazz.getSuperclass() != null )
{
Class<?> superClass = getSuperOrInterfaceClass( m, clazz.getSuperclass() );
if ( superClass != null )
{
return superClass;
}
}
if ( Modifier.isPublic( clazz.getModifiers() ) && containsMethod( m, 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 m 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 m, Class<?> clazz )
{
Method[] methods = clazz.getMethods();
if ( methods == null )
{
return false;
}
for ( Method method : methods )
{
if ( method.getName().equals( m.getName() ) && method.getReturnType() == m.getReturnType() )
{
Class<?>[] parms = m.getParameterTypes();
if ( parms == null )
{
continue;
}
Class<?>[] mparms = method.getParameterTypes();
if ( mparms == null || mparms.length != parms.length )
{
continue;
}
boolean parmsMatch = true;
for ( int p = 0; p < parms.length; p++ )
{
if ( parms[p] != mparms[p] )
{
parmsMatch = false;
break;
}
}
if ( !parmsMatch )
{
continue;
}
Class<?>[] exceptions = m.getExceptionTypes();
if ( exceptions == null )
{
continue;
}
Class<?>[] mexceptions = method.getExceptionTypes();
if ( mexceptions == null || mexceptions.length != exceptions.length )
{
continue;
}
boolean exceptionsMatch = true;
for ( int e = 0; e < exceptions.length; e++ )
{
if ( exceptions[e] != mexceptions[e] )
{
exceptionsMatch = false;
break;
}
}
if ( !exceptionsMatch )
{
continue;
}
return true;
}
}
return false;
}
/**
* {@inheritDoc}
*/
public Class<?> getInterfaceClass( Class<?> clazz )
{
if ( "java.util.AbstractList$Itr".equals( clazz.getName() ) )
{
return Iterator.class;
}
if ( Modifier.isPublic( clazz.getModifiers() ) && clazz.isInterface() || clazz.isPrimitive() )
{
return clazz;
}
Class<?>[] intf = clazz.getInterfaces();
for ( Class<?> anIntf : intf )
{
if ( List.class.isAssignableFrom( anIntf ) )
{
return List.class;
}
if ( Iterator.class.isAssignableFrom( anIntf ) )
{
return Iterator.class;
}
if ( Map.class.isAssignableFrom( anIntf ) )
{
return Map.class;
}
if ( Set.class.isAssignableFrom( anIntf ) )
{
return Set.class;
}
if ( Collection.class.isAssignableFrom( anIntf ) )
{
return Collection.class;
}
}
if ( clazz.getSuperclass() != null && clazz.getSuperclass().getInterfaces().length > 0 )
{
return getInterfaceClass( clazz.getSuperclass() );
}
return clazz;
}
/**
* {@inheritDoc}
*/
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;
}
/**
* {@inheritDoc}
*/
public void compileExpression( OgnlContext context, Node expression, Object root )
throws Exception
{
// System.out.println("Compiling expr class " + expression.getClass().getName() + " and root " + root);
if ( expression.getAccessor() != null )
{
return;
}
String getBody, setBody;
EnhancedClassLoader loader = getClassLoader( context );
ClassPool classPool = getClassPool( context, loader );
CtClass newClass = classPool.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, objClass, classPool, 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, objClass, classPool, 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 = classPool.toClass( 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 );
}
}
protected String generateGetter( OgnlContext context, CtClass newClass, CtClass objClass, ClassPool classPool,
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().isEmpty()
&& !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 = !"null".equals( getterCode ) ? getRootExpression( expression, root, context ) : "";
String noRoot = (String) context.remove( "_noRoot" );
if ( noRoot != null )
{
rootExpr = "";
}
createLocalReferences( context, classPool, newClass, objClass, 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
+ ";}";
}
body = body.replaceAll( "\\.\\.", "." );
// System.out.println("Getter Body: ===================================\n" + body);
valueGetter.setBody( body );
newClass.addMethod( valueGetter );
return body;
}
/**
* {@inheritDoc}
*/
public String createLocalReference( OgnlContext context, String expression, Class<?> type )
{
String referenceName = "ref" + context.incrementLocalReferenceCounter();
context.addLocalReference( referenceName, new LocalReferenceImpl( referenceName, expression, type ) );
String castString = "";
if ( !type.isPrimitive() )
{
castString = "(" + ExpressionCompiler.getCastString( type ) + ") ";
}
return castString + referenceName + "($$)";
}
void createLocalReferences( OgnlContext context, ClassPool classPool, CtClass clazz, CtClass unused,
CtClass[] params )
throws NotFoundException, CannotCompileException
{
Map<String, LocalReference> referenceMap = context.getLocalReferences();
if ( referenceMap == null || referenceMap.isEmpty() )
{
return;
}
Iterator<LocalReference> it = referenceMap.values().iterator();
while( it.hasNext() )
{
LocalReference ref = it.next();
String widener = ref.getType().isPrimitive() ? " " : " ($w) ";
String body = format( "{ return %s %s; }", widener, ref.getExpression() ).replaceAll( "\\.\\.", "." );
// System.out.println("adding method " + ref.getName() + " with body:\n" + body + " and return type: " +
// ref.getType());
CtMethod method =
new CtMethod( classPool.get( getCastString( ref.getType() ) ), ref.getName(), params, clazz );
method.setBody( body );
clazz.addMethod( method );
it.remove();
}
}
protected String generateSetter( OgnlContext context, CtClass newClass, CtClass objClass, ClassPool classPool,
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, classPool, newClass, objClass, valueSetter.getParameterTypes() );
body = "{" + ( castExpression != null ? castExpression : "" ) + pre + setterCode + ";}";
body = body.replaceAll( "\\.\\.", "." );
// System.out.println("Setter Body: ===================================\n" + body);
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 javassist.NotFoundException When the class definition can't be found.
*/
protected CtClass getCtClass( Class<?> searchClass )
throws NotFoundException
{
return pool.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 <code>loader</code> 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(org.apache.commons.ognl.OgnlContext)}.
* @return The existing or new {@link ClassPool} instance.
*/
protected ClassPool getClassPool( OgnlContext context, EnhancedClassLoader loader )
{
if ( pool != null )
{
return pool;
}
pool = ClassPool.getDefault();
pool.insertClassPath( new LoaderClassPath( loader.getParent() ) );
return pool;
}
}