Thursday, December 11, 2008

BIRT - Reversing a Chart Scale.

The Chart Engine that is delivered as part of the BIRT project offers a lot of functionality. Not only can you extend most of its features, the chart engine also offers hooks to do scripting (either in Java or JavaScript). Scripting can be very useful for modifying the chart based on parameters or data. As an example assume you have a chart that is displayed as follows:



Assume these numbers represent percent of sales returns and the lower the bar the better. In some cases larger bars graphically represent better values, so instead of displaying the chart as above the user may wish to see the chart as follows.



One way to achieve this effect is to modify the values and labels in chart script. To modify or add scripts to a chart select the chart and then the script tab within the designer. Bear in mind that although this example illustrates building the event handler in JavaScript it can also be done with Java.

The first thing to setup is the scale for the chart, which we want to tie to a report parameter. So you can enter a script like:


//Global scale
gblscale = 100.00;


//this is the first event called for charting
//So set scale here
function beforeDataSetFilled( series, idsp, icsc )
{
gblscale = icsc.getExternalContext().getScriptable().getParameterValue("Scale");
}

We set the scale for the chart to the parameter value in the beforeDataSetFilled event because this is the first event called for chart scripts. This variable will be used in other scripts for calculating labels.

The next script we need to override is the afterDataSetFilled event handler. This script can be used to modify the values used in a chart.

function afterDataSetFilled(series, dataSet, icsc)
{
if( series.getSeriesIdentifier() == "series1"){
var list = dataSet.getValues();
for ( i=0; i < list.length; i=i+1)
{
list[i] = gblscale - list[i];
}
}
}



This script first checks the series identifier as this call is made for every series, but we only want to change the bar series. The series identifier should be entered in the chart wizard. This can be done in the third tab of the chart wizard.



Once we know that we are working with the correct series, we can modify the values by calling the dataSet.getValues. In the script we are just subtracting the values from the global scale and resetting the value. So if the global scale is 100, a 5 value becomes 95.

We can then set the scale for the entire chart in the beforeGeneration event as follows.


function beforeGeneration(chart, icsc)
{
importPackage( Packages.org.eclipse.birt.chart.model.data.impl );
xAxis = chart.getBaseAxes()[0];
yAxis = chart.getOrthogonalAxes( xAxis, true)[0]
yscale = yAxis.getScale();
yscale.setStep (10);
yscale.setMin( NumberDataElementImpl.create(0) )
yscale.setMax( NumberDataElementImpl.create(gblscale) )
yAxis.setScale(yscale);

}

By default this script sets the Y axis to range from 0 to whatever the parameter value is set to. We now can reverse the labels by adding a beforeDrawAxisLabel script.

function beforeDrawAxisLabel(axis, label, icsc)
{
importPackage(Packages.org.eclipse.birt.chart.model.attribute);
if (axis.getType() == AxisType.LINEAR_LITERAL){
var val = parseFloat(label.getCaption().getValue());
var newval = gblscale - val;
label.getCaption().setValue(newval);
}
}

This script only changes the labels. Internally the chart is still scaled from 0 to the parameter value. So if the scale is 100 and the original value is 5, its new value is 95 and the bar will be drawn from 100 to 5. Internally this is still 0 – 95, but we reversed the labels. The last script we may want to add can change the data point labels. This script is called beforeDrawDataPointLabel and is called whenever the chart renders labels on the bars.

function beforeDrawDataPointLabel(dph, label, icsc)
{
var val = parseFloat(label.getCaption().getValue());
var newval = gblscale - val;
newval = newval.toFixed(2);
label.getCaption().setValue(newval);
}

In this script we just reverse the value label. This example can be downloaded at
BIRT Exchange.

Monday, November 24, 2008

Friday is the last Day for EclipseCon 2009 proposals

I just wanted to put a post up to let everyone know that the deadline for submissions for EclipseCon 2009 has been extended to Friday (11/28/2008 5pm PST). We are still looking for some use case proposals in the Reporting track and would appreciate feedback on the existing submissions. If you are interested, the EclipseCon 2009 page is located here.

Wednesday, November 05, 2008

BIRT Connection Pooling Continued Again

With the release of BIRT 2.3.1 connection pooling options have been extended.
In a prior post, I built an example that showed how to implement connection pooling using the driverBridge extension point. While this method is useful, with BIRT 2.3.1 there is a much easier method for passing BIRT an already created connection object.

You can now supply a connection by adding the connection object to BIRT’s application context object. The key for this object is OdaJDBCDriverPassInConnection. So to pass in the connection while using the Report Engine API, use code similar to:



IReportRunnable design = null;
//Open the report design
design = engine.openReportDesign("Reports/passinconnection.rptdesign");
IRunAndRenderTask task = engine.createRunAndRenderTask(design);
HTMLRenderOption options = new HTMLRenderOption();
options.setOutputFileName("output/resample/passinconnobj.html");
options.setOutputFormat("HTML");
task.setRenderOption(options);
task.getAppContext().put("OdaJDBCDriverPassInConnection", this.getConnection());
task.run();
task.close();


This assumes you already have the report engine started. The getConnection method in this example simply creates a java.sql.Connection to my database. You will need to create your own function. Also keep in mind that the BIRT JDBC plugin will close the connection when it has finished with it, so if you plan on using the object in multiple tasks, you will need to reopen it. There is a bug to allow the closing of the connection to be optional -Bugzilla Entry. This application context setting should be applied at the task level.

If you wish to set it in the Example Viewer’s application context take a look at this wiki entry.

These are the ways that a connection can now be manipulated in BIRT:
1-Property binding
2-JNDI
3-Script data set
4-DataSource.beforeOpen() event.
5–driverBridge extension
6-Application Context Ojbect (described here)

Another setting the JDBC plugin now provides is OdaJDBCDriverClassPath, which allows setting the classpath for locating drivers. This prevents the user from having to put JDBC drivers in the drivers directory of the JDBC plugin. This should be set on the EngineConfig object and not on the task object.


config = new EngineConfig( );
config.setBIRTHome("C:\\birt\\birt-runtime-2_3_1\\birt-runtime-2_3_1\\ReportEngine");
config.getAppContext().put("OdaJDBCDriverClassPath", "c:/birt/mysql/mysql-connector-java-5.0.4-bin.jar");
Platform.startup( config );
IReportEngineFactory factory = (IReportEngineFactory) Platform.createFactoryObject( IReportEngineFactory.EXTENSION_REPORT_ENGINE_FACTORY );
engine = factory.createReportEngine( config );

Monday, November 03, 2008

Getting BIRT Source

Back in September I blogged about BIRT Team Project Sets.  Last week the BIRT team was able to finish up implementing Team Project Sets.  Instructions on how to use a team project set are available here.

The project set to pull from HEAD is available here.

All of the BIRT releases, milestones, and release candidates now have team project sets are also available.  To get the team project set files go to the birt download page for the release you are interested in.  From there select the full BIRT 2.3.1 Download Page link (each named download has its own link).  At the bottom of the full download page, you will find a link to the Project Set File at the bottom of the page.

Sunday, November 02, 2008

Showing BIRT Reports using the Actuate JSAPI

Actuate is preparing to release a new version of several products that are based on the open source BIRT project. One the coolest new features is a new JavaScript API that can be used to execute and display report content with virtually any front end framework.

This API allows report content to be inserted into any DIV element, and contains methods for parameter manipulation, data extraction, and displaying the viewer. The viewer component has complete functionality for table of contents, exporting reports to different formats, and even the ability to modify sorting, filtering, and grouping from within the browser.

