[Jaspersoft] Creating custom Spring managed DataSource

Posted by {"name"=>"Palash Ray", "email"=>"paawak@gmail.com", "url"=>"https://www.linkedin.com/in/palash-ray/"} on July 19, 2014 · 7 mins read

Problem Statement

Often for reporting, we need to gather data from many diverse resources. In case we are using Jaspersoft server, its sometimes not possible to manage with the existing DataSources provided by Jaspersoft. In this article, we will explore step by step how to write our custom DataSource. The main challenge is that in the given examples like the WebScraperDataSource, the factory beans are not Spring managed. They are instantiated by the framework through a no-argument constructor by reflection. In case I need to talk to other Spring managed beans, it becomes incredibly difficult. In the below example, I will try to put a hack so that we can use our own Spring managed DataSourceFactory.

Dev Environment Setup

I would need to hook into the Jaspersoft's framework and for that, I need to create my own jar which can then be deployed into the WEB-INF/lib directory of Jaspersoft's webapp. I am using Maven. The following dependencies need to be declared:

	
		
			net.sf.jasperreports
			jasperreports
			5.6.0
			provided
		
		
			com.jaspersoft.jasperserver.api.engine.impl
			jasperserver-api-engine-impl
			5.6.0
			provided
		
		
			com.jaspersoft.jasperserver.api.metadata
			jasperserver-api-metadata
			5.6.0
			provided
		
		
			com.jaspersoft.jasperserver.api.common
			jasperserver-api-common
			5.6.0
			provided
		
		
			org.springframework
			spring-core
			3.1.4.RELEASE
			provided
		
		
			org.springframework
			spring-context
			3.1.4.RELEASE
			provided
		
		
			org.springframework.security
			spring-security-core
			2.0.7.RELEASE
			provided
		
	

Note that some of these are not available in Maven public repos and need to manually deployed from the webapps/WEB-INF/lib directory:

mvn install:install-file -DgroupId=com.jaspersoft.jasperserver.api.engine.impl -DartifactId=jasperserver-api-engine-impl -Dversion=5.6.0 -Dpackaging=jar -Dfile=/path-to-webapps/jasperserver-pro/WEB-INF/lib/jasperserver-api-engine-impl-5.6.0.jar
mvn install:install-file -DgroupId=com.jaspersoft.jasperserver.api.metadata -DartifactId=jasperserver-api-metadata -Dversion=5.6.0 -Dpackaging=jar -Dfile=/path-to-webapps/jasperserver-pro/WEB-INF/lib/jasperserver-api-metadata-5.6.0.jar
mvn install:install-file -DgroupId=com.jaspersoft.jasperserver.api.common -DartifactId=jasperserver-api-common -Dversion=5.6.0 -Dpackaging=jar -Dfile=/path-to-webapps/jasperserver-pro/WEB-INF/lib/jasperserver-api-common-5.6.0.jar

Spring bean configuration

Jaspersoft works with Spring, thanks God for that!
jasper-bean-creation-sequence-1
jasper-bean-creation-sequence-2
Finally, this is what the Spring file looks like:



	
	
	
		
		
		
		
		
			
				
					
				
				
					
				
			
		
		
			
				
			
		
	
	
		
		
			
				WEB-INF/springBeanDataSource
			
		
		
	

Information flow

jasper-flow-highlevel-1

public class SpringBeanDataSourceServiceFactory implements CustomDelegatedDataSourceServiceFactory, ApplicationContextAware {
    private ApplicationContext applicationContext;
    @Override
    public void setCustomDataSourceDefinition(CustomDataSourceDefinition customDataSourceDefinition) {
    }
    @Override
    public ReportDataSourceService createService(ReportDataSource reportDataSource) {
    if (!(reportDataSource instanceof CustomReportDataSourceImpl)) {
        throw new IllegalArgumentException("Expecting an instance of " + CustomReportDataSourceImpl.class + ", but found " + reportDataSource.getClass());
    }
    CustomReportDataSourceImpl customDataSource = (CustomReportDataSourceImpl) reportDataSource;
    Map map = customDataSource.getPropertyMap();
    String beanName = (String) map.get("beanName");
    String methodName = (String) map.get("methodName");
    return new SpringBeanDataSourceService(applicationContext, beanName, methodName);
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
    this.applicationContext = applicationContext;
    }
}

 

