/**************************************************************************************
* Copyright (C) 2008 EsperTech, Inc. All rights reserved. *
* http://esper.codehaus.org *
* http://www.espertech.com *
* ---------------------------------------------------------------------------------- *
* The software in this package is published under the terms of the GPL license *
* a copy of which has been included with this distribution in the license.txt file. *
**************************************************************************************/
package com.espertech.esper.epl.core;
import com.espertech.esper.client.ConfigurationMethodRef;
import com.espertech.esper.client.ConfigurationPlugInAggregationFunction;
import com.espertech.esper.client.ConfigurationPlugInSingleRowFunction;
import com.espertech.esper.client.hook.AggregationFunctionFactory;
import com.espertech.esper.collection.Pair;
import com.espertech.esper.epl.agg.service.AggregationSupport;
import com.espertech.esper.epl.expression.*;
import com.espertech.esper.util.JavaClassHelper;
import com.espertech.esper.util.MethodResolver;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Implementation for engine-level imports.
*/
public class EngineImportServiceImpl implements EngineImportService
{
private static final Log log = LogFactory.getLog(EngineImportServiceImpl.class);
private final List<String> imports;
private final Map<String, ConfigurationPlugInAggregationFunction> aggregationFunctions;
private final Map<String, EngineImportSingleRowDesc> singleRowFunctions;
private final Map<String, ConfigurationMethodRef> methodInvocationRef;
private final boolean allowExtendedAggregationFunc;
private final boolean isUdfCache;
private final boolean isDuckType;
/**
* Ctor
* @param allowExtendedAggregationFunc true to allow non-SQL standard builtin agg functions.
*/
public EngineImportServiceImpl(boolean allowExtendedAggregationFunc, boolean isUdfCache, boolean isDuckType)
{
imports = new ArrayList<String>();
aggregationFunctions = new HashMap<String, ConfigurationPlugInAggregationFunction>();
singleRowFunctions = new HashMap<String, EngineImportSingleRowDesc>();
methodInvocationRef = new HashMap<String, ConfigurationMethodRef>();
this.allowExtendedAggregationFunc = allowExtendedAggregationFunc;
this.isUdfCache = isUdfCache;
this.isDuckType = isDuckType;
}
public boolean isUdfCache() {
return isUdfCache;
}
public boolean isDuckType() {
return isDuckType;
}
public ConfigurationMethodRef getConfigurationMethodRef(String className)
{
return methodInvocationRef.get(className);
}
/**
* Adds cache configs for method invocations for from-clause.
* @param configs cache configs
*/
public void addMethodRefs(Map<String, ConfigurationMethodRef> configs)
{
methodInvocationRef.putAll(configs);
}
public void addImport(String importName) throws EngineImportException
{
if(!isClassName(importName) && !isPackageName(importName))
{
throw new EngineImportException("Invalid import name '" + importName + "'");
}
if (log.isDebugEnabled()) {
log.debug("Adding import " + importName);
}
imports.add(importName);
}
public void addAggregation(String functionName, ConfigurationPlugInAggregationFunction aggregationDesc) throws EngineImportException
{
ConfigurationPlugInAggregationFunction existing = aggregationFunctions.get(functionName);
if (existing != null)
{
throw new EngineImportException("Aggregation function by name '" + functionName + "' is already defined");
}
if (singleRowFunctions.containsKey(functionName))
{
throw new EngineImportException("Single-row function by name '" + functionName + "' is already defined");
}
if(!isFunctionName(functionName))
{
throw new EngineImportException("Invalid aggregation function name '" + functionName + "'");
}
if (aggregationDesc.getFactoryClassName() != null) {
if(!isClassName(aggregationDesc.getFactoryClassName()))
{
throw new EngineImportException("Invalid class name for aggregation factory '" + aggregationDesc.getFactoryClassName() + "'");
}
}
else {
if(!isClassName(aggregationDesc.getFunctionClassName()))
{
throw new EngineImportException("Invalid class name for aggregation function '" + aggregationDesc.getFunctionClassName() + "'");
}
}
aggregationFunctions.put(functionName.toLowerCase(), aggregationDesc);
}
public void addSingleRow(String functionName, String singleRowFuncClass, String methodName, ConfigurationPlugInSingleRowFunction.ValueCache valueCache, ConfigurationPlugInSingleRowFunction.FilterOptimizable filterOptimizable, boolean rethrowExceptions) throws EngineImportException {
EngineImportSingleRowDesc existing = singleRowFunctions.get(functionName);
if (existing != null)
{
throw new EngineImportException("Single-Row function by name '" + functionName + "' is already defined");
}
if (aggregationFunctions.containsKey(functionName))
{
throw new EngineImportException("Aggregation function by name '" + functionName + "' is already defined");
}
if(!isFunctionName(functionName))
{
throw new EngineImportException("Invalid single-row function name '" + functionName + "'");
}
if(!isClassName(singleRowFuncClass))
{
throw new EngineImportException("Invalid class name for aggregation '" + singleRowFuncClass + "'");
}
singleRowFunctions.put(functionName.toLowerCase(), new EngineImportSingleRowDesc(singleRowFuncClass, methodName, valueCache, filterOptimizable, rethrowExceptions));
}
public AggregationSupport resolveAggregation(String name) throws EngineImportException, EngineImportUndefinedException
{
ConfigurationPlugInAggregationFunction desc = aggregationFunctions.get(name);
if (desc == null)
{
desc = aggregationFunctions.get(name.toLowerCase());
}
if (desc == null || desc.getFunctionClassName() == null)
{
throw new EngineImportUndefinedException("A function named '" + name + "' is not defined");
}
String className = desc.getFunctionClassName();
Class clazz;
try
{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
clazz = Class.forName(className, true, cl);
}
catch (ClassNotFoundException ex)
{
throw new EngineImportException("Could not load aggregation class by name '" + className + "'", ex);
}
Object object;
try
{
object = clazz.newInstance();
}
catch (InstantiationException e)
{
throw new EngineImportException("Error instantiating aggregation class by name '" + className + "'", e);
}
catch (IllegalAccessException e)
{
throw new EngineImportException("Illegal access instatiating aggregation function class by name '" + className + "'", e);
}
if (!(object instanceof AggregationSupport))
{
throw new EngineImportException("Aggregation class by name '" + className + "' does not subclass AggregationSupport");
}
return (AggregationSupport) object;
}
public AggregationFunctionFactory resolveAggregationFactory(String name) throws EngineImportUndefinedException, EngineImportException {
ConfigurationPlugInAggregationFunction desc = aggregationFunctions.get(name);
if (desc == null)
{
desc = aggregationFunctions.get(name.toLowerCase());
}
if (desc == null || desc.getFactoryClassName() == null)
{
throw new EngineImportUndefinedException("A function named '" + name + "' is not defined");
}
String className = desc.getFactoryClassName();
Class clazz;
try
{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
clazz = Class.forName(className, true, cl);
}
catch (ClassNotFoundException ex)
{
throw new EngineImportException("Could not load aggregation class by name '" + className + "'", ex);
}
Object object;
try
{
object = clazz.newInstance();
}
catch (InstantiationException e)
{
throw new EngineImportException("Error instantiating aggregation class by name '" + className + "'", e);
}
catch (IllegalAccessException e)
{
throw new EngineImportException("Illegal access instatiating aggregation function class by name '" + className + "'", e);
}
if (!(object instanceof AggregationFunctionFactory))
{
throw new EngineImportException("Aggregation class by name '" + className + "' does not implement AggregationFunctionFactory");
}
return (AggregationFunctionFactory) object;
}
public Pair<Class, EngineImportSingleRowDesc> resolveSingleRow(String name) throws EngineImportException, EngineImportUndefinedException
{
EngineImportSingleRowDesc pair = singleRowFunctions.get(name);
if (pair == null)
{
pair = singleRowFunctions.get(name.toLowerCase());
}
if (pair == null)
{
throw new EngineImportUndefinedException("A function named '" + name + "' is not defined");
}
Class clazz;
try
{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
clazz = Class.forName(pair.getClassName(), true, cl);
}
catch (ClassNotFoundException ex)
{
throw new EngineImportException("Could not load single-row function class by name '" + pair.getClassName() + "'", ex);
}
return new Pair<Class, EngineImportSingleRowDesc>(clazz, pair);
}
public Method resolveMethod(String className, String methodName, Class[] paramTypes)
throws EngineImportException
{
Class clazz;
try
{
clazz = resolveClassInternal(className, false);
}
catch (ClassNotFoundException e)
{
throw new EngineImportException("Could not load class by name '" + className + "', please check imports", e);
}
try
{
return MethodResolver.resolveMethod(clazz, methodName, paramTypes, false);
}
catch (EngineNoSuchMethodException e)
{
throw convert(clazz, methodName, paramTypes, e, false);
}
}
public Constructor resolveCtor(Class clazz, Class[] paramTypes) throws EngineImportException {
try
{
return MethodResolver.resolveCtor(clazz, paramTypes);
}
catch (EngineNoSuchCtorException e)
{
throw convert(clazz, paramTypes, e);
}
}
public Method resolveMethod(String className, String methodName)
throws EngineImportException
{
Class clazz;
try
{
clazz = resolveClassInternal(className, false);
}
catch (ClassNotFoundException e)
{
throw new EngineImportException("Could not load class by name '" + className + "', please check imports", e);
}
Method methods[] = clazz.getMethods();
Method methodByName = null;
// check each method by name
for (Method method : methods)
{
if (method.getName().equals(methodName))
{
if (methodByName != null)
{
throw new EngineImportException("Ambiguous method name: method by name '" + methodName + "' is overloaded in class '" + className + "'");
}
int modifiers = method.getModifiers();
if (Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers))
{
methodByName = method;
}
}
}
if (methodByName == null)
{
throw new EngineImportException("Could not find static method named '" + methodName + "' in class '" + className + "'");
}
return methodByName;
}
public Class resolveClass(String className)
throws EngineImportException
{
Class clazz;
try
{
clazz = resolveClassInternal(className, false);
}
catch (ClassNotFoundException e)
{
throw new EngineImportException("Could not load class by name '" + className + "', please check imports", e);
}
return clazz;
}
public Class resolveAnnotation(String className) throws EngineImportException {
Class clazz;
try
{
clazz = resolveClassInternal(className, true);
}
catch (ClassNotFoundException e)
{
throw new EngineImportException("Could not load annotation class by name '" + className + "', please check imports", e);
}
return clazz;
}
/**
* Finds a class by class name using the auto-import information provided.
* @param className is the class name to find
* @return class
* @throws ClassNotFoundException if the class cannot be loaded
*/
protected Class resolveClassInternal(String className, boolean requireAnnotation) throws ClassNotFoundException
{
// Attempt to retrieve the class with the name as-is
try
{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return Class.forName(className, true, cl);
}
catch(ClassNotFoundException e)
{
if (log.isDebugEnabled())
{
log.debug("Class not found for resolving from name as-is '" + className + "'");
}
}
// Try all the imports
for(String importName : imports)
{
boolean isClassName = isClassName(importName);
// Import is a class name
if(isClassName)
{
if(importName.endsWith(className))
{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return Class.forName(importName, true, cl);
}
String prefixedClassName = importName + '$' + className;
try
{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class clazz = Class.forName(prefixedClassName, true, cl);
if (!requireAnnotation || clazz.isAnnotation()) {
return clazz;
}
}
catch(ClassNotFoundException e){
if (log.isDebugEnabled())
{
log.debug("Class not found for resolving from name '" + prefixedClassName + "'");
}
}
}
else
{
// Import is a package name
String prefixedClassName = getPackageName(importName) + '.' + className;
try
{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class clazz = Class.forName(prefixedClassName, true, cl);
if (!requireAnnotation || clazz.isAnnotation()) {
return clazz;
}
}
catch(ClassNotFoundException e){
if (log.isDebugEnabled())
{
log.debug("Class not found for resolving from name '" + prefixedClassName + "'");
}
}
}
}
// try to resolve from method references
for (String name : methodInvocationRef.keySet())
{
if (JavaClassHelper.isSimpleNameFullyQualfied(className, name))
{
try
{
ClassLoader cl = Thread.currentThread().getContextClassLoader();
Class clazz = Class.forName(name, true, cl);
if (!requireAnnotation || clazz.isAnnotation()) {
return clazz;
}
}
catch (ClassNotFoundException e1)
{
if (log.isDebugEnabled())
{
log.debug("Class not found for resolving from method invocation ref:" + name);
}
}
}
}
// No import worked, the class isn't resolved
throw new ClassNotFoundException("Unknown class " + className);
}
public Method resolveMethod(Class clazz, String methodName, Class[] paramTypes)
throws EngineImportException
{
try
{
return MethodResolver.resolveMethod(clazz, methodName, paramTypes, true);
}
catch (EngineNoSuchMethodException e)
{
throw convert(clazz, methodName, paramTypes, e, true);
}
}
private EngineImportException convert(Class clazz, String methodName, Class[] paramTypes, EngineNoSuchMethodException e, boolean isInstance)
{
String expected = JavaClassHelper.getParameterAsString(paramTypes);
String message = "Could not find ";
if (!isInstance) {
message += "static ";
}
else {
message += "enumeration method, date-time method or instance ";
}
if (paramTypes.length > 0)
{
message += "method named '" + methodName + "' in class '" + JavaClassHelper.getClassNameFullyQualPretty(clazz) + "' with matching parameter number and expected parameter type(s) '" + expected + "'";
}
else
{
message += "method named '" + methodName + "' in class '" + JavaClassHelper.getClassNameFullyQualPretty(clazz) + "' taking no parameters";
}
if (e.getNearestMissMethod() != null)
{
message += " (nearest match found was '" + e.getNearestMissMethod().getName();
if (e.getNearestMissMethod().getParameterTypes().length == 0) {
message += "' taking no parameters";
}
else {
message += "' taking type(s) '" + JavaClassHelper.getParameterAsString(e.getNearestMissMethod().getParameterTypes()) + "'";
}
message += ")";
}
return new EngineImportException(message, e);
}
private EngineImportException convert(Class clazz, Class[] paramTypes, EngineNoSuchCtorException e)
{
String expected = JavaClassHelper.getParameterAsString(paramTypes);
String message = "Could not find constructor ";
if (paramTypes.length > 0)
{
message += "in class '" + JavaClassHelper.getClassNameFullyQualPretty(clazz) + "' with matching parameter number and expected parameter type(s) '" + expected + "'";
}
else
{
message += "in class '" + JavaClassHelper.getClassNameFullyQualPretty(clazz) + "' taking no parameters";
}
if (e.getNearestMissCtor() != null)
{
message += " (nearest matching constructor ";
if (e.getNearestMissCtor().getParameterTypes().length == 0) {
message += "taking no parameters";
}
else {
message += "taking type(s) '" + JavaClassHelper.getParameterAsString(e.getNearestMissCtor().getParameterTypes()) + "'";
}
message += ")";
}
return new EngineImportException(message, e);
}
public ExprNode resolveAggExtendedBuiltin(String name, boolean isDistinct) {
if (!allowExtendedAggregationFunc) {
return null;
}
if (name.toLowerCase().equals("firstever"))
{
return new ExprFirstEverNode(isDistinct);
}
if (name.toLowerCase().equals("lastever"))
{
return new ExprLastEverNode(isDistinct);
}
if (name.toLowerCase().equals("rate"))
{
return new ExprRateAggNode(isDistinct);
}
if (name.toLowerCase().equals("nth"))
{
return new ExprNthAggNode(isDistinct);
}
if (name.toLowerCase().equals("leaving"))
{
return new ExprLeavingAggNode(isDistinct);
}
return null;
}
/**
* For testing, returns imports.
* @return returns auto-import list as array
*/
protected String[] getImports()
{
return imports.toArray(new String[imports.size()]);
}
private static boolean isFunctionName(String functionName)
{
String classNameRegEx = "\\w+";
return functionName.matches(classNameRegEx);
}
private static boolean isClassName(String importName)
{
String classNameRegEx = "(\\w+\\.)*\\w+(\\$\\w+)?";
return importName.matches(classNameRegEx);
}
private static boolean isPackageName(String importName)
{
String classNameRegEx = "(\\w+\\.)+\\*";
return importName.matches(classNameRegEx);
}
// Strip off the final ".*"
private static String getPackageName(String importName)
{
return importName.substring(0, importName.length() - 2);
}
}