Wenbin, Virgil and I wrote an article on how this API can be used. It is available here. If you are interested in trying out this new API, take a look at the Actuate 10 wiki page located on BIRT Exchange.

Monday, October 27, 2008

BIRT Properties

If you have done much API level programming with BIRT you have probably found yourself in need of the name and values for a property.  This happens if you are working with either the Java or JavaScript modes of BIRT.

The good thing is that the properties that are used in BIRT are defined someplace in the Java code.  The hard part is finding exactly where the definition has been placed.  I have created a crib sheet with the classes that contain the most common property names on the wiki.  You can take a look here .

If you have been working with the BIRT APIs and have found similar information that might be useful, please feel free to update the wiki with your hints.

Monday, October 20, 2008

Dynamically adding a series to a BIRT Chart

Often it is a requirement to customize report output for specific users. Generally this is accomplished with parameters and some form of conditional logic within the report itself. This approach can be used with BIRT as well. BIRT supports JavaScript expressions and has an event model that allows report customization with handlers written in either Java or JavaScript. For more information on the event model see the scripting primer.

To illustrate modifying a BIRT chart at runtime, assume we have a bar chart that we wish to add additional line series to, based on some report parameter. This can be accomplished by creating an event handler for the beforeFactory event and modifying the report prior to it running.


cht = reportContext.getDesignHandle().findElement(“MyChart”);

This line of code requires that the chart be named MyChart in the General tab of the Properties View. This line of code gets a handle to the Chart report item. Once the chart report item is located we then can retrieve the actual chart model. The chart model is de-serialized from the xml within the report design and stored in a property called chart.instance. So to get the model, you can use code like the following:

mychart = cht.getReportItem().getProperty(“chart.instance”);

Now that we have the actual chart model, we can use standard chart engine API calls to modify it prior to the report running. Remember to import the CE API packages prior to using them. See the final example for more details.

Since we have a bar chart, the chart model will be an instance of the ChartWithAxesImpl class. This class has methods for getting the chart axes and adding series definitions. If we define a static report parameter that allows multi-selection, with values that correspond to the row values we want to map with line series, the following code will add the appropriate series.



//Get the x axis
xAxis =mychart.getAxes().get(0);
//Get the first y axis
yAxis1 = xAxis.getAssociatedAxes().get(0);
alternate_label_position = false;

for( i=0; i < params["SelectSeriesToAdd"].value.length; i++){
var sdNew = SeriesDefinitionImpl.create();
var ls = LineSeriesImpl.create();
ls.getLabel().setVisible(true);
if( alternate_label_position ){
ls.setLabelPosition( Position.BELOW_LITERAL );
alternate_label_position = false;
}else{
ls.setLabelPosition( Position.ABOVE_LITERAL );
alternate_label_position = true;
}
var qry = QueryImpl.create("row[\"" + params["SelectSeriesToAdd"].value[i] + "\"]" );
ls.getDataDefinition().add(qry)
sdNew.getSeries().add( ls );
yAxis1.getSeriesDefinitions().add( sdNew );


The for-loop iterates over the selected values within the parameter and creates a Line series for each value. The value of each parameter is used as the query for the new series. Finally the new series definition is added to the first y axis.

The final code the beforeFactory event handler looks like:

importPackage(Packages.org.eclipse.birt.chart.model.data.impl);
importPackage(Packages.org.eclipse.birt.chart.model.component.impl);
importPackage(Packages.org.eclipse.birt.chart.model.type.impl);
importPackage(Packages.org.eclipse.birt.chart.model.attribute);

//Chart must be named
cht = reportContext.getDesignHandle().findElement("MyChart");
mychart = cht.getReportItem().getProperty( "chart.instance" );


//Get the x axis
xAxis =mychart.getAxes().get(0);

//Get the first y axis
yAxis1 = xAxis.getAssociatedAxes().get(0);

//alternate the series label position of each new series
alternate_label_position = false;

for( i=0; i < params["SelectSeriesToAdd"].value.length; i++){

//create a new series definition for each new series
var sdNew = SeriesDefinitionImpl.create();
//create a new series for each parameter selection
var ls = LineSeriesImpl.create();
ls.getLabel().setVisible(true);
if( alternate_label_position ){
ls.setLabelPosition( Position.BELOW_LITERAL );
alternate_label_position = false;
}else{
ls.setLabelPosition( Position.ABOVE_LITERAL );
alternate_label_position = true;
}
//Create the query. The row value must correlate with the data set row value
var qry = QueryImpl.create("row[\"" + params["SelectSeriesToAdd"].value[i] + "\"]" );
ls.getDataDefinition().add(qry)
sdNew.getSeries().add( ls );
//Add the new series definition to the first y axis
yAxis1.getSeriesDefinitions().add( sdNew );
}




The complete example is located here.

For an example of resizing a chart dynamically see
this.

Friday, September 19, 2008

Team Project Sets For BIRT

To start with the obvious, one of the advantages of an OSS project is that you use the source code to study, modify, or improve the software as you see fit.  In a large project, like BIRT, it can be difficult to get the source. Worse, the documentation for fetching the source changes with each build.  No one likes to writes docs, but having to re-write docs is particularly tedious.

In the Eclipse world there are two mechanisms that can be used to help tame these project structures and make it easier to get and build the source code from the version control systems.  The release engineering (releng) systems is a comprehensive system that is used to fetch, build and release the Eclipse projects, including BIRT.  Unfortunately, the releng process has a relatively high barrier to entry for someone that is un-familiar with the Eclipse process (its complicated).

The more user focused mechanism for getting a collection of projects from a version control system is the Team Project Set.  Developers can create a Project Set File (PSF) that describes the repositories, projects, and version tags that make up a collection.  Users can use the Import => Team => Team Project Set functionality in the Java Perspective to fetch the projects specified in the PSF.

I have created a simple program that uses the map files that are core to the RelEng process to build PSF files for the BIRT project.  I am working to get this added to the BIRT version control system, but I am hoping to get some feedback on the files before we commit this to the BIRT project.

Here are the direct links to the PSF files:

To use the team project sets files.  Download the file to your local machine.  Put the file into a Java project and open the Java Perspective.  Right click on the file an select Import.  There will be an option in the context menu to Import Project Set.. I don't recommend using that option since you can't run the import in the background.  
When you click Next, you will be taken to a dialog with the name of the .psf file in it.  You will probably want to select the "Run The Import in the Background" option.  You may get a pop-up dialog asking about the repository, if you do simply select the repositories and off it goes. 
The all_projects.psf will fetch close to 200 projects, so you may want to be careful choosing this option.

I currently have the PSF files set up to pull the current release candidates from BIRT and DTP respectively.

   BIRT_2_3_1_RC2_20080910
   DTP_1_6_1_M1_20080725

If you would like to pull a different version of the source you have a couple of options.  First you can pull the PSF files and then just do a string substitution on the version tags.  The other option is to use a subversion client (subclipse or subversive) to check out the conversion project from version control. The project that builds the PSf files is located on my subversion server at http://longlake.minnovent.com/repos/birt_example.

Next you need to pull the org.eclipse.birt.releng project from the dev.eclipse.org server using the /cvsroot/birt repository.  The project can be found under the Head/source folder, anonymous will work as the user name.  Remember you will want to pull the version of the releng project that matches the version you are trying to get from CVS.  

Now you have to open up the ConvertMapToPsf.java file and change lines 30 and 31 to use the appropriate CVS version tags.  You will need to lookup the tags from CVS.  The other popular tag you will want is the BIRT Ganymede Release 2.3.0:

    BIRT_2_3_0_Release_200806181122
    DTP_1_6_release_200806181028
Once you have the tags set, and you have gotten the org.birt.eclipse.releng project, simply run the ConvertMapToPsf.java file as an application and you will see the PSF files.

If you have any problems with the Team Project Sets let me know.  Hopefully we will have this in the product and a part of the BIRT web site soon, your feedback will be helpful.

EDITED TO ADD: Is anyone using stackoverflow.com?  I have been following this startup question and answer site for a while.  If you have questions or comments maybe you want to post them on the related post at stackoverflow.com.  Here is the Post

Thursday, September 11, 2008

Naming Exported files from the BIRT WebViewer

A common question we get on the BIRT news group is how to change the name of an exported document. For instance, exporting to PDF, the developer may wish to have more control on what filename is used for the export. Currently the name used is just the report name followed by the emitter extension (eg MyReport.pdf).

BIRT 2.3.1 which will be released later this month now supplies a solution to this problem. The example web viewer has a setting that can be added to the web.xml that allows you to specify a Java class that will be responsible for generating the name.



<!-- Filename generator class/factory to use -->
<context-param>
<param-name>BIRT_FILENAME_GENERATOR_CLASS
<param-value>org.eclipse.birt.report.utility.filename.DefaultFilenameGenerator
</context-param>








The class specified must implement the IFilenameGenerator interface, which has one method named getFilename. This method is passed four parameters.

baseName – Contains the base filename for the report, with no extension provided.
fileExtension – The extension for the selected operation (ppt for export to PowerPoint).
outputType – The operation being executed. More on this parameter later.
options – Specific options for the operation.

The instance of the IFilenameGenerator is called in multiple locations within the example viewer. When you export the report:



When you export the report data:



And when you use the /document servlet mapping, for example:

http://localhost:8080/WebViewerExample/document?__report=OrderDetails.rptdesign





This URL will run the report and download the rptdocument.

Suppose you wish to have the date in your filename, when exporting the report. To do this, create a class with the following code:



package my.filename.generator;
import java.util.Date;
import java.util.Map;
import org.eclipse.birt.report.utility.filename.*;
public class MyFilenameGenerator implements IFilenameGenerator{

public static final String DEFAULT_FILENAME = "BIRTReport";

public String getFilename( String baseName, String extension, String outputType, Map options )
{
return makeFileName( baseName, extension );
}
public static String makeFileName( String fileName, String extensionName )
{
String baseName = fileName;
if (baseName == null || baseName.trim().length() <= 0)
{
baseName = DEFAULT_FILENAME;
}

// check whether the file name contains non US-ASCII characters
for (int i = 0; i < baseName.length(); i++) {
char c = baseName.charAt(i);

// char is from 0-127
if (c < 0x00 || c >= 0x80) {
baseName = DEFAULT_FILENAME;
break;
}
}

// append extension name
if (extensionName != null && extensionName.length() > 0) {
baseName += (new Date()).toString() + "." + extensionName;
}
return baseName;
}
}





If you check the source, you will notice this is just the default class with one modification.


baseName += (new Date()).toString() + "." + extensionName;


Which just inserts the date into the output.

You can also check the operation type if you wish to set the name based on the operation. Currently the available options for outputType are:

IFilenameGenerator.OUTPUT_TYPE_EXPORT – When exporting report to one of the supported formats.
IFilenameGenerator.OUTPUT_TYPE_DATA_EXTRACTION – When exporting report data.
IFilenameGenerator.OUTPUT_TYPE_REPORT_DOCUMENT – When using the document servlet mapping.

This example is located here.

Vincent Petry from the dev team has also uploaded a Birt Viewer 2.3 User Reference, which describes the settings and parameters available with the example web viewer. This document is informative and is located here.

If you wish to build your own version of the filename generator, make sure to include viewservlets.jar from the WebViewerExample\WEB_INF\lib directory in your build path.

Wednesday, August 20, 2008

Deploying BIRT to a JBoss Portlet


BIRT uses OSGi plugins to implement most of its functionality. This works great in most cases, but when deploying to servers, configuration can be problematic especially if you are new to OSGi. If you have used BIRT’s Report Engine API before you are probably familiar with setting BIRT_HOME or using the EngineConfig.setBirtHome method. This method essentially just points to the location of the BIRT plugins.

The BIRT EngineConfig class also has a method for setting the PlatformContext (setPlatformContext). This method takes an instance of a class that implements the IPlatformContext interface. This interface is fairly simple in that it has only one method, named getPlatform which returns the path that contains the BIRT plugins. BIRT provides two default implementations of this interface (PlatformServletContext and PlatformFileContext). If the setPlatformContext is never called, BIRT defaults to a PlatformFileContext, which looks for a BIRT home location that is either set using:

System.setProperty(“BIRT_HOME”, locationtobirtplugins);
Or
EngineConfig.setBIRTHome(locationtobirtplugins);

The PlatformServletContext class is used when deploying BIRT to a web application and uses resource based operations for locating the plugins which can be included in the application. For an example of using the PlatformServletContext go
here.

This brings us the intent of this post “Deploying BIRT to a JBoss Portlet”. Within the BIRT wiki is an example of deploying BIRT to a portlet. The example is located
here.

This example works fine, but not with JBoss’s Portal Server. The reason for this is in that example the ServletContext is used to locate the BIRT plugins. Within a JBoss Portlet, I found no ready way of retrieving the ServletContext. It may be possible, but instead of going that route I decided to implement my own version of the IPlatformContext interface, which would use the PortalContext to locate the BIRT plugins.

In the example I built, I just copied the PlatformServletContext class from the BIRT source and modified it to take a PortletContext instead of a ServletContext. Being that it was only two small changes I will not post it here, but it is in the example. I then modified the example BirtEngine.java class from the portlet example above to create an instance of my new PlatformPortletContext class and set it in the engine config with the following code:



IPlatformContext context = new PlatformPortletContext( pc );
config.setPlatformContext( context );

try
{
Platform.startup( config );
}
catch ( BirtException e )
{
}


I also modified the example above to use the PortletContext to set image directories and base image url. The Portlet code looks like this:



package org.eclipse.birt.examples;
import javax.portlet.GenericPortlet;
import javax.portlet.PortletException;
import javax.portlet.RenderRequest;
import javax.portlet.RenderResponse;
import javax.portlet.UnavailableException;
import java.io.IOException;
import java.io.PrintWriter;

//+++++++++++++BIRT
import org.eclipse.birt.report.engine.api.EngineConfig;
import org.eclipse.birt.report.engine.api.IReportEngine;
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 java.util.logging.Level;
import org.eclipse.birt.report.engine.api.EngineConstants;
import org.eclipse.birt.report.engine.api.HTMLRenderOption;
import org.eclipse.birt.report.engine.api.IReportRunnable;
import org.eclipse.birt.report.engine.api.IRunAndRenderTask;
import org.eclipse.birt.report.engine.api.IReportEngine;
import org.eclipse.birt.report.engine.api.HTMLServerImageHandler;
import java.util.logging.*;
public class JbossBirtPortlet extends javax.portlet.GenericPortlet {
/**
*
*/
private static final long serialVersionUID = 1L;

/**
* Constructor of the object.
*/
private IReportEngine birtReportEngine = null;
protected static Logger logger = Logger.getLogger( "org.eclipse.birt" );
public JbossBirtPortlet() {
super();
}
/**
* Destruction of the portlet.
*/
public void destroy() {
super.destroy();
BirtEngine.destroyBirtEngine();
}
protected void doView(RenderRequest rRequest, RenderResponse rResponse) throws PortletException, IOException, UnavailableException {
rResponse.setContentType("text/html");
this.birtReportEngine = BirtEngine.getBirtEngine(rRequest.getPortletSession().getPortletContext());
logger.log( Level.FINE, "image directory " + rRequest.getPortletSession().getPortletContext().getRealPath("/images"));
IReportRunnable design;
try
{
//Open report design
design = this.birtReportEngine.openReportDesign( rRequest.getPortletSession().getPortletContext().getRealPath("/Reports/TopNPercent.rptdesign"));
//create task to run and render report
IRunAndRenderTask task = birtReportEngine.createRunAndRenderTask( design );
//set output options
HTMLRenderOption options = new HTMLRenderOption();
options.setOutputFormat(HTMLRenderOption.OUTPUT_FORMAT_HTML);
options.setImageHandler( new HTMLServerImageHandler() );
options.setOutputStream(rResponse.getPortletOutputStream());
options.setBaseImageURL(rRequest.getContextPath()+"/images");
options.setImageDirectory(rRequest.getPortletSession().getPortletContext().getRealPath("/images"));
task.setRenderOption(options);
//run report
task.run();
task.close();
}catch (Exception e){
e.printStackTrace();
throw new javax.portlet.PortletException( e );
}
}

/**
* Initialization of the portlet.
*
* @throws PortletException if an error occure
*/
public void init() throws javax.portlet.PortletException {
BirtEngine.initBirtConfig();
}
}






A couple of things to note: Doing it this way requires that the BIRT plugins be included as part of the Portlet WAR. This can create big war files. A better approach may be to deploy the BIRT plugins to a hard location on the system and use the system property and the default PlatformFileContext class to set the BIRT home. I also had to up the PermGen space for the JBoss AS. If you wish to download the example it is located here. Be sure to read the readme for instructions on building the WAR. The example is based off of the JBoss sample HelloWorld portlet and is located
here.

Wednesday, June 25, 2008

BIRT 2.3 Released



BIRT 2.3 has arrived and with this release many new features are available. Most notable are the JavaScript improvements that include an improved JavaScript Editor (validation, line numbers, code folding), a new JavaScript Debugger, and the ability to add JavaScript files to a report design using the GUI. Also new scripting events are available for crosstabs and charts. BIDI support is also provided in all output formats. Many crosstab enhancements were made, including the ability to display measure data using charts within the crosstab. With BIRT 2.3 crosstabs support derived measures as well.


To read more about the new BIRT 2.3 features see the BIRT New and Notable Features.

To view a webinar on the new feature, check out this Eclipse Live recording. The examples used in this webinar are available on
BIRT-Exchange.

Tuesday, June 10, 2008

BIRT - Help Wanted

The majority of the work on the BIRT project is done by the core BIRT committers. At the same time, we recognize that we have a huge community of users and integrators that may want to dedicate some time back to our community.

Unfortunately, it is a little difficult to find a place to start. Yes, there are a lot of open issues in Bugzilla, but it is difficult to determine which items would be best to take on. Unless you are well versed on the internals of BIRT, it is difficult to tell which items are easy, and which are difficult. To complicate things a bit, those items that are easy and relatively high priority tend to get fixed pretty quickly.

In an effort to provide better entry points for people that are interested in contributing code to the BIRT project, we have started to tag bugzilla entries with the keyword "helpwanted". You can find the bugs by using this search. The assumption is that discussion and contributions towards the resolution would be handled through Bugzilla.

If you do come up with a contribution for the BIRT team, please make sure that you select the IP review box if you are submitting code. Also please note that the helpwanted tag is new effort for the BIRT team, and will be a bit of a work in progress.

One other friendly reminder is that the BIRT team is in the closing cycles of the Ganymede release at the end of this month. If you decided to get active, please understand that the entire development team is very focused on the release and may take a while to respond to your contributions and questions.

Friday, May 23, 2008

Designing High Performance BIRT Reports

Over the last couple of months BIRT-Exchange has hosted many webinars explaining and demonstrating BIRT technology. Last weeks webinar featured Mica Block discussing BIRT performance. In this presentation, Mica explains how to gauge performance and provides tips for improving generation time. If you missed it or any of the others, they are available
here.

In addition the following topics will be discussed in the future:

5/30/2008
Using the BIRT Report Engine API
Virgil Dodson

6/13/2008
Using the BIRT Design Engine API
Jason Weathersby

6/27/2008
What's New with BIRT 2.3
Virgil Dodson

7/11/2008
Ad-hoc BIRT Reporting for End Users
Rob Murphy

7/25/2008
BIRT Charting Primer
Virgil Dodson

8/8/2008
Using the BIRT Chart API
Jason Weathersby

Thursday, May 22, 2008

Embed HTML

So this is a really simple problem that Jason solved for me. If you have HTML in your database and you want to embed it into your report as HTML you need to do a few things. First start with a Text control.

1) Change the top drop-down to HTML

2) Change the second drop-down to Dynamic Text

3) Click on the tag and use the expression builder to select the appropriate field.





4) Manually insert the attribute format="HTML" into the VALUE-OF entity













If you follow these steps your HTML text will show up in your report document as formatted text that obeys the HTML rules.


Monday, May 19, 2008

Java Event Handler ClassPaths

One of the most common questions that comes up when developing Java Event Handlers is:

"Where do I put my classes?"

Rather than write it up here, I added an entry to the BIRT FAQ here.

Friday, May 09, 2008

BIRT: Swapping Data Set at Runtime

Often it is a requirement to design a report against a development database and then switch the connection at runtime. This can be done in many ways with BIRT. The options include:

JNDI - Look up the data source.
Property Binding - Swap the data source at runtime using property binding expression.
Script – Write a script in the beforeOpen event handler to modify the data source properties.
Connection Profiles – Store the connection information in a shared profile.
DTP driverBridge extension – Implement a driverBridge extension to intercept calls to a specific driver.
Design Engine API (DE API) - Swap the dataset property on a specific table before running the report.

Using the DE API often serves as a useful alternative, because the data sources may be different types. For example your deployed report may use a scripted data source to retrieve some EJB value, but at design time you may not have access to this data source. So you could develop a stubbed data source that uses JDBC to test your report layout and when deployed swap the data set on elements that are bound to the data set. To do this you need to do a few things. First name your elements that use the data set to be swapped. This can be set in the general properties for each report element. Next make sure your two data sets (one for design time and one for deploy time) have the same result columns. If the result columns are not the same much more work needs to be done in order to implement this solution. Finally implement a beforeFactory script that modifies the report design before it is executed. Assume we have two data sources/data sets and one table. If I name my table “mytable” and my data sets are named “viewer” and “designer” the beforeFactory event script would look as follows:



des = reportContext.getHttpServletRequest().getParameter("__designer");

if( des == null || des == "false" ){
mytable = reportContext.getReportRunnable().designHandle.getDesignHandle().findElement("mytable");
mytable.setProperty( "dataSet", "viewer" );
}


Note that the “designer” data set is the default data set used in the report. This is the data set that you should drag to the canvas to build your report


The first line of code determines whether we are using the designer. The designer adds the __designer parameter to URL when the report is previewed. This parameter will not exist when deployed. If this parameter does not exist or is set to false we assume that the report is deployed. This line:



reportContext.getReportRunnable().designHandle.getDesignHandle()



returns the report design handle. BIRT 2.3 has an easier way of getting the report design, but this method should work for BIRT 2.2 and 2.3. We then use the report design handle to locate the table element to be modified. Finally we set the dataSet property to the viewer dataset.


An example report illustrating this concept is located here.

Friday, May 02, 2008

BIRT Drill Through

BIRT supports drilling down from a master report to a detail report. To facilitate this feature, BIRT provides a Hyperlink Options builder.



To drill from a table data element, the developer can select the data element, choose the hyperlink property and click on the ellipsis. This will launch the Hyperlink builder. Once the builder is launched, select the Drill-through radial, select either a report design or report document to drill to and supply any needed parameters. Parameters are often based on the current row value of the master table. That is all that is needed to create a master detail report, but suppose you want the detail report to be based on report parameter or some other variable. One way of accomplishing this task is to call the Design Engine API from script and modify the design at runtime.

For example suppose you have a master report and you want the detail report to be based on report parameter. You could define a report parameter for the master report that contains the name of different report designs to be used as a detail. The follow report parameter illustrates doing this with a static list box parameter. Its values are DetailOne and DetailTwo.



Next select the data element that you want to link, select general properties and name the element. In this example it is named mydataelement.




Finally enter script similar to the following in the beforeFactory script event.
importPackage(Packages.org.eclipse.birt.report.model.api);
importPackage(Packages.org.eclipse.birt.report.model.api.elements);


dataHandle = reportContext.getReportRunnable().designHandle.getDesignHandle().findElement("mydataelement");

ac = StructureFactory.createAction();
ah = dataHandle.setAction( ac );
ah.setLinkType("drill-through");
ah.setReportName( params["detailrpt"].value +".rptdesign");
ah.setTargetFileType("report-design");
ah.setTargetWindow("_blank");
pb = StructureFactory.createParamBinding();
pb.setParamName("order");
pb.setExpression("row[\"ORDERNUMBER\"]");
ah.addParamBinding(pb);




This code creates a drill-through hyperlink on the fly and sets the detail report based on the report parameter with this line of code:




ah.setReportName( params["detailrpt"].value +".rptdesign");





This example is available at
BIRT Exchange.

Friday, April 25, 2008

Data Driven Reports

Last week I posted about how to debug reports using a tester application. As part of that exercise I showed a dynamic table report where the number of columns in the report was driven by the data. This week, I'm going to examine how we access the database to determine the dynamic column values in that example.

BIRT event handlers have three distinct phases:

- BeforeFactory / OnPrepare
- OnCreate
- OnRender

If you want to modify the design of the report (add columns, drop columns, etc.) it has to be done in the beforeFactory/OnPrepare method. At this time you can get access to the DesignElementHandle objects that make up the report design and modify appropriately. The problem is that the report does not have any access to the data at this time. The DataSources and DataSets have not been run at this time.

In theory, one could get a handle to the DataSource / DataSets and manually instantiate and run them, but it's not pretty. You would need to write a lot of code. I have entered a bugzilla entry to rectify this situation, but I am not sure when or if that will be a feature we can add (volunteers? anyone? anyone?).

So how do you get access to data in the beforeFactory/OnPrepare method? Well it turns out that it is relatively easy to use the Report Engine API to access parameters through the ParameterDefinitionTask. Parameters in turn can be tied to a DataSet. So it seems like we have a route to access data from the OnPrepare method.

To demonstrate, I will create a simple report that combines both the REAPI and DEAPI to dynamicaly create a column for each type of order status and in that column will be a label for the column type. As stated before, the source code for this example can be found on my subversion server at http://longlake.minnovent.com/report/birt_example. The projects that you will need are:
- birt_api_example
- birt_runtime_lib_222
- script.lib

The first thing we are going to do is to build a simple report that gets all of the orders and separates them by the order status. If you create a data source that uses classic models and add a data set named setOrders with the following SQL.
select status from orders

Drag the data set onto the report layout and you will have a table with all of the statuses. Now it is trivial to create a report that summarizes the count for each status as a row. What we would like to do instead is have the statuses be columns for this report. This means that we want to take our one column report at design time, and make it an N columns at runtime. Since we are not concerned with the detail columns, you can delete the detail row from the table for now.

This leaves you with a report with only one column and one cell that holds a label which says Status. I like to start simple.



To make this happen we need to have a different data set that returns all of the unique statuses, name the data set setStatus and make the sql:

select distinct status from orders

As stated before, the easiest way to get to this dataset's values in the OnPrepare method is to wrap the DataSet inside a parameter. So create a new parameter named pStatus. Make it a combo and add setup the dataSet to be the setStatus that we just defined. For the value field use row[0] which is a standard field that will return the row number for each row. Make the Display field equal to row["status"] as shown in the accompanying screen shot.



Once you have the parameter setup, you are ready to start writing your event handler. If you want to test whether you have the parameter setup correctly, run the report and click on the Show Report Parameters button at the top of the preview panel. It should show you seven order statuses.

Now we need to add an event handler to the report. We could add the event handler in a number of locations. We could add it to the label, the cell which hold the label, the column, or the table. It makes sense to add the event handler to the table for a couple or reasons. The most basic reason is that we may want to support more than one dynamic column within this table.

To create a table event handler, create a new java class that extends the TableEventAdapter. We want to create this class in the script.lib project so that we can debug the code as we produce it.

package deapi_event_handler;

import org.eclipse.birt.report.engine.api.script.eventadapter.TableEventAdapter;

public class DynamicTable extends TableEventAdapter {

}



If you right click in the java class and select Source -> Override/Implement Methods and then select the OnPrepare method in the dialog you will have the method that we need to override. The first thing we want to do is to get the values that have been exposed through our parameter. I have created a helper class ParameterUtil that has methods to get either a list of values using the ParameterDefinitionTask.

public static Collection<IParameterSelectionChoice> getParameterValues(String parameterName, IReportContext reportContext) {
IGetParameterDefinitionTask task = null;
HashMap curParams = new HashMap();
IReportRunnable runnable = reportContext.getReportRunnable();
try {
task = runnable.getReportEngine().createGetParameterDefinitionTask(runnable);

// get the names of all the parameters
Collection paramRefs = task.getParameterDefns(false);

// for each parameter name, get the parameter value
// add the name and value to a hashmap
for (Iterator iterator = paramRefs.iterator(); iterator.hasNext();) {

ParameterDefn pDefn = (ParameterDefn) iterator.next();
String name = pDefn.getName();
Object curP = reportContext.getParameterValue(name);

curParams.put(name, curP);
}

// set the parameter values for this task from the hashmap.
task.setParameterValues(curParams);

// get the parameter that is tied to this table.
IParameterDefnBase scalar = task.getParameterDefn(parameterName);
if (scalar instanceof IScalarParameterDefn) {

// bind the parameters to the query text
task.evaluateQuery(scalar.getName());

// get the values for this parameter from its DataSet
Collection<IParameterSelectionChoice> paramChoices = (Collection<IParameterSelectionChoice>) task
.getSelectionList(scalar.getName());

return paramChoices;

}
} catch (Exception e) {
System.out.println("Failure to get parameters");
} finally {
task.close();
}
return null;
}


