Thursday, November 05, 2009

Calling Spring Objects from BIRT Expressions and Event Handlers

Several examples are already available on the web, which demonstrate calling the BIRT engine from a Spring MVC application.

Integrating BIRT with Spring in a Web application
and
Eclipse BIRT in Spring web applications
are a couple of examples.

There is not much information on how to include Spring Beans within a report design. This post details an example of injecting the Spring ApplicationContext into BIRT’s AppContext object which will allow you to call your Spring Beans in BIRT expressions or event handlers. A link for the source is listed at the bottom. A readme file containing instructions for building the example is included in the download.

You can include the BIRT runtime by following the post above or examining the download. The Example implements a Spring Controller with the following code.


package org.eclipse.birt.spring;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.eclipse.birt.report.engine.api.HTMLRenderOption;
import org.eclipse.birt.report.engine.api.HTMLServerImageHandler;
import org.eclipse.birt.report.engine.api.IReportEngine;
import org.eclipse.birt.report.engine.api.IReportRunnable;
import org.eclipse.birt.report.engine.api.IRunAndRenderTask;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.AbstractController;

public class BirtController extends AbstractController {

private IReportEngine reportEngine;

protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {

String reportName = request.getParameter("ReportName");
ServletContext sc = request.getSession().getServletContext();
reportEngine = BirtEngine.getBirtEngine(sc);
IReportRunnable runnable = null;
runnable = reportEngine.openReportDesign( sc.getRealPath("/Reports")+"/"+reportName );
IRunAndRenderTask runAndRenderTask = reportEngine.createRunAndRenderTask(runnable);

HTMLRenderOption options = new HTMLRenderOption();
options.setOutputFormat("html");
options.setOutputStream(response.getOutputStream());
options.setImageHandler(new HTMLServerImageHandler());
options.setBaseImageURL(request.getContextPath()+"/images");
options.setImageDirectory(sc.getRealPath("/images"));
runAndRenderTask.setRenderOption(options);
runAndRenderTask.run();

return null;
}

}


Note that no error checking is implemented in this example. This controller class just extends the Spring AbstractController class passes the response object to the BIRT engine to output the report. The report name is retrieved from the request. Before running the report the BirtEngine class is used to retrieve the BIRT engine. This is virtually the same BirtEngine class used in the servlet example available in the BIRT wiki with some exceptions that are noted here.


package org.eclipse.birt.spring;

import java.io.InputStream;
import java.io.IOException;
import java.util.Properties;
import java.util.logging.Level;

import org.eclipse.birt.report.engine.api.EngineConfig;
import org.eclipse.birt.report.engine.api.IReportEngine;
import javax.servlet.*;
import org.eclipse.birt.core.framework.PlatformServletContext;
import org.eclipse.birt.core.framework.IPlatformContext;
import org.eclipse.birt.core.framework.Platform;
import org.eclipse.birt.core.exception.BirtException;
import org.eclipse.birt.report.engine.api.IReportEngineFactory;
import org.springframework.context.ApplicationContext;

