/*
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program 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.
*
* Copyright (c) 2001 - 2013 Object Refinery Ltd, Pentaho Corporation and Contributors.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.core.modules.misc.datafactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.LinkedHashSet;
import javax.swing.table.TableModel;
import org.pentaho.reporting.engine.classic.core.AbstractDataFactory;
import org.pentaho.reporting.engine.classic.core.DataFactory;
import org.pentaho.reporting.engine.classic.core.DataRow;
import org.pentaho.reporting.engine.classic.core.ReportDataFactoryException;
import org.pentaho.reporting.libraries.base.util.CSVTokenizer;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
/**
* This report data factory uses introspection to search for a report data source. The query can have the following
* formats:
* <p/>
* <full-qualified-classname>#methodName(Parameters) <full-qualified-classname>(constructorparams)#methodName(Parameters)
* <full-qualified-classname>(constructorparams)
*
* @author Thomas Morgner
*/
public class StaticDataFactory extends AbstractDataFactory
{
private static final String[] EMPTY_NAMES = new String[0];
private static final String[] EMPTY_PARAMS = EMPTY_NAMES;
/**
* DefaultConstructor.
*/
public StaticDataFactory()
{
}
/**
* Checks whether the query would be executable by this datafactory. This performs a rough check, not a full query.
*
* @param query
* @param parameters
* @return
*/
public boolean isQueryExecutable(final String query, final DataRow parameters)
{
return true;
}
/**
* Queries a datasource. The string 'query' defines the name of the query. The Parameterset given here may contain
* more data than actually needed.
* <p/>
* The dataset may change between two calls, do not assume anything!
*
* @param query the method call.
* @param parameters the set of parameters.
* @return the tablemodel from the executed method call, never null.
*/
public TableModel queryData(final String query, final DataRow parameters)
throws ReportDataFactoryException
{
final int methodSeparatorIdx = query.indexOf('#');
if ((methodSeparatorIdx + 1) >= query.length())
{
// If we have a method separator, then it cant be at the end of the text.
throw new ReportDataFactoryException("Malformed query: " + query); //$NON-NLS-1$
}
if (methodSeparatorIdx == -1)
{
// we have no method. So this query must be a reference to a tablemodel
// instance.
final int parameterStartIdx = query.indexOf('(');
final String[] parameterNames;
final String constructorName;
if (parameterStartIdx == -1)
{
parameterNames = StaticDataFactory.EMPTY_PARAMS;
constructorName = query;
}
else
{
parameterNames = createParameterList(query, parameterStartIdx);
constructorName = query.substring(0, parameterStartIdx);
}
try
{
final Constructor c = findDirectConstructor(constructorName, parameterNames.length);
final Object[] params = new Object[parameterNames.length];
for (int i = 0; i < parameterNames.length; i++)
{
final String name = parameterNames[i];
params[i] = parameters.get(name);
}
return (TableModel) c.newInstance(params);
}
catch (Exception e)
{
throw new ReportDataFactoryException
("Unable to instantiate class for non static call.", e); //$NON-NLS-1$
}
}
return createComplexTableModel
(query, methodSeparatorIdx, parameters);
}
public String[] getParameterFields(final String query) throws ReportDataFactoryException
{
final int methodSeparatorIdx = query.indexOf('#');
if ((methodSeparatorIdx + 1) >= query.length())
{
// malformed query ..
return null;
}
if (methodSeparatorIdx == -1)
{
// we have no method. So this query must be a reference to a tablemodel
// instance.
final int parameterStartIdx = query.indexOf('(');
if (parameterStartIdx == -1)
{
return StaticDataFactory.EMPTY_PARAMS;
}
else
{
final String[] list = createParameterList(query, parameterStartIdx);
final LinkedHashSet<String> hashSet = new LinkedHashSet<String>(Arrays.asList(list));
return hashSet.toArray(new String[hashSet.size()]);
}
}
final String constructorSpec = query.substring(0, methodSeparatorIdx);
final int constParamIdx = constructorSpec.indexOf('(');
if (constParamIdx == -1)
{
final String methodSpec = query.substring(methodSeparatorIdx + 1);
final int parameterStartIdx = methodSpec.indexOf('(');
if (parameterStartIdx == -1)
{
// no parameters. Nice.
return StaticDataFactory.EMPTY_PARAMS;
}
else
{
final String[] list = createParameterList(methodSpec, parameterStartIdx);
final LinkedHashSet<String> hashSet = new LinkedHashSet<String>(Arrays.asList(list));
return hashSet.toArray(new String[hashSet.size()]);
}
}
// We have to find a suitable constructor ..
final String[] constructorParameterNames = createParameterList(constructorSpec, constParamIdx);
final LinkedHashSet<String> hashSet = new LinkedHashSet<String>();
hashSet.addAll(Arrays.asList(constructorParameterNames));
final String methodQuery = query.substring(methodSeparatorIdx + 1);
final int parameterStartIdx = methodQuery.indexOf('(');
if (parameterStartIdx != -1)
{
final String[] list = createParameterList(methodQuery, parameterStartIdx);
hashSet.addAll(Arrays.asList(list));
}
return hashSet.toArray(new String[hashSet.size()]);
}
/**
* Performs a complex query, where the tablemodel is retrieved from an method that was instantiated using parameters.
*
* @param query the query-string that contains the method to call.
* @param methodSeparatorIdx the position where the method specification starts.
* @param parameters the set of parameters.
* @return the resulting tablemodel, never null.
* @throws ReportDataFactoryException if something goes wrong.
*/
private TableModel createComplexTableModel(final String query,
final int methodSeparatorIdx,
final DataRow parameters)
throws ReportDataFactoryException
{
final String constructorSpec = query.substring(0, methodSeparatorIdx);
final int constParamIdx = constructorSpec.indexOf('(');
if (constParamIdx == -1)
{
// Either a static call or a default constructor call..
return loadFromDefaultConstructor(query, methodSeparatorIdx, parameters);
}
// We have to find a suitable constructor ..
final String className = query.substring(0, constParamIdx);
final String[] parameterNames = createParameterList(constructorSpec, constParamIdx);
final Constructor c = findIndirectConstructor(className, parameterNames.length);
final String methodQuery = query.substring(methodSeparatorIdx + 1);
final String[] methodParameterNames;
final String methodName;
final int parameterStartIdx = methodQuery.indexOf('(');
if (parameterStartIdx == -1)
{
// no parameters. Nice.
methodParameterNames = StaticDataFactory.EMPTY_PARAMS;
methodName = methodQuery;
}
else
{
methodName = methodQuery.substring(0, parameterStartIdx);
methodParameterNames = createParameterList(methodQuery, parameterStartIdx);
}
final Method m = findCallableMethod(className.trim(), methodName.trim(), methodParameterNames.length);
try
{
final Object[] constrParams = new Object[parameterNames.length];
for (int i = 0; i < parameterNames.length; i++)
{
final String name = parameterNames[i];
constrParams[i] = parameters.get(name);
}
final Object o = c.newInstance(constrParams);
final Object[] methodParams = new Object[methodParameterNames.length];
for (int i = 0; i < methodParameterNames.length; i++)
{
final String name = methodParameterNames[i];
methodParams[i] = parameters.get(name);
}
final Object data = m.invoke(o, methodParams);
if (data == null)
{
throw new ReportDataFactoryException("The call did not return a valid tablemodel.");
}
return (TableModel) data;
}
catch (Exception e)
{
throw new ReportDataFactoryException
("Unable to instantiate class for non static call."); //$NON-NLS-1$
}
}
/**
* Loads a tablemodel from a parameterless class or method. Call does not use any parameters.
*
* @param query the query-string that contains the method to call.
* @param methodSeparatorIdx the position where the method specification starts.
* @param parameters the set of parameters.
* @return the resulting tablemodel, never null.
* @throws ReportDataFactoryException if something goes wrong.
*/
private TableModel loadFromDefaultConstructor(final String query,
final int methodSeparatorIdx,
final DataRow parameters)
throws ReportDataFactoryException
{
final String className = query.substring(0, methodSeparatorIdx);
final String methodSpec = query.substring(methodSeparatorIdx + 1);
final String methodName;
final String[] parameterNames;
final int parameterStartIdx = methodSpec.indexOf('(');
if (parameterStartIdx == -1)
{
// no parameters. Nice.
parameterNames = StaticDataFactory.EMPTY_PARAMS;
methodName = methodSpec;
}
else
{
parameterNames = createParameterList(methodSpec, parameterStartIdx);
methodName = methodSpec.substring(0, parameterStartIdx);
}
try
{
final Method m = findCallableMethod(className.trim(), methodName.trim(), parameterNames.length);
final Object[] params = new Object[parameterNames.length];
for (int i = 0; i < parameterNames.length; i++)
{
final String name = parameterNames[i];
params[i] = parameters.get(name);
}
if (Modifier.isStatic(m.getModifiers()))
{
final Object data = m.invoke(null, params);
if (data == null)
{
throw new ReportDataFactoryException("The call did not return a valid tablemodel.");
}
return (TableModel) data;
}
final ClassLoader classLoader = getClassLoader();
final Class c = Class.forName(className, false, classLoader);
final Object o = c.newInstance();
if (o == null)
{
throw new ReportDataFactoryException
("Unable to instantiate class for non static call."); //$NON-NLS-1$
}
final Object data = m.invoke(o, params);
if (data == null)
{
throw new ReportDataFactoryException("The call did not return a valid tablemodel.");
}
return (TableModel) data;
}
catch (ReportDataFactoryException rdfe)
{
throw rdfe;
}
catch (Exception e)
{
throw new ReportDataFactoryException
("Something went terribly wrong: ", e); //$NON-NLS-1$
}
}
/**
* Creates the list of column names that should be mapped into the method or constructor parameters.
*
* @param query the query-string.
* @param parameterStartIdx the index from where to read the parameter list.
* @return an array with column names.
* @throws ReportDataFactoryException if something goes wrong.
*/
private String[] createParameterList(final String query,
final int parameterStartIdx)
throws ReportDataFactoryException
{
final int parameterEndIdx = query.lastIndexOf(')');
if (parameterEndIdx < parameterStartIdx)
{
throw new ReportDataFactoryException("Malformed query: " + query); //$NON-NLS-1$
}
final String parameterText =
query.substring(parameterStartIdx + 1, parameterEndIdx);
final CSVTokenizer tokenizer = new CSVTokenizer(parameterText, ",", "\"", false);
final int size = tokenizer.countTokens();
final String[] parameterNames = new String[size];
int i = 0;
while (tokenizer.hasMoreTokens())
{
parameterNames[i] = tokenizer.nextToken();
i += 1;
}
return parameterNames;
}
/**
* Returns the current classloader.
*
* @return the current classloader.
*/
protected ClassLoader getClassLoader()
{
return ObjectUtilities.getClassLoader(StaticDataFactory.class);
}
/**
* Tries to locate a method-object for the call. This method will throw an Exception if the method was not found or
* not public.
*
* @param className the name of the class where to seek the method.
* @param methodName the name of the method.
* @param paramCount the parameter count of the method we seek.
* @return the method object.
* @throws ReportDataFactoryException if something goes wrong.
*/
private Method findCallableMethod(final String className,
final String methodName,
final int paramCount)
throws ReportDataFactoryException
{
final ClassLoader classLoader = getClassLoader();
if (classLoader == null)
{
throw new ReportDataFactoryException("No classloader!"); //$NON-NLS-1$
}
try
{
final Class c = Class.forName(className, false, classLoader);
if (Modifier.isAbstract(c.getModifiers()))
{
throw new ReportDataFactoryException("Abstract class cannot be handled!"); //$NON-NLS-1$
}
final Method[] methods = c.getMethods();
for (int i = 0; i < methods.length; i++)
{
final Method method = methods[i];
if (Modifier.isPublic(method.getModifiers()) == false)
{
continue;
}
if (method.getName().equals(methodName) == false)
{
continue;
}
final Class returnType = method.getReturnType();
if (TableModel.class.isAssignableFrom(returnType) == false)
{
continue;
}
if (method.getParameterTypes().length != paramCount)
{
continue;
}
return method;
}
}
catch (ClassNotFoundException e)
{
throw new ReportDataFactoryException("No such Class: " + className, e); //$NON-NLS-1$
}
throw new ReportDataFactoryException("No such Method: " + className + '#' + methodName); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Tries to locate a suitable public constructor for the number of parameters. This will return the first constructor
* that matches, no matter whether the parameter types will match too.
* <p/>
* The Class that is referenced must be a Tablemodel implementation.
*
* @param className the classname on where to find the constructor.
* @param paramCount the number of parameters expected in the constructor.
* @return the Constructor object, never null.
* @throws ReportDataFactoryException if the constructor could not be found or something went wrong.
*/
private Constructor findDirectConstructor(final String className,
final int paramCount)
throws ReportDataFactoryException
{
final ClassLoader classLoader = getClassLoader();
if (classLoader == null)
{
throw new ReportDataFactoryException("No classloader!"); //$NON-NLS-1$
}
try
{
final Class c = Class.forName(className, false, classLoader);
if (TableModel.class.isAssignableFrom(c) == false)
{
throw new ReportDataFactoryException
("The specified class must be either a TableModel or a ReportData implementation: " + className); //$NON-NLS-1$
}
if (Modifier.isAbstract(c.getModifiers()))
{
throw new ReportDataFactoryException
("The specified class cannot be instantiated: it is abstract:" + className); //$NON-NLS-1$
}
final Constructor[] methods = c.getConstructors();
for (int i = 0; i < methods.length; i++)
{
final Constructor method = methods[i];
if (Modifier.isPublic(method.getModifiers()) == false)
{
continue;
}
if (method.getParameterTypes().length != paramCount)
{
continue;
}
return method;
}
}
catch (ClassNotFoundException e)
{
throw new ReportDataFactoryException("No such Class", e); //$NON-NLS-1$
}
throw new ReportDataFactoryException
("There is no constructor in class " + className + //$NON-NLS-1$
" that accepts " + paramCount + " parameters."); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Tries to locate a constructor that accepts the specified number of parameters. The referenced class can be of any
* type, as we will call a method on that class that will return the tablemodel for us.
*
* @param className the classname of the class where to search the constructor.
* @param paramCount the numbers of parameters expected.
* @return the constructor object, never null.
* @throws ReportDataFactoryException if the constructor could not be found or something went wrong.
*/
private Constructor findIndirectConstructor(final String className,
final int paramCount)
throws ReportDataFactoryException
{
final ClassLoader classLoader = getClassLoader();
if (classLoader == null)
{
throw new ReportDataFactoryException("No classloader!"); //$NON-NLS-1$
}
try
{
final Class c = Class.forName(className, false, classLoader);
if (Modifier.isAbstract(c.getModifiers()))
{
throw new ReportDataFactoryException(
"The specified class cannot be instantiated: it is abstract."); //$NON-NLS-1$
}
final Constructor[] methods = c.getConstructors();
for (int i = 0; i < methods.length; i++)
{
final Constructor method = methods[i];
if (Modifier.isPublic(method.getModifiers()) == false)
{
continue;
}
if (method.getParameterTypes().length != paramCount)
{
continue;
}
return method;
}
}
catch (ClassNotFoundException e)
{
throw new ReportDataFactoryException("No such Class", e); //$NON-NLS-1$
}
throw new ReportDataFactoryException
("There is no constructor in class " + className + //$NON-NLS-1$
" that accepts " + paramCount + " parameters."); //$NON-NLS-1$ //$NON-NLS-2$
}
/**
* Returns a copy of the data factory that is not affected by its anchestor and holds no connection to the anchestor
* anymore. A data-factory will be derived at the beginning of the report processing.
*
* @return a copy of the data factory.
*/
public DataFactory derive()
{
return this;
}
/**
* Closes the data factory and frees all resources held by this instance.
* <p/>
* This method is empty.
*/
public void close()
{
}
public String[] getQueryNames()
{
return EMPTY_NAMES;
}
public String translateQuery(final String queryName)
{
return queryName;
}
}