net.sf.jasperreports.compilers.GroovyEvaluator Maven / Gradle / Ivy
/*
* JasperReports - Free Java Reporting Library.
* Copyright (C) 2001 - 2023 Cloud Software Group, Inc. All rights reserved.
* http://www.jaspersoft.com
*
* Unless you have purchased a commercial license agreement from Jaspersoft,
* the following license terms apply:
*
* This program is part of JasperReports.
*
* JasperReports is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* JasperReports is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with JasperReports. If not, see .
*/
package net.sf.jasperreports.compilers;
import groovy.lang.ExpandoMetaClass;
import groovy.lang.GroovyObject;
import groovy.lang.GroovyRuntimeException;
import groovy.lang.MetaClass;
import groovy.lang.MetaMethod;
import groovy.lang.MissingMethodException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sf.jasperreports.engine.JRExpression;
import net.sf.jasperreports.engine.JRRuntimeException;
import net.sf.jasperreports.engine.JasperReportsContext;
import net.sf.jasperreports.engine.fill.JREvaluator;
import net.sf.jasperreports.engine.fill.JRExpressionEvalException;
import net.sf.jasperreports.engine.fill.JasperReportsContextAware;
import net.sf.jasperreports.functions.FunctionSupport;
import net.sf.jasperreports.functions.FunctionsUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.MethodClosure;
import org.codehaus.groovy.runtime.metaclass.ClosureMetaMethod;
/**
* Groovy expression evaluator that compiles expressions at fill time.
*
* @author Lucian Chirita ([email protected])
* @see JRGroovyCompiler
*/
public abstract class GroovyEvaluator extends JREvaluator implements JasperReportsContextAware
{
private static final Log log = LogFactory.getLog(GroovyEvaluator.class);
public static final String EXCEPTION_MESSAGE_KEY_FUNCTION_NOT_FOUND = "compilers.groovy.function.not.found";
//copied from groovy.lang.MetaClassImpl.chooseMostSpecificParams()
private static final Pattern GROOVY_EXCEPTION_PATTERN_AMBIGUOUS_NULL =
Pattern.compile("Ambiguous method overloading for method.*Cannot resolve which method to invoke for \\[null\\] due to overlapping prototypes between.*",
Pattern.DOTALL);
private FunctionsUtil functionsUtil;
private List functionMethods = new ArrayList<>();
@Override
public void setJasperReportsContext(JasperReportsContext context)
{
this.functionsUtil = FunctionsUtil.getInstance(context);
}
@Override
protected Object handleEvaluationException(JRExpression expression, Throwable e) throws JRExpressionEvalException
{
if (ignoreNPE && e instanceof GroovyRuntimeException && e.getMessage() != null)
{
//in Groovy 2.0.1, 1 + null (and other e.g. BigDecimal * null) was throwing NPE
//in 2.4.3, it fails with "Ambiguous method overloading..."
//we're catching this exception (by message) and treating it like a NPE
Matcher matcher = GROOVY_EXCEPTION_PATTERN_AMBIGUOUS_NULL.matcher(e.getMessage());
if (matcher.matches())
{
//evaluating the expression to null to match Groovy 2.0.1 behaviour
return null;
}
}
//throw the exception
return super.handleEvaluationException(expression, e);
}
protected Object functionCall(String methodName, Object[] args)
{
Method functionMethod = functionsUtil.getMethod4Function(methodName);
if (functionMethod == null)
{
throw
new JRRuntimeException(
EXCEPTION_MESSAGE_KEY_FUNCTION_NOT_FOUND,
new Object[]{methodName});
}
// we're relying on this, if we'll ever resolve functions to methods
// that have different names we need to adapt the code
assert functionMethod.getName().equals(methodName);
Class> functionClass = functionMethod.getDeclaringClass();
MetaClass functionMetaClass = DefaultGroovyMethods.getMetaClass(functionClass);
MethodClosure functionMethodClosure = null;
if (FunctionSupport.class.isAssignableFrom(functionClass))
{
// search for an instance method that applies to the arguments
MetaMethod metaMethod = functionMetaClass.getMetaMethod(methodName, args);
if (metaMethod != null && metaMethod.isPublic())
{
@SuppressWarnings("unchecked")
FunctionSupport functionObject = getFunctionSupport((Class extends FunctionSupport>) functionClass);
functionMethodClosure = new MethodClosure(functionObject, methodName);
if (log.isDebugEnabled())
{
log.debug("found public instance method " + metaMethod + " in class " + functionClass);
}
}
}
if (functionMethodClosure == null)
{
// searching for a static method that applies to the arguments
MetaMethod metaMethod = functionMetaClass.getStaticMetaMethod(methodName, args);
if (metaMethod != null && metaMethod.isPublic())
{
// creating a static method closure
functionMethodClosure = new MethodClosure(functionClass, methodName);
if (log.isDebugEnabled())
{
log.debug("found public static method " + metaMethod + " in class " + functionClass);
}
}
}
if (functionMethodClosure == null)
{
// we didn't find a public instance/static method that applies to the arguments
throw new MissingMethodException(methodName, functionMetaClass.getTheClass(), args);
}
// adding the function methods for the name to the list of registered methods.
// we need to add all the methods with the same name in the beginning because once we add one method
// methodMissing might no longer be called.
// we need to reregister all methods on each new method since we create ExpandoMetaClass from scratch.
addFunctionClosureMethods(functionMethodClosure, methodName);
// adding the methods to the evaluator MetaClass so that it doesn't go again into methodMissing
ExpandoMetaClass extendedMetaClass = new ExpandoMetaClass(getClass(), false);
registerMethods(extendedMetaClass);
extendedMetaClass.initialize();
DefaultGroovyMethods.setMetaClass((GroovyObject) this, extendedMetaClass);
// returning what we have to return
return functionMethodClosure.call(args);
}
protected void addFunctionClosureMethods(MethodClosure methodClosure, String functionName)
{
// calling registerInstanceMethod(String, Closure) would register all methods, but we only want public methods
List closureMethods = ClosureMetaMethod.createMethodList(functionName, getClass(), methodClosure);
for (MetaMethod metaMethod : closureMethods)
{
if (!(metaMethod instanceof ClosureMetaMethod))
{
// should not happen
log.warn("Got unexpected closure method " + metaMethod + " of type " + metaMethod.getClass().getName());
continue;
}
ClosureMetaMethod closureMethod = (ClosureMetaMethod) metaMethod;
if (!closureMethod.getDoCall().isPublic())
{
if (log.isDebugEnabled())
{
log.debug("method " + closureMethod.getDoCall() + " is not public, not registering");
}
continue;
}
if (log.isDebugEnabled())
{
log.debug("creating closure method for " + closureMethod.getDoCall());
}
functionMethods.add(closureMethod);
}
}
protected void registerMethods(ExpandoMetaClass extendedMetaClass)
{
for (ClosureMetaMethod closureMethod : functionMethods)
{
extendedMetaClass.registerInstanceMethod(closureMethod);
}
}
}