public class SpringBeanDataSourceService implements ReportDataSourceService {
    private final ApplicationContext applicationContext;
    private final String beanName;
    private final String methodName;
    public SpringBeanDataSourceService(ApplicationContext applicationContext, String beanName, String methodName) {
    this.applicationContext = applicationContext;
    this.beanName = beanName;
    this.methodName = methodName;
    }
    @Override
    public void closeConnection() {
    // do nothing
    }
    @SuppressWarnings("unchecked")
    @Override
    public void setReportParameterValues(@SuppressWarnings("rawtypes") Map paramMap) {
    paramMap.put(SpringBeanQueryExecutorFactory.APPLICATION_CONTEXT, applicationContext);
    paramMap.put(SpringBeanQueryExecutorFactory.BEAN_NAME, beanName);
    paramMap.put(SpringBeanQueryExecutorFactory.METHOD_NAME, methodName);
    }
}

jasper-flow-highlevel-2

public class SpringBeanQueryExecutorFactory extends AbstractQueryExecuterFactory {
    public static final String APPLICATION_CONTEXT = "APPLICATION_CONTEXT";
    public static final String BEAN_NAME = "BEAN_NAME";
    public static final String METHOD_NAME = "METHOD_NAME";
    @Override
    public Object[] getBuiltinParameters() {
    return new Object[] { APPLICATION_CONTEXT, ApplicationContext.class, BEAN_NAME, String.class, METHOD_NAME, String.class };
    }
    @Override
    public JRQueryExecuter createQueryExecuter(JasperReportsContext jasperReportsContext, JRDataset dataset, Map parameters) throws JRException {
    return new SpringBeanQueryExecutor(jasperReportsContext, dataset, parameters);
    }
    @Override
    public boolean supportsQueryParameterType(String className) {
    return true;
    }
}
public class SpringBeanQueryExecutor extends JRAbstractQueryExecuter {
    private final ApplicationContext applicationContext;
    private final Map reportParametrsMap;
    private final String beanName;
    private final String methodName;
    protected SpringBeanQueryExecutor(JasperReportsContext jasperReportsContext, JRDataset dataset, Map parametersMap) {
	super(jasperReportsContext, dataset, parametersMap);
	reportParametrsMap = (Map) parametersMap.get(JRParameter.REPORT_PARAMETERS_MAP).getValue();
	applicationContext = (ApplicationContext) reportParametrsMap.get(SpringBeanQueryExecutorFactory.APPLICATION_CONTEXT);
	beanName = (String) reportParametrsMap.get(SpringBeanQueryExecutorFactory.BEAN_NAME);
	methodName = (String) reportParametrsMap.get(SpringBeanQueryExecutorFactory.METHOD_NAME);
	parseQuery();
    }
    @Override
    public JRDataSource createDatasource() throws JRException {
	String queryString = getQueryString();
	String[] queryTokens = queryString.split("=");
	Object serviceInterfaceImpl = applicationContext.getBean(beanName);
	System.out.println("SpringBeanQueryExecutor.createDatasource() queryString=" + queryString + ", serviceInterfaceImpl=" + serviceInterfaceImpl
	        + ", beanName=" + beanName + ", methodName=" + methodName);
	Method method;
	try {
	    method = serviceInterfaceImpl.getClass().getDeclaredMethod(methodName, Class.forName(queryTokens[1]));
	} catch (Exception e) {
	    throw new RuntimeException(e);
	}
	String language = (String) reportParametrsMap.get(queryTokens[0]);
	List data;
	try {
	    data = (List) method.invoke(serviceInterfaceImpl, Language.valueOf(language.toUpperCase()));
	} catch (Exception e) {
	    throw new RuntimeException(e);
	}
	return new JRBeanCollectionDataSource(data);
    }
    @Override
    public void close() {
    }
    @Override
    public boolean cancelQuery() throws JRException {
	return false;
    }
    @Override
    protected String getParameterReplacement(String parameterName) {
	return null;
    }
}

Making our custom Query appear on the Jaspersoft

We need to make further configuration changes before our custom query appears on the Jaspersoft screen.

Make an entry in the applicationContext-rest-services.xml

 
           sql
           hql
           domain
           HiveQL
           MongoDbQuery
           cql
   	SpringBeanQuery
       

Register your factory at WEB-INF/classes/jasperreports.properties

Make the following entry:

net.sf.jasperreports.query.executer.factory.SpringBeanQuery=com.swayam.demo.jasper.SpringBeanQueryExecutorFactory

Generate report

We are all set now to generate a report. Create the DataSource we have just now defined. Then create a JRXML and define the query there:



	
	
	
	
		
	
	
	
	
...

Link the DataSource and the JRXML. You need to define an Input as well. Then run the report. You should see the below report:

generated-reportResources

The sources for this can be found here: https://github.com/paawak/blog/tree/master/code/jasper/custom-datasource