public class BirtEngine {

private static IReportEngine birtEngine = null;

private static Properties configProps = new Properties();

private final static String configFile = "BirtConfig.properties";

public static synchronized void initBirtConfig() {
loadEngineProps();
}
public static synchronized IReportEngine getBirtEngine(ServletContext sc) {
if (birtEngine == null)
{
//optionally load engine props
//loadEngineProps();
EngineConfig config = new EngineConfig();
if( configProps != null){
String logLevel = configProps.getProperty("logLevel");
Level level = Level.OFF;
if ("SEVERE".equalsIgnoreCase(logLevel))
{
level = Level.SEVERE;
} else if ("WARNING".equalsIgnoreCase(logLevel))
{
level = Level.WARNING;
} else if ("INFO".equalsIgnoreCase(logLevel))
{
level = Level.INFO;
} else if ("CONFIG".equalsIgnoreCase(logLevel))
{
level = Level.CONFIG;
} else if ("FINE".equalsIgnoreCase(logLevel))
{
level = Level.FINE;
} else if ("FINER".equalsIgnoreCase(logLevel))
{
level = Level.FINER;
} else if ("FINEST".equalsIgnoreCase(logLevel))
{
level = Level.FINEST;
} else if ("OFF".equalsIgnoreCase(logLevel))
{
level = Level.OFF;
}

config.setLogConfig(configProps.getProperty("logDirectory"), level);
}
config.setEngineHome("");

IPlatformContext context = new PlatformServletContext( sc );
config.setPlatformContext( context );
config.getAppContext().put("PARENT_CLASSLOADER", BirtEngine.class.getClassLoader());
ApplicationContext sprCtx = ContextAccess.getApplicationContext();
config.getAppContext().put("spring",sprCtx);
try
{
Platform.startup( config );
}
catch ( BirtException e )
{
e.printStackTrace( );
}

IReportEngineFactory factory = (IReportEngineFactory) Platform
.createFactoryObject( IReportEngineFactory.EXTENSION_REPORT_ENGINE_FACTORY );
birtEngine = factory.createReportEngine( config );
}
return birtEngine;
}
public static synchronized void destroyBirtEngine() {
if (birtEngine == null) {
return;
}
birtEngine.destroy();
Platform.shutdown();
birtEngine = null;
}
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
private static void loadEngineProps() {
try {
//Config File must be in classpath
ClassLoader cl = Thread.currentThread ().getContextClassLoader();
InputStream in = null;
in = cl.getResourceAsStream (configFile);
configProps.load(in);
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}

This class just wraps access to the BIRT engine in a singleton.
The notable changes are in these lines of code deal with the EngineConfig class.

config.setEngineHome("");

IPlatformContext context = new PlatformServletContext( sc );
config.setPlatformContext( context );
config.getAppContext().put("PARENT_CLASSLOADER", BirtEngine.class.getClassLoader());
ApplicationContext sprCtx = ContextAccess.getApplicationContext();
config.getAppContext().put("spring",sprCtx);


The setEngineHome is passed a blank value and setting the PlatformContext to use a PlatformServletContext class will by default look for the BIRT plugins in the WEB-INF/Platform/plugins directory. Next we set the parent classloader for the report engine plugin so that classes available to the project will also be available to the BIRT engine. The final line gets the Spring ApplicationContext instance and loads it into the BIRT AppContext object and names it “spring”. The method used for retrieving the Spring context is described in a great post here.

The ContextAccess class is defined as follows.


package org.eclipse.birt.spring;
import org.springframework.context.ApplicationContext;
public class ContextAccess {
private static ApplicationContext ctx;
public static void setApplicationContext(ApplicationContext applicationContext) {
ctx = applicationContext;
}
public static ApplicationContext getApplicationContext() {
return ctx;
}
}


This Class just contains methods to set and get the static variable ctx, which will contain the Spring ApplicationContext with the addition of one more class.



package org.eclipse.birt.spring;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;


public class SpringContextProvider implements ApplicationContextAware {
public void setApplicationContext(ApplicationContext ctx) throws BeansException {
ContextAccess.setApplicationContext(ctx);
}
}



This Class implements the ApplicationContextAware interface which causes the Spring framework to callback this class with the Spring context, when it is created. The Spring configuration file for this example looks like the following.

<beans>
<bean id="springContextProvider" class="org.eclipse.birt.spring.SpringContextProvider">
</bean>
<bean id="urlMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/runbirt.htm">birtController</prop>
</props>
</property>
</bean>
<bean id="birtEngine"
class="org.eclipse.birt.spring.BirtEngine"
destroy-method="destroyBirtEngine">
</bean>
<bean id="birtController"
class="org.eclipse.birt.spring.BirtController">
</bean>
<bean id="carPojo"
class="org.eclipse.birt.spring.CarPojo">
</bean>

</beans>

We have a carPojo bean that is available. To access this from a BIRT expression, all we have to do is use the following syntax.


var mypojo = spring.getBean("carPojo");


You can then call the methods on the object. Eg mypojo.getYear();



Output for the example is presented below.




Caveats
If you preview the report in the designer the report will not work. This is because the Spring context is not available to the designer. The report has to be deployed to test it. There are many ways this example could be extended to circumvent this issue. BIRT provides an extension point to enhance the expression builder which could be used with Spring Remoting to access the Spring context. BIRT also provides an extension to implement a BIRT application context object within the designer that could be used in this same fashion. I will try to implement one of these methods in the future to illustrate this concept.

The example can be downloaded from Birt-Exchange.

No comments: