Mark Clarke is lead architect and developer at Jumping Bean, a leading Linux and Java service company located in Johannesburg South Africa. The majority of Jumping Bean's Java consulting work is in the development of bespoke applications for clients, with extensive integration of open source components and libraries to architect a robust and cost effective solution. Mark has posted 5 posts at DZone. View Full User Profile

Integrating BIRT With Wicket

09.07.2009
| 10492 views |
  • submit to reddit

In my previous article I setout a way we used to, at least partially, integrated BIRT into the Maven build process for a recent project. My next challenge on the project was how to integrate Birt into the Wicket framework? We have used Wicket in some of our previous projects, but in the past we have not had the need to delve beneath the Wicket API to understand its life cycle process, for the creation of a user sessions, population of wicket components from raw HTML request parameters and the handling of the request. This is a testament to Wicket's  wonderful architecture since we have been able to write non-trivial applications without having the go beneath the component abstraction provided by the framework;  however this was to change with BIRT.

How to Integrate BIRT into Your Application

There are essentially two ways to integrate BIRT (Business Intelligence and Reporting Tools) into your application:

  • Using the Birt Viewer application,
  • Using the Report Engine API

From the eclipse BIRT documentation the easiest way to integrate BIRT into your web app is to make use of the BIRT Viewer application.  Once I read the documentation it was clear to me that this method would be extremely bothersome for use with Wicket. For integration with the viewer one needs either to use JSP tags, which is not an option in Wicket, or to provide URL links that pass through the relevant report parameters in the query string, which is possible in Wicket.

Both these methods require one to install separate war file, or web app, for the BIRT Viewer. Although the BIRT Viewer does provide some great functionality, like ability to export to different formats, automatic creation of parameter dialog boxes and more, I was not keen on this deployment model. It  meant that each time I deployed our web app I would need to deploy a BIRT Viewer app as well. I decided to rather use the lower level API's to integrate BIRT. More information on how to integrate BIRT Viewer can be found at the Eclipse site.

BIRT Integration using the Report Engine API with Wicket

I decided the best approach was to use the Report engine API. Ths report engine api allows one to have   the report generated via API calls to the BIRT Runtime, and then one would have to handle the streaming of the report output to the client browser oneself. If I was using the servlet API directly it would be a simple matter of writing the output stream to the response object in the servlet's get or post method, but in Wicket the servlet API is hidden from direct access under the layers of the framework.

I do not cover the BIRT Reporting API in any detail as it is fairly straight forward to use. The code snippet at the end of the article shows how I used the API to generate the report but good information on how to use the BIRT API can be found at the  Eclipse BIRT site

Wicket's Request Life Cycle

To understand how one can get access to the response outputstream in Wicket, requires an understanding of the Wicket request life cycle flow. Sadly there is scant documentation on the Internet about the way the Wicket framework goes, from raw html request, to the framework calling your Wicket code. Some information can be found here but it glosses over the details of the RequestCycle object itself leaving one scratching your head as to where you can get hold of the outputstream.

After some investigation, the diagramme below captures my current understanding of the Wicket Request Life Cycle Proces.

Sequence Diagramme of Wicket Request Life Cycle

The life cycle starts when the WicketFilter, as in the diagramme, or WicketServlet, depending on your web.xml setup, gets the raw HTML request. The Filter then wraps the HTMLServlerRequest and HTMLServletResponse object in Wicket's WebRequest and WebResponse objects and creates, or retrieves, a RequestCycle object. The RequestCycle class is abstract so the object returned is actually a WebRequestCycle object but most of the interesting work in done in the abstract RequestCycle object.

The RequestCycle object's "request" methond  is then called, to handle the remainder of the processing. The RequestCycle object requires a IRequestCycleProcessor object that actually does all of the work.  Our actual object is an instance of the WebRequestCycleProcessor class which extends the AbstractRequestCycleProcessor class, where, once again, most of the interesitng work is done. The WebRequestCycle object calls the methods on the WebRequestCycleProcessor in a inversion of control pattern during the processing of the "request" method.

Programme Flow of the WebRequest

The WebRequestCycleProcessor object calls the methods on the WebRequest object in a three step process in the following order:

  • Resolve - this method resolves the URL to the target component, IRequestTarget, that was called. It will invlolve creating the component from scratch, or retrieving it from the user session,
  • ProcessEvents - this calls any events that are applicable for the comoponent. In the diagramme above the "onSubmit()" event of a form button is being called. During this processing the RequestTarget can changed, with a call the the RequestCycle's "setRequesTarget" method. This will reset the three step process to start at "ProcessEvents" for the new requestTarget. (The RequestCycle object keeps a stack of RequestTargets to ensure all events are fired and cleaned up properly). In the case of BIRT we use the "onSubmit" method to change the IRequestTarget (See below) to the PDF output of the report engine. Thus when the "respond" method is called in the next step in the processes, it is called on our custom object rather than on the original  target object. 
  • Respond - this is where the "render" method from the target component is called. 