As you can see, this method takes a string which names the parameter we are going to use and the report context and returns the values/labels exposed by that parameter. So if we want to use that method we have to figure out which parameter holds the values that we want to show as columns.



To do this, I am going to add a NamedExpression to my table. Once I have defined a NamedExpression on my table, I can look up the name of that expression from the script and use that in my ParameterUtil method. To add a NamedExpression to a table, to to the PropertyEditor for the table and select the NamedExpression tab.

In addition to creating the named expression, you will need to specify that this table will use the Event Handler we created in a previous step.

So now I will modify my OnPrepare method to first get the named parameter value and then to get the values defined by that expression.





@Override
public void onPrepare(ITable tableScript, IReportContext reportContext) {

String paramName = tableScript.getNamedExpression("dyn_parameter");

Collection<IParameterSelectionChoice> colNames = ParameterUtil.getParameterValues(paramName, reportContext);

for (IParameterSelectionChoice paramChoice : colNames) {
System.out.println(paramChoice.getValue() + ": " + paramChoice.getLabel());
}

}




At this point, you should be able to run the report using the tester. If all goes well, you should see the following in your console.

home: C:\BIRT\ws_ec2008\birt_api_example\Reports
1 property file(s) found
output: C:\BIRT\ws_ec2008\birt_api_example\tester_output_20080425_171337\data_driven.
Executing tester_run\data_driven.properties
Start run
0: Disputed
1: On Hold
2: In Process
3: Shipped
4: Cancelled
5: Resolved
C:\BIRT\ws_ec2008\birt_api_example\tester_output_20080425_171337\data_driven.: built. Start render.
Run duration = 2531 ms
Render duration = 641ms
Success 1 : tester_run\data_driven.properties
Finished Fri Apr 25 17:13:45 CDT 2008


So what have we accomplished? So far, we have a simple report that is able to dynamically get a list of values from a query defined within the report at runtime, in the onPrepare method. Now that we have this working, we can start on the next step which is to use those values to dynamically create new columns in the report.

Next week, I will examine how you can use the Design Engine API to do just that for both labels and data items.

Wednesday, April 23, 2008

BIRT Resizing Charts

Note: I have added the reports that are used in this post to the subversion repository http://longlake.minnovent.com/repos/birt_example
the project is birt_api_example
and the reports can be found in /Reports/charts
Alternatively you can just go to:
http://longlake.minnovent.com/repos/birt_example/birt_api_example/Reports/charts/
navigate into the charts and cut and paste into your report designs

When creating charts in BIRT the size is set automatically. The chart can be resized with the mouse after the chart is in the report as well. This is fine if you know before hand the amount of data your chart is going to display. Currently there are several bugzilla entries tracking chart resize features, but I thought I would offer a work around until those entries are completed. Presented below are two options for resizing the chart. Using the simple chart API, and using the afterDataSetFilled chart event to modify the size of the chart based on the data retrieved.

One way of resizing the chart is to use the chart simple API within the beforeFactory event. The simple api allows simple properties of the chart to be changed before the report is executed.

Some examples include setting titles, chart dimensions, and output formats:


var chart1 = this.getReportElement( "Chart1" )
var chart2 = this.getReportElement( "Chart2" )
var chart3 = this.getReportElement( "Chart3" )
var chart4 = this.getReportElement( "Chart4" )

var color1 = chart1.getTitle().getCaption().getColor()

chart1.setColorByCategory( true );
chart1.getTitle().getCaption().setValue( "Color by Category" );
chart1.setColorByCategory( true );
color1.setRed( 255 );
color1.setGreen( 0 );
color1.setBlue( 0 );
chart1.getTitle().getCaption().setColor( color1 );

chart2.setDimension( "ThreeDimensional" );
chart3.getCategory().setSorting( "Descending" );
chart4.setOutputType("PNG");


In this example the charts must be named. Select the general properties for the chart and in the name field enter a value.

The size of the chart can be set using the following code in the before factory.


var mychart = this.getReportElement( "mychart1" );
if( reportContext.getOutputFormat() == "pdf" ){
mychart.setWidth("3in");
mychart.setHeight("3in");

}



In this example we resize the chart based on output format.

If you want the chart to resize based on the data populated in the chart data sets it requires a little bit of a work around. First, name the chart as with the simple chart API and enter the following code in your beforeFactory event handler.



var mychart = this.getReportElement( "mychart" );
mychart.setWidth("");
mychart.setHeight("");



After you have done that, enter code similar to the following in your afterDataSetFilled chart event handler.


function afterDataSetFilled(series, dataSet, icsc)
{
if( series.getSeriesIdentifier() == "seriesone" ){
if( dataSet.getValues().length > 4 ){
icsc.getChartInstance().getBlock().getBounds().setWidth(800);
icsc.getChartInstance().getBlock().getBounds().setHeight(600);
}else{
icsc.getChartInstance().getBlock().getBounds().setWidth(400);
icsc.getChartInstance().getBlock().getBounds().setHeight(300);

}
}
}


In this example we have named our series “seriesone” on the third tab of the chart wizard.

If the category values contain more than four entries we double the size of the chart. The sizing could be more dynamic, for example we could use the list size to increment a delta per category as well.

This example works with BIRT 2.2.2, but will not work with the chart output type set to SVG, when rendering to HTML.

For Birt 2.3, everything can be done in the beforeGeneration script and it does work with SVG. No other scripts are needed.


function beforeGeneration( chart, icsc )
{
xAxis = chart.getBaseAxes()[0];
yAxis = chart.getOrthogonalAxes( xAxis, true )[0];
seriesDef = yAxis.getSeriesDefinitions().get(0);
runSeries = seriesDef.getRunTimeSeries()[0];
//Retrieve list of data values
list = runSeries.getDataSet().getValues();
if( list.length > 3){
chart.getBlock().getBounds().setHeight(250);
chart.getBlock().getBounds().setWidth(600);
}else{
chart.getBlock().getBounds().setHeight(100);
chart.getBlock().getBounds().setWidth(600);
}
}



Friday, April 18, 2008

Return To Eureka

Two weeks ago I had a Eureka moment. In that post I described how I was able to use reflection to get the DesignElementHandle from an ICell object. It was a fairly simple solution since there was only one super class to the ICell.

Yesterday, I started to re-factor my other example classes to use this helper utility and I figured out that the helper utility need a little more work. It turns out that the ITable class is a bit more complicated then the ICell class. For the ITable object there are five levels super classes to get back to the DesignElement class.

So the ITable object actually resolves to:

  • class org.eclipse.birt.report.engine.script.internal.element.Table
The hierarchy for this class is:
  • class org.eclipse.birt.report.engine.script.internal.element.Listing
  • class org.eclipse.birt.report.engine.script.internal.element.ReportItem
  • class org.eclipse.birt.report.engine.script.internal.element.ReportElement
  • class org.eclipse.birt.report.engine.script.internal.element.DesignElement
When we use reflection to find the protected field we have to navigate all the way back to DesignElement which can be n levels up the hierarchy. So we need to use a loop to search for the parent.

Once we have the DesignElement we can use reflection to get access to the protected field for the simple api DesignElement. The simple api has the same hiearchy issue, with one additional super-class.
  • class org.eclipse.birt.report.model.simpleapi.Table
  • class org.eclipse.birt.report.model.simpleapi.Listing
  • class org.eclipse.birt.report.model.simpleapi.MultiRowItem
  • class org.eclipse.birt.report.model.simpleapi.ReportItem
  • class org.eclipse.birt.report.model.simpleapi.ReportElement
  • class org.eclipse.birt.report.model.simpleapi.DesignElement
