Thursday, August 23, 2007

BIRT: Using the Design Engine API and Open Libraries

Recently I gave a presentation on BIRT at the Actuate International Users Conference. One of the things I discussed was embedding the BIRT Design Engine API into an application. This is an often overlooked aspect of BIRT, since most discussions center around report creation using the Eclipse editor and the BIRT Report Engine. I figured it would be cool to do something with the design engine as well. This is useful if your users would like to create their own simple, custom reports and you would like to give them that functionality. There are already products out there that are built on this concept.

The BIRT Design engine is actually a fairly simple API to use. It is part of the org.eclipse.birt.report.model.api package. The steps for creating a report using the API are illustrated below.

Figure 1. Creating a BIRT Report using the Design Engine API

In the above sequence, the user is presented with a list of data sets that are available in a report Library, the user selects a data set to build their own custom report off of, and a new report is created. I just recycled this diagram from my presentation since I am lazy, but the steps are illustrated in the third section, Create New Report.

Its fairly simple, the program instantiates a Report Design Engine object, and creates a new Design Session, the session creates a new report design, data sets are added to the new report design, and a table is built off of the data sets. Then the report design file is saved.

Below is sample code for using the BIRT Report Design engine. The below example will create a simple report in a temporary folder, add a simple report footer, add a grid component, and inside of the grid, add a label that says Hello World. Nothing too fancy with this one.

import java.io.IOException;

import org.eclipse.birt.core.exception.BirtException;
import org.eclipse.birt.core.framework.Platform;
import org.eclipse.birt.report.model.api.CellHandle;
import org.eclipse.birt.report.model.api.DesignConfig;
import org.eclipse.birt.report.model.api.DesignElementHandle;
import org.eclipse.birt.report.model.api.ElementFactory;
import org.eclipse.birt.report.model.api.GridHandle;
import org.eclipse.birt.report.model.api.IDesignEngine;
import org.eclipse.birt.report.model.api.IDesignEngineFactory;
import org.eclipse.birt.report.model.api.LabelHandle;
import org.eclipse.birt.report.model.api.ReportDesignHandle;
import org.eclipse.birt.report.model.api.RowHandle;
import org.eclipse.birt.report.model.api.SessionHandle;
import org.eclipse.birt.report.model.api.SimpleMasterPageHandle;
import org.eclipse.birt.report.model.api.activity.SemanticException;
import org.eclipse.birt.report.model.api.command.ContentException;
import org.eclipse.birt.report.model.api.command.NameException;

import com.ibm.icu.util.ULocale;