So we need to set our PDF report, generated by BIRT and the Report Engine APIs, as the RequestTarget once a button or link is clicked. Wicket uses the IResourceStream interface to link to resource streams. Most of the classes implementing this interface expect to get an input stream object from which to read the stream for final output to the response. In cases where you need to write directly to the output stream for dynamically created content, such as PDF reports, you need to create an object that implements the IResourceStreamWriter interface. The convenience class AbstractResourceStreamWriter is provided for easy extension and overriding of the "write" method.


So after this rather involved explanation, the code to actual get your BIRT pdf to display is quiet simple.

           public void onSubmit() {
HashMap map = new HashMap();
....(process parameters from Wicket for the report and place in the map object)
ReportParameterPage.this.generateReport(map,((WebApplication)ReportParameterPage.this.getApplication()).getServletContext().getRealPath(reportName.toString()));
}

protected void generateReport(final Map map,final String strReport){
AbstractResourceStreamWriter writer = new AbstractResourceStreamWriter (){
public void write(OutputStream os) {
EngineConfig config = new EngineConfig();
String path = ((WebApplication)ReportParameterPage.this.getApplication()).getServletContext().getRealPath("WEB-INF/birt");
config.setEngineHome(path);
path = ((WebApplication)ReportParameterPage.this.getApplication()).getServletContext().getRealPath("WEB-INF/log");
config.setLogConfig(path,Level.FINE);
try {

Platform.startup(config);
IReportEngineFactory factory =(IReportEngineFactory) Platform.createFactoryObject(IReportEngineFactory.EXTENSION_REPORT_ENGINE_FACTORY);
IReportEngine engine = factory.createReportEngine(config);
IReportRunnable report = engine.openReportDesign(strReport);

IRunAndRenderTask task = engine.createRunAndRenderTask(report);
task.setParameterValues(map);
PDFRenderOption options = new PDFRenderOption();
options.setOutputFormat("pdf");
options.setOutputStream(os);
task.setRenderOption(options);
task.run();
task.close();
} catch (BirtException e) {
logger.error(e.getLocalizedMessage());
}
}

public String getContentType() {
return "application/pdf";
}
};
this.getRequestCycle().setRequestTarget(new ResourceStreamRequestTarget(writer));
}

Conclusion

Wicket is a great framework and BIRT is a great reporting tool, I hope that this article helps others in using the two technologies together. I also hope that we seee more articles explaining the inner workings of wicket, as I am sure this will help with the development of new 3rd party components.

Published at DZone with permission of its author, Mark Clarke.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Tags:

Comments

Pierre Teddy replied on Wed, 2009/10/14 - 2:32pm

Hi Mark,

 

Great article, this will help me a lot. Please note that the link to the source code:

Source URL: http://java.dzone.com/articles/integrating-birt-your-wicket just point back to the article. Can you please fix the link.Thanks Again for a great article.

 

Mark Clarke replied on Tue, 2009/12/29 - 4:30pm in response to: Pierre Teddy

Hi Clermont38,

 

Sorry for the delayed response! Its weird because when I am logged in and edit the article the link is pointing to the correct url but appears to be incorrect for non-authenticated users. Anywya the link is

 

http://java.dzone.com/articles/integrating-birt-your-maven

 

 I will try and get it fixed in the article.

 

regards

 

 

Balamurugan Kokilan replied on Tue, 2010/07/06 - 3:49am

Hi Mark,

 

A good article.

But still I am not able to access source code for this article.

Please post the correct link for sourc code of this article as the above mentioned is for maven.

Sylvia Fronczak replied on Wed, 2010/08/04 - 12:27pm

Thanks for the great article!

I am running into an issue where I keep getting a "Can't startup the OSGI framework" exception. I have validated that all the necessary BIRT runtime files are in the WEB-INF folder, based mostly on http://wiki.eclipse.org/Servlet_Example_%28BIRT%29_2.1 

Has anyone else had this problem? Do you know of anything I should take a look at?

Sylvia Fronczak replied on Wed, 2010/08/04 - 2:19pm in response to: Sylvia Fronczak

Nevermind. Got it. My engine home path was incorrect.