Fortunately, the solution is not that difficult. The addition of one relatively simple loop allows us to find the parent classes we need.


private static final String DESIGN_ELEMENT_NAME = ".DesignElement";

private static Class findDesignElementParent(Class childClass) throws NoSuchFieldException {
System.out.println(childClass.toString());
do {
childClass = childClass.getSuperclass();
if (childClass == null)
throw new NoSuchFieldException("Did not find DesignElement");
System.out.println(childClass.toString());
} while (!childClass.toString().endsWith(DESIGN_ELEMENT_NAME));
return childClass;
}


You may ask scratch your head and ask why am I searching for the class using the name of the class as opposed to a reference to the actual class name. This goes back to the issue that the classes that we are referencing are not actually exposed through the runtime interface.

In any case, once I have made these changes my utility method is still relatively simple.

public static DesignElementHandle getDesignElementFromScript(IDesignElement scriptObj)
throws IllegalAccessException, SecurityException, NoSuchFieldException {

Class scriptParentClass = findDesignElementParent(scriptObj.getClass());
Field fieldFromScript = scriptParentClass.getDeclaredField("designElementImpl");
if (fieldFromScript == null) {
return null;
}

// Access the DesignElement field from the Script object
fieldFromScript.setAccessible(true);
Object designElement = fieldFromScript.get(scriptObj);

// The DesignElement object has a field named "handle"
// which is the DesignElementHandle for that DesignElement
// Use reflection again to get this field
Class simpleApiParentClass = findDesignElementParent(designElement.getClass());
Field fieldFromSimpleApi = simpleApiParentClass.getDeclaredField("handle");
if (fieldFromSimpleApi == null) {
return null;
}

// Now access the DesignElementHandle from the DesignElement
fieldFromSimpleApi.setAccessible(true);
return (DesignElementHandle) fieldFromSimpleApi.get(designElement);
}


The complete source is available in the script.lib project on the http://longlake.minnovent.com/repos/birt_example subversion project. I have moved the code into the ScriptUtil class.

There is an underlying question here. You may ask why do I have to go through those gyrations to get a handle to the object? Shouldn't the API just support this method. That's a good question. The issue is that working with the DesignElementHandle object directly is a relatively advanced feature.

The BIRT script objects are meant to be a report developer interface and have been designed to protect the report developer from obscure and hard to debug conditions. When you start working with the ElementHandles there is very little that is protecting the developer from doing something they don't want to do.

For example, in my Eureka example I demonstrated changing the column span for a cell. What I didn't mention is that for the example in question, the change I made was always going to be appropriate. In reality when you change the column span on one cell in the row, you have to account for the total number of cells. This means that you have to:
  • Modify the column span of another cell in the row
  • Drop cells from the row
  • Add cells to the row
These operations are not difficult, but they do require a level of sophistication and thought that many beginning report developers are not ready to take on. So is there a way to expose methods, but somehow warn that these methods are for power users only? Is it better to just not expose the power methods and use a utility class such as the one I have demonstrated?

I have opened a bugzilla item to expose the DesignElementHandle through the API. I would like to hear what other people think of this idea. How do you expose the power interface while still protecting users that don't want or need access to objects that powerful?

Thursday, April 17, 2008

Testing and Debug of Reports

At EclipseCon this year I did a two hours tutorial on BIRT integration. I think it went fairly well. I went a little long, but covered all the best bits. I would like to do a series of posts on this site that go over some of the same material.

The stuff I really want to talk about is a technique that I have developed for debugging / stepping through your Java Event Handler classes. My method requires a fair amount of setup, but in the end it makes it far easier to step through your event handlers. In version 2.3 of the product, this process is simplified. That said many people are on version 2.2 and will continue to be on version 2.2 for a while.

So if you intend to do any Java event handler development in BIRT 2.2. this is the post for you.

Background

The first thing that we need to do is to understand the report test framework that I use on BIRT projects. This is a really simple format that allows me to batch run a large number of pre-defined BIRT reports. I am not going to dive into the details of how all of this works, but you will have all of the source code, and it is relatively simple to figure out.

All of the code for these examples is stored on my subversion server at:
http://longlake.minnovent.com/repos/birt_example

You will find a lot of projects there, they are all available under EPL. The ones that you will need to follow these examples are:
birt_api_example
birt_runtime_lib_222
script.lib

First a couple of words about the projects. If you take a look at these projects you will notice that they are plug-in projects (look under builders). I use the plug-in mechanism to associate the projects together. You do not need to do this, but I have found that it allows people to download them into their workspace and they will just run.

The birt_api_example project contains the majority of the code that you will work with. The REAPI package contains code showing how to run reports. The DEAPI package contains code showing how to create report designs through code. The Reports folder contains report designs that are used by both the REAPI and DEAPI. In addition, Reports contains reports that use the DEAPI.

The birt_runtime_lib_222 project contains the BIRT runtime libraries. It also acts as BIRT_HOME for both the REAPI and DEAPI examples. The runtime lib takes all of the libs and plug-ins from the and exposes them as a plug-in project. I will return to the what exactly the runtime_lib does in a later post. First, I want to get to the fun stuff.

The final project is the script.lib project which contains java event handler code that you want to run or debug. If all goes well, I will tie all of this back to that project at the end of this post.

So lets start with the birt_api_example project. In large BIRT report development projects, you often have multiple reports that need to be tested under a variety of conditions. Typically, we build a relatively small number of rptdesigns that can create a large number of rptdocuments based on the parameters used when the report runs.

This technique is great since it dramatically reduces the amount of code that needs to be maintained. The problem is that one rptdesign may have a large number of test conditions that need to be run. Manually running each condition by entering the required parameters is going to be difficult at best.

What we wanted was a relatively simple solution where we could store report run configurations and then run them in batch. This is the role of the tester package. There is a single java class TestAllReports.java which can be run as a java application.

When you run the TestAllReports project the program will look in the tester_run folder for any .properties files. For each properties file the tester will try to run according to the configuration information in the file. The format of the properties file is very simple.

file_name=customers.rptdesign (relative path from ./Reports directory)
param1_name=param1_Value
param2_name=param1_Value

Currently I have added support for Strings and java.sql.Date parameter data types. It is trivial to modify to support Integers or Booleans if you require these data types.

Once the tester has opened your properties file it will run the report design using Run then Render tasks to build PDF files. You can modify the code in the testReport method to enable output to HTML, Excel, or Word formats (the code is there but commented out). Reports are run into a new folder that will have the format:

tester_output_20080417_133034

Using the tester you can batch test large number of reports. The tester does not have a way to automate the comparison of rendered files, but there are a number of tools that can be used if your processes require this level of automation.

Debugging Java Event Handlers

Now that we have the tester running, it is time to go ahead and look at stepping through event handler code. The 'official' technique for debugging BIRT event handlers is to create a runtime workspace that runs your reports as defined here. This process is relatively straight-forward, but there is one problem, MemoryWoes. I have a two gig laptop and running MaxPermSize I still run into memory issues when running the Runtime workbench.

What I wanted was a method where I could run my reports through the debugger, without having to load the runtime workbench. Well it turns out that I can use my tester program to do just that.

The starting place for this is the script.lib project. This project is where I am going to place all of my event handlers. I then have an ant build file that I can use to compile my event handlers into a jar file. Back in the tester project, when it loads up the EngineConfig, I add that jar file:

    EngineConfig config = new EngineConfig();