public class DesignTest {

/**
* @param args
*/
public static void main(String[] args) {
try {
//create the report design engine configuration pointing to the BIRT runtime
DesignConfig dconfig = new DesignConfig();
dconfig.setBIRTHome("C:/BIRT_RUNTIME_2_2/birt-runtime-2_2_0/ReportEngine");
IDesignEngine engine = null;

//try to start up the eclipse platform to load any plugins and create
//a new design engine
Platform.startup( dconfig );
IDesignEngineFactory factory = (IDesignEngineFactory) Platform.createFactoryObject( IDesignEngineFactory.EXTENSION_DESIGN_ENGINE_FACTORY );
engine = factory.createDesignEngine( dconfig );

//create a new session
SessionHandle session = engine.newSessionHandle( ULocale.ENGLISH ) ;

// create a design or a template. Then create a report element factory
ReportDesignHandle design = session.createDesign();
ElementFactory efactory = design.getElementFactory();

//set my initial properties
design.setDisplayName("my Test Report");
design.setDescription("test");
design.setIconFile("/templates/blank_report.gif");
design.setFileName("c:/TEMP/sample.rptdesign");
design.setDefaultUnits("in");
design.setProperty("comments", "what not and what have you");

SimpleMasterPageHandle element = efactory.newSimpleMasterPage( "Page Master" );
DesignElementHandle footerText = efactory.newTextItem("test");
footerText.setProperty("contentType", "html");
footerText.setStringProperty("content", "MyTest");

//Add in a simple page footer to our master page
element.getPageFooter().add(footerText);

//try to add the footer to the Master Page
try {
design.getMasterPages( ).add( element );
} catch (ContentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NameException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

//create a new grid element, and set the width to 100 percent of the page design
GridHandle grid = efactory.newGridItem( null, 1, 1);
grid.setWidth( "100%" );

//Add the grid to the report body
design.getBody( ).add( grid );

//create a new row
RowHandle row = (RowHandle) grid.getRows( ).get( 0 );

// Create a label and add it to the first cell.
LabelHandle label = efactory.newLabel( "Hello, world!" );
label.setText("Hello, World!");
CellHandle cell = (CellHandle) row.getCells( ).get( 0 );
cell.getContent( ).add( label );

//save the report design
design.saveAs( "c:/TEMP/sample.rptdesign" );
design.close( );
System.out.println("Finished");
} catch (ContentException e) {
e.printStackTrace();
} catch (NameException e) {
e.printStackTrace();
} catch (SemanticException e) {
e.printStackTrace();
} catch (BirtException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

}
}

That is a simple example. So what happens when we want to put some real data in? Well thats when things get fun, due to having to bind the data to the output elements. So while adding elements is easy, data binding is a bit tricky. If you remember from the old days of using the BIRT report designer, you had to bind data sets and tables/lists in order for data to show up. This is done for us automatically nowadays, however if you are writing a program that utilizes the Design Engine API, you will need to do this step for your users. The binding is done by adding a ComputedColumn to the Table/List ColumnBindings property. Then you can add your element to the column. Below is the code that implements the functionality in Figure 1. Not only does it demonstrate how to create a report design in BIRT, it also demonstrates how to open a Report Library, which also has to be done using the BIRT Design Engine API. It will open a report library using code and retrieve the data set that matches the name of the string passed into the method. (Note: for my example, I used a contains comparison instead of the equals comparison. This isn’t necessary, and I only used this since I was working around another issue related and copied and pasted the code I had used). You can only add in a data set once, hence the use of the hasDataSetAlready variable. If you try to add in the same data set multiple times, you will get an error. And since the DataSet method does not allow for the clone method, creating a copy would have taken too much effort to demonstrate this simple concept.

public boolean createReport(String reportName, List dataSetNames) {
try {
DesignConfig dconfig = new DesignConfig();
DataSetHandle dataSetHandleToUse = null;
DataSourceHandle dataSourceHandle = null;
dconfig.setBIRTHome("C:/BIRT_RUNTIME_2_2/birt-runtime-2_2_0/ReportEngine");
IDesignEngine dengine = null;

//try to start up the eclipse platform
IDesignEngineFactory factory = (IDesignEngineFactory) Platform.createFactoryObject( IDesignEngineFactory.EXTENSION_DESIGN_ENGINE_FACTORY );
dengine = factory.createDesignEngine( dconfig );

//create a new session, open the library, and retrieve the first data source since it is uniform in our library
SessionHandle session = dengine.newSessionHandle( ULocale.ENGLISH ) ;
LibraryHandle design = session.openLibrary("C:/eclipse/GWTBirt/BIRTGwt/src/reports/DataSets.rptlibrary");
dataSourceHandle = (DataSourceHandle) design.getDataSources().get(0);

//create a new report
ReportDesignHandle reportDesign = session.createDesign();
reportDesign.getDataSources().add(dataSourceHandle);


//find the correct data set based on dateSetName
int dataSetCount = 0;
for (Iterator dataSetIterator = dataSetNames.iterator(); dataSetIterator.hasNext();)
{
dataSetCount++;
String dataSetName = (String) dataSetIterator.next();

for (Iterator i = design.getDataSets().iterator(); i.hasNext(); )
{
DataSetHandle dataSetHandle = (DataSetHandle) i.next();

if (dataSetHandle.getName().contains(dataSetName))
{

dataSetHandleToUse = dataSetHandle;
dataSetHandleToUse.setName(dataSetHandle.getName());
}
}

//Add the current data set to the report design
boolean hasDataSetAlready = false;
for (Iterator i = reportDesign.getDataSets().iterator(); i.hasNext();)
{
DataSetHandle dataSetInReport = (DataSetHandle) i.next();

if (dataSetInReport.getName().equalsIgnoreCase(dataSetHandleToUse.getName()))
{
hasDataSetAlready = true;
}
}
if (hasDataSetAlready == false)
reportDesign.getDataSets().add(dataSetHandleToUse);

//get the columns from the selected dataset
List columnList = new ArrayList();
for (Iterator i = dataSetHandleToUse.getCachedMetaDataHandle().getResultSet().iterator(); i.hasNext(); )
{
ResultSetColumnHandle colInfo = (ResultSetColumnHandle)i.next();

columnList.add(colInfo.getColumnName());
}

//create new table, set the data set
TableHandle reportTable = reportDesign.getElementFactory().newTableItem("testTable" + dataSetHandleToUse.getName(), columnList.size());
reportTable.setWidth("100%");
reportTable.setDataSet(dataSetHandleToUse);

//create a new detail row and add to the report
RowHandle detailRow = (RowHandle) reportTable.getDetail().get(0);
int x = 0; //used to mark current column position

//go through column list and create a new column binding, otherwise data will not be populated into the report
//Then add a new column to our row
for (Iterator i = columnList.iterator(); i.hasNext();)
{
String columnName = (String) i.next();

ComputedColumn computedColumn = StructureFactory.createComputedColumn();
computedColumn.setName(columnName);
computedColumn.setExpression("dataSetRow[\"" + columnName +"\"]");
PropertyHandle computedSet = reportTable.getColumnBindings( );
reportTable.getColumnBindings().addItem(computedColumn);

//add new data item and cell
DataItemHandle data = reportDesign.getElementFactory().newDataItem(columnName);
data.setResultSetColumn(columnName);
CellHandle cell = (CellHandle)detailRow.getCells().get(x);
cell.getContent().add(data);
x++; //advance position
}

//add the table to my report
reportDesign.getBody().add(reportTable);
}
//set my initial properties for the new report
reportDesign.setDisplayName(reportName);
reportDesign.setDescription(reportName);
reportDesign.setIconFile("/templates/blank_report.gif");
reportDesign.setFileName("C:/eclipse/GWTBirt/BIRTGwt/src/reports/" + reportName + ".rptdesign");
reportDesign.setDefaultUnits("in");
reportDesign.setProperty("comments", reportName);
reportDesign.setProperty(IReportRunnable.TITLE, reportName);

//save report design
reportDesign.saveAs("C:/eclipse/GWTBirt/BIRTGwt/src/reports/" + reportName + ".rptdesign");

return true;
} catch (ContentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (NameException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (DesignFileException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SemanticException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

return false;
}

12 comments:

Pranav Aggarwal said...

Hi,

Its nice to meet a BIRT enthusiast. Well i am trying to be one. I am actually working with BIRT and kinda just starter.

I am able to make BIRT reports through Eclipse IDE, but when I need to deploy the reports I am stuck up. I don't know where BIRT reports will fetch the Database connection string and database related information. I am looking to generalize that Database connection issue, rather hard code any where in report. !!!

Secondly, I need to understand how to deploy them into the web application. Will it be wise to just give a link for the BIRT reports in .jsp pages?

I would appreciate if you could tell me how that could be possible.

Thanks,
Pranav

John Ward said...

Sorry for the delay in response.

There are a number of different ways to obscure the actual report file. I suppose that it may not be a good idea to display the report file in a URL< but I have yet to see anyone exploit it, although it may not be that difficult. I suppose it would be possible to build a report mapping into the Web Viewer, then you would just pass in a report ID rather than the path to the report to run.

As far as the database conneciton string, you could build a event handler for the Data Source that would populate that connection string based off of some sort of logic. There is also the connection pooling, which I havent worked with myself, however Jason Weathersby and Scott Rosenbaum over at BIRTWorld wrote an article about a few months back.

John

Anonymous said...

Hi to all
very good aproach
Hi to all
Very good approach!
I’m learning how to build reports with birt 2.2. I’m working with myeclipse 6.0.1 GA
Is it possible to add a column to a dataset using javascript? I m not talking of
“computed columns”. I want to build using another column using a formular similar to this

amount = 23790;
i = 0;
curr2 = new Array(); //is the new column I want to build
While ( tmp = fetch(mydataset) ){
my_cur = tmp[“cur”]; //cur is a column of the dataset, and my_cur the current element of the column
val = amount / my_cur;
amount -= val * my_cur;
curr2[i++] = val ; //adding an element to the new column
}


Have a nice day
surf_dgm at yahoo dot com

Anonymous said...

Hello All,

I am newbei to BIRT. I do load data from a pojo model and show it in reports. This works for a sample examples found in internet.

In my case, Running RCP application with model built completely by using EMF relfection API and I donno how to invoke those from a javascript and hence I populate to a dummy list and give it to the Birt to generate reports.

I have copied all the jars referred in the classes under
eclipse\plugins\org.eclipse.birt.report.viewer\birt\WEB-INF\lib

There are No errors, but data is not getting populated in viewer.

I Appreciate for any posssible solution.

Regards,
Ramapriya

Eric Njogu said...

I found your article very helpful.

I wanted to add some columns to a pre-existing dataset but could not figure out how to iterate over the static columns from the javadocs.

John Ward said...

Sorry for the delay in response. I've been kind of in a funk in terms of blogging over the past few months. I hate prepping for conferences.

To answer your question, it's right there in the example.

dataSetHandleToUse.getCachedMetaDataHandle().getResultSet().iterator

Hope that helps.

John

Eugene said...

Hi John,

Thanks for helping me out today at eclipsecon 2009! I am having some problems with the the code you have posted here. I am trying to execute this code through a wizard on the init method, just to see if it will work, and the code keeps failing here:

IDesignEngineFactory factory = (IDesignEngineFactory) Platform
.createFactoryObject(IDesignEngineFactory.EXTENSION_DESIGN_ENGINE_FACTORY);

the factory always comes back null. Do you have any quick suggestions? Perhaps you can contact me tonight or tomororw?

Thanks in advance,
-Eugene

John Ward said...

Eugene,

Let me know if you are still having issues.

John

Alp Yogurtcuoglu said...

So cool thank you :)

Cassandra said...

Hi,

I have all the datasets and data that i need in a rpt report but if a data is not binded to a dataset then its value expression cannot be displayed.
So, could u please kindly show/ guide me in binding the data to its dataset so i can display the value expression?

Thank you!

John Ward said...

This is already in the example. The lines with:

for (Iterator i = columnList.iterator(); i.hasNext();)
{
String columnName = (String) i.next();

ComputedColumn computedColumn = StructureFactory.createComputedColumn();
computedColumn.setName(columnName);
computedColumn.setExpression("dataSetRow[\"" + columnName +"\"]");
PropertyHandle computedSet = reportTable.getColumnBindings( );
reportTable.getColumnBindings().addItem(computedColumn);

//add new data item and cell
DataItemHandle data = reportDesign.getElementFactory().newDataItem(columnName);
data.setResultSetColumn(columnName);
CellHandle cell = (CellHandle)detailRow.getCells().get(x);
cell.getContent().add(data);
x++; //advance position
}

That is creating the Table Binding, and creating a Data Report Item that references those Table Bindings.

Cassandra said...

Hi John,

Actually the report is created earlier on so my program only needs to run over the report and display all the info in the report. If i use the one that you shows me, then it's like creating a new Data Report Item, am i right?

Another way of getting value expression of the data is extracting the data column binding of the (parent/main) table. I tried this method but i face great difficulties...

Do you mind showing me how to extract the data column binding of a table?

Thanks.