Prem Saharan replied on Wed, 2012/07/18 - 8:48am

i am using following code to generate report and all works fine

add(new SubmitLink("runReport") {

      private static final long serialVersionUID = 10011L;

      @Override

      public void onSubmit() {

        HashMap<String, Object> map = new HashMap<String, Object>();

        SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");

        map.put("reportStartDate", formatter.format(getReportStartDate()));

        Calendar cal = Calendar.getInstance();

        cal.setTime(getReportEndDate());

        cal.add(Calendar.DATE, 1);

        map.put("reportEndDate", formatter.format(cal.getTime()));

        generateReport(map,"ecommerceReport.rptdesign");

      }   

    }

    .add(RelativePathPrefixHandler.RELATIVE_PATH_BEHAVIOR)

    ); 

 

 

public AbstractResourceStreamWriter generateReport(final Map<String, Object> map,final String reportPath, final String reportOutputFormat, final DS ds) {

   AbstractResourceStreamWriter writer = new AbstractResourceStreamWriter () {

        public void write(OutputStream os) {

            try {

              String path = WebApplication.get().getServletContext().getRealPath("WEB-INF");

              String imagesPath = WebApplication.get().getServletContext().getRealPath("images");

              String urlContext = WebApplication.get().getServletContext().getContextPath();

 

              String strReport = path + reportsPath + reportPath;

 

              if(CSV.equals(reportOutputFormat)) {

                String b = generateCSV(map, strReport);

                  os.write(b.getBytes());

               } else {

                 IReportEngine engine = getReportEngine();

                 IReportRunnable report = engine.openReportDesign(strReport);

 

                 IRunAndRenderTask task = engine.createRunAndRenderTask(report);

                 task.setParameterValues(map);

                 RenderOption options = null;

                 if(HTML.equals(reportOutputFormat)) {

                 options = new HTMLRenderOption();

                 options.setActionHandler(new HTMLActionHandler());

                 options.setImageHandler(new HTMLServerImageHandler());

                 ((HTMLRenderOption)options).setImageDirectory(imagesPath);

                 ((HTMLRenderOption)options).setBaseImageURL(urlContext+"/images/");

                 } else {

                 options = new PDFRenderOption();

                 }

                 options.setOutputFormat(reportOutputFormat);

                 options.setOutputStream(os);

                 task.setRenderOption(options);

 

                      switch (ds){

                           case OLTP:

                               task.getAppContext().put("OdaJDBCDriverPassInConnection", getDataSource().getConnection());

                               break;

                           case EDW:

                               task.getAppContext().put("OdaJDBCDriverPassInConnection", getEdwDataSource().getConnection());

                               break;

                      }

 

                 task.run();

                 task.close();

               }

            } catch (BirtException e) {

                log.error(e.getLocalizedMessage());

            } catch (IOException io) {

              log.error("Exception creating csv", io);

            } catch (SQLException se) {

              log.error("Exception getting database connection", se);

            }

        }

 

        public String getContentType() {

          if(PDF.equals(reportOutputFormat)) return "application/pdf";

          if(HTML.equals(reportOutputFormat)) return "text/html";

          if(XLS.equals(reportOutputFormat)) return "application/vnd.ms-excel";

          if(DOC.equals(reportOutputFormat)) return "application/msword";

          if(PPT.equals(reportOutputFormat)) return "application/vnd.ms-powerpoint";

          if(CSV.equals(reportOutputFormat)) return "application/csv";

          return "application/pdf";

        }

   };

 

   return writer;

  }

  now i want to display spinner while report is generated so i use AjaxSubmitLink, on submit spinner displays but report is not generated, the code for AjaxSubmitLink as follows quite similar to above, birt code is same as above

add(new AjaxSubmitLink("runReport1") {

@Override

public void onSubmit(AjaxRequestTarget target, Form<?> form) {

HashMap<String, Object> map = new HashMap<String, Object>();

       SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");

       map.put("reportStartDate", formatter.format(getReportStartDate()));

       // Add a day to the end date to include the date entered by the user.

       Calendar cal = Calendar.getInstance();

       cal.setTime(getReportEndDate());

       cal.add(Calendar.DATE, 1);

       map.put("reportEndDate", formatter.format(cal.getTime()));

       AbstractResourceStreamWriter writer = reportService.generateReport(map, "ecommerceReport.rptdesign", reportFormatOption.getModelValue());

       this.getRequestCycle().setRequestTarget(new ResourceStreamRequestTarget(writer, getString("reportForm.invoices")));

}

}); 

 any idea whats missing 

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.