config.setProperty(EngineConstants.WEBAPP_CLASSPATH_KEY, getScriptLibFileNames());

...

/*
* The engine needs to see a list of each jar file concatenated as a string
* using the standard file system separator to divide the files
*/
private static String getScriptLibFileNames() {
File scriptHome = new File(SCRIPT_LIB);

if (!scriptHome.exists()) {
scriptHome.mkdir();
}

File[] dirFile = scriptHome.listFiles(new JarFilter());
StringBuffer scriptlibClassPath = new StringBuffer(); //$NON-NLS-1$
for (int i = 0; i &lt; dirFile.length; i++) {
if (scriptlibClassPath.length() > 0) {
scriptlibClassPath.append(File.pathSeparatorChar);
}
scriptlibClassPath.append(dirFile[i].getAbsolutePath());
}

return scriptlibClassPath.toString();

}


Now, I know what you are thinking. Why did you have to add the file as a jar file, why not just add the folder to the classpath. The short answer is that I don't know how to add folder structures to the classpath, and I didn't bother to learn. This is not due to laziness, my thought is that when I deploy to my application, I am going to deploy the jar file. By using the jar when I run the tester, I have a better test environment.

Testing It Out

Now that you have an explanation of what we are doing, lets test it out. If you go back to the birt_api_example project and look in the tester_save folder you will find a dyn_table.properties file. Copy this file to the tester_run folder. Examining the file you will see that I am running the report found in:

Reports/EventHandlerReports/a_dyn_table_columns.rptdesign

Opening this design directly and run it and you will see that a three column table generates a report with eight columns. How does that happen? If you look on the properties for the table, you will see that I am using an event handler named TableMagic, which is in the script.lib project. (I have pictures coming, but blogger is having issues with images right now)

Now put a breakpoint in the TableMagic.java class. (try using from the Java perspective). If you have copied the dyn_table.properties file to the tester_run folder, and you execute the tester in debug mode, it will take you directly to the break point.

How cool is that? You are now debugging your java classes without launching a runtime workspace. The best part is using this type of debugging, I can walk you through the DEAPI calls that I have made to dynamically modify my report based on data from the database.

Now I feel like Howie Mandel on Deal or No Deal. Just when I get to the good stuff, I am going to have to take a break. Next week, I will go through the TableMagic event handler to discuss how you can use the DEAPI to create data driven report designs.


Monday, April 14, 2008

BIRT 2.3 Milestone 6 New and Notable Features.

BIRT 2.3 Milestone 6 has been released and contains improvements to scripting, the addition of the DTP graphical Query Builder prototype, cross tab and chart improvements, and a couple new extension points. Be sure to try out the Query Builder. The DTP team is looking for feedback. Cross tabs can now be modified using script and marker script events are available for charts.

To read more about these features and others available in the 2.3 M6 release, go here.

Friday, April 04, 2008

Eureka....

I wanted to share a little DEAPI related discovery. If I wait to type it up the 'right' way I may not ever get it done, so here it is. I have been doing a lot of work with the DEAPI inside of a report.

If you have not worked with the DEAPI inside of a report, I should remind you that using the DEAPI is only valid within the OnPrepare method of the reportItems or from the BeforeFactory method of the ReportDesign.

When you work with the DEAPI you need to be aware of three basic types of objects in the report engine. Event handlers are passed objects that are part of the script api package. Script objects have limited functionality. For instance, an ICell script object can tell how many cells the column is spanning cell.getColumnsSpan(), but does not allow you to set the column span.

The type of object you want to work with is the DesignElementHandle, which for a cell is CellHandle. DesignElementHandles are defined in the Model API and give you access to all of the functionality in the model. Once you have the DesignElementHandle, you can change the report design in almost any manner.

In between the DesignElementHandles and the script objects are the DesignElements. The DesignElements objects represent the actual instance of the object in the report. DesignElement objects are not exposed by the report engine, and are not particularly useful except for one key role, they provide a link between the script objects and the DesignElementHandles.

If a report were a house: the DesignElementHandles are the blue prints, the DesignElements are the house itself and the script objects are pictures of the house. If you modify the blue print (DesignElementHandle) early in the build process, you can completely change the way the report is built. To stretch the metaphor you are looking at a picture of someone else house (script object) and using that to change the blue prints (DesignElementHandle) for the house that you are building.

Clear as mud? Good...

I have always navigated to the DesignElementHandle by using the name value of the script object.

   DesignElementHandle designHandle = rptContext.getReportRunnable().getDesignHandle();
ModuleHandle moduleHandle = designHandle.getModuleHandle();
TableHandle tableHandle = (TableHandle) moduleHandle.findElement(inTable.getName());
//modify the table handle as I want



Some script objects (cells, rows, columns) do not support the name value, in that case I can use the getParent() method to navigate to a container which supports the name value. Typically I end up back at the table.

From the tableHandle you can then navigate to the appropriate, row, column, group within the report. This works okay, but it can be tricky. In some cases, if you are looking to modify one particular cell it is down right hard. What I wanted to be able to do is get the CellHandle directly from the scriptable object that that is passed to the event handler. You end up having to write a bunch of code to get to navigate back to the object where you started.

It turns out that the ScriptObject has a protected field named designElementImpl which is a handle to the DesignElement object. The DesignElement object has a field "handle" which is the DesignElementHandle. The problem is that neither of the fields are exposed through the API.

Don't you hate it when you know something is there but you just can't figure out how to access it?

I finally got tired of writing all kinds of code to navigate back to an object. It turns out you can use reflection to get access to the fields that are there, but not exposed through the API.

I have created a simple generic method that will get the associated DesignElementHandle from the script object. This is fairly fresh, and has not been fully wrung out for all issues, but it is a decent place to start.

/**
* Modify the columnSpan of a cell item
*/
public void onPrepare(ICell cell, IReportContext reportContext) {

Integer unitSpan = reportContext.getGlobalVariable("cell_span");

try {
// call generic method to get DesignElementHandle
DesignElementHandle cellHdl = getDesignElementFromScript(cell);
if (cellHdl instanceof CellHandle) {
((CellHandle) cellHdl).setColumnSpan(unitSpan);
} catch (Exception e) {
// trap errors
}

/**
* Generic method to access DesignElementHandle from a scriptable object
*
* @param scriptObject
* @return
* @throws NoSuchFieldException
* @throws IllegalAccessException
*/
public DesignElementHandle getDesignElementFromScript(IDesignElement scriptObj)
throws NoSuchFieldException, IllegalAccessException {

// The designElementImpl field is a protected field
// that is declared on the super class
Field fieldFromScript = scriptObj.getClass().getSuperclass()
.getDeclaredField("designElementImpl");
if (fieldFromScript == null){
return null;
}

// Access the DesignElement field from the Script object
fieldFromScript.setAccessible(true);
Object designElement = fieldFromScript.get(scriptObj);

// The DesignElement object has a field named "handle"
// which is the DesignElementHandle for that DesignElement
// Use reflection again to get this field
Field fieldFromSimpleApi = designElement.getClass().getSuperclass()
.getDeclaredField("handle");
if (fieldFromSimpleApi == null){
return null;
}

// Now access the DesignElementHandle from the DesignElement
fieldFromSimpleApi.setAccessible(true);
return (DesignElementHandle)fieldFromSimpleApi.get(designElement);
}