Saturday, July 12, 2008

BIRT: Reading Chart From Library and Adding to Report Design

I recently came back from a client engagement that was pretty exciting. They are integrating BIRT into their product using a “report wizard” to dynamically build reports. I was pretty stoked by this. There are different ways to accomplish what they wanted internal to the BIRT report scripting and Report Engine environment, and the way they are utilizing it is pretty darn cool.

One of the ideas we came up with was to read chart prototypes from a library and to add them using the Report Engine API to their final report design. There are several advantages to this. First on, of course, is that this gives them the ability to graphically build the visual attributes of their charts using the BIRT Report Designer, and the ability to rapidly change elements and have them propagate to any report consuming them. This will keep chart look and feel consistent throughout their site. But with any approach, pros and cons need to be taken into consideration. In this case, there were a few hurdles to overcome. First being that since their data is build dynamically through their report wizard, data binding to the chart cannot be done in the report library and would need to be done at run-time, as would any chart series formatting. This differs from my previous example in that it actually adds to a report, not just rendering a stand-alone chart.

The following example of how to read a chart from a Library design, and add the data binding to it. There are a few things to notate about this example. First, if you look at my previous Chart Engine API example, you will notice that data is bound differently. When you work with the Chart Engine API in a standalone fashion, you have to manually create what is called a Data Set (not to be confused with a BIRT Report Data Set). A Data Set in the Chart Engine API is basically an array of values bound to an axis, there are no report binding elements involved. When adding to a report, you have to create a dataset binding to the ExtendedReportItem, not the chart itself.

package com.digiassn.blogspot.birt.chartBuilder;

import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;

import org.eclipse.birt.chart.model.ChartWithoutAxes;
import org.eclipse.birt.chart.model.component.Series;
import org.eclipse.birt.chart.model.component.impl.SeriesImpl;
import org.eclipse.birt.chart.model.data.Query;
import org.eclipse.birt.chart.model.data.SeriesDefinition;
import org.eclipse.birt.chart.model.data.impl.QueryImpl;
import org.eclipse.birt.chart.model.data.impl.SeriesDefinitionImpl;
import org.eclipse.birt.chart.model.type.PieSeries;
import org.eclipse.birt.chart.model.type.impl.PieSeriesImpl;
import org.eclipse.birt.core.exception.BirtException;
import org.eclipse.birt.core.framework.Platform;
import org.eclipse.birt.core.framework.PlatformConfig;
import org.eclipse.birt.report.engine.api.EngineConfig;
import org.eclipse.birt.report.engine.api.EngineConstants;
import org.eclipse.birt.report.engine.api.EngineException;
import org.eclipse.birt.report.engine.api.HTMLRenderContext;
import org.eclipse.birt.report.engine.api.HTMLRenderOption;
import org.eclipse.birt.report.engine.api.IRenderTask;
import org.eclipse.birt.report.engine.api.IReportDocument;
import org.eclipse.birt.report.engine.api.IReportEngine;
import org.eclipse.birt.report.engine.api.IReportEngineFactory;
import org.eclipse.birt.report.engine.api.IReportRunnable;
import org.eclipse.birt.report.engine.api.IRunTask;
import org.eclipse.birt.report.model.api.DataSetHandle;
import org.eclipse.birt.report.model.api.DataSourceHandle;
import org.eclipse.birt.report.model.api.DesignConfig;
import org.eclipse.birt.report.model.api.DesignElementHandle;
import org.eclipse.birt.report.model.api.DesignFileException;
import org.eclipse.birt.report.model.api.ElementFactory;
import org.eclipse.birt.report.model.api.ExtendedItemHandle;
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.LibraryHandle;
import org.eclipse.birt.report.model.api.PropertyHandle;
import org.eclipse.birt.report.model.api.ReportDesignHandle;
import org.eclipse.birt.report.model.api.SessionHandle;
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 org.eclipse.birt.report.model.api.elements.structures.ComputedColumn;

import com.ibm.icu.util.ULocale;

public class ChartBuilder7 {
private static String BIRT_HOME = "C:/birt-runtime-2_2_1_1/ReportEngine";

public static void main(String[] args) {
try {
final ChartBuilder7 cb = new ChartBuilder7();

cb.buildReport();
cb.runReport();
cb.renderReport();
//cb.renderReportlet();

cb.shutdown();
} catch (final ContentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (final NameException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (final SemanticException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (final DesignFileException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (final BirtException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (final IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

//factories and engines used in program
IDesignEngine dengine;
IDesignEngineFactory designFactory;
IReportEngine rengine; // stores the single instance of the report engine

SessionHandle session;

public ChartBuilder7() throws BirtException {
final PlatformConfig platformConfig = new PlatformConfig();
platformConfig.setBIRTHome(BIRT_HOME);
Platform.startup(platformConfig);

// create a new report engine factory
final IReportEngineFactory reportFactory = (IReportEngineFactory) Platform
.createFactoryObject(IReportEngineFactory.EXTENSION_REPORT_ENGINE_FACTORY);

// create a new report engine
final EngineConfig engineConfig = new EngineConfig();
engineConfig.setBIRTHome(BIRT_HOME); // will replace with
// configuration file
rengine = reportFactory.createReportEngine(engineConfig);

final DesignConfig dconfig = new DesignConfig();
dconfig.setBIRTHome(BIRT_HOME);

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

// create a new session, open the library, and retrieve the first data
// source since it is uniform in our library
session = dengine.newSessionHandle(ULocale.ENGLISH);

}

/**
* Adds data binding information to chart retrieved from library
*
* @param eih
*/

public void addPieChartFromLibrary(ExtendedItemHandle eih) {
try {
// create chart and set standard options
ChartWithoutAxes cwaPie = (ChartWithoutAxes)eih.getReportItem().getProperty("chart.instance");

//clear any existing definitions from the chart
cwaPie.getSeriesDefinitions().clear();

//create actual series data. This will not be in the library chart
Series seCategory = SeriesImpl.create( );

//This is where things differ from standalone charts. A Query is an EList
//that contains an expression for data. Keep in mind we need to set out
//data set below, otherwise setting these expressions are pointless
Query query = QueryImpl.create( "row[\"OrganizationName\"]");//$NON-NLS-1$
// When creating a series, getDataDefinition() is expecting the EList,
// and the Query is an instance of that
seCategory.getDataDefinition( ).add( query );

SeriesDefinition series = SeriesDefinitionImpl.create( );
series.getSeries( ).add( seCategory );
cwaPie.getSeriesDefinitions( ).add( series );
series.getSeriesPalette().shift(1); // to change the color of the next slice

// Orthogonal Series
PieSeries sePie = (PieSeries) PieSeriesImpl.create( );

Query query2 = QueryImpl.create( "row[\"CandidatesImage\"]");//$NON-NLS-1$
sePie.getDataDefinition().add(query2);
sePie.setExplosion( 5 );

SeriesDefinition sdEmpCount = SeriesDefinitionImpl.create( );
sdEmpCount.getSeriesPalette().shift(2);
series.getSeriesDefinitions( ).add( sdEmpCount );
sdEmpCount.getSeries( ).add( sePie );

//add to report
// eih.loadExtendedElement(); // this may or may not need to
//be called - it's typically used for reading (if already in the rpt design file)
// eih.getReportItem().setProperty("chart.instance", cwaPie);
eih.setProperty("outputFormat", "PNG");
eih.setProperty("dataSet", "GridXMLDataSet"); // tell the chart which dataset to use to bind the chart to the report

//do the bindings to the extended element handle
PropertyHandle computedSet = eih.getColumnBindings( );
ComputedColumn cs1 = new ComputedColumn();
cs1.setExpression( "dataSetRow[\"OrganizationName\"]");//$NON-NLS-1$
cs1.setName( "OrgName" );
cs1.setDataType( "string" );
computedSet.addItem( cs1 );

ComputedColumn cs2 = new ComputedColumn();
cs2.setExpression( "dataSetRow[\"CandidatesImage\"]");//$NON-NLS-1$
cs2.setName( "Candidates" );
cs2.setDataType( "float" );
computedSet.addItem( cs2 );
}
catch ( Exception E ) {
E.printStackTrace();
}
}


/**
* Build report
* @throws ContentException
* @throws NameException
* @throws SemanticException
* @throws DesignFileException
* @throws IOException
*/
public void buildReport() throws ContentException, NameException,
SemanticException, DesignFileException, IOException {
// create a new report
final ReportDesignHandle reportDesign = session.createDesign();

final ElementFactory ef = reportDesign.getElementFactory();

// add new master page element
final DesignElementHandle element = ef
.newSimpleMasterPage("Page Master");
reportDesign.getMasterPages().add(element);

// get the session, open the library
final SessionHandle session = dengine.newSessionHandle(ULocale.ENGLISH);
final LibraryHandle library = session
.openLibrary("C:\\contracts\\CompanyA\\ChartBuild\\chart_library.rptlibrary");

// get the data source and date set from library
final DataSourceHandle dataSourceHandle = (DataSourceHandle) library
.getDataSources().get(0);
final DataSetHandle dataSetHandle = (DataSetHandle) library
.getDataSets().get(0);

// add the data sources and data sets to report
reportDesign.getDataSources().add(dataSourceHandle);
reportDesign.getDataSets().add(dataSetHandle);

// create a new extended item handle and add. This is read in the library
//so we do not need to manually create visual elements. Then add to the body of our
//new report design
final ExtendedItemHandle eih = (ExtendedItemHandle)library.findElement("PieChart");
reportDesign.getBody().add(eih);

//modify the chart with data bindings so that it is bound to our new report design
this.addPieChartFromLibrary(eih);

// add some sample labels
final LabelHandle lh = ef.newLabel("New Label");
lh.setText("Can you see me?");
reportDesign.getBody().add(lh);

// set my initial properties for the new report
final String reportName = "Example Chart";
reportDesign.setDisplayName(reportName);
reportDesign.setDescription(reportName);
reportDesign.setIconFile("/templates/blank_report.gif");
reportDesign.setFileName("C:/Temp/" + reportName + ".rptdesign");
reportDesign.setDefaultUnits("in");
reportDesign.setProperty("comments", reportName);
reportDesign.setProperty(IReportRunnable.TITLE, reportName);

// save report design
reportDesign.saveAs("C:/Temp/" + reportName + ".rptdesign");
}

/**
* Render full report
*/
public void renderReport() {
try {
final IReportDocument rptdoc = rengine
.openReportDocument("C:/temp/tempDoc.rptDocument");

// Create Render Task
final IRenderTask rtask = rengine.createRenderTask(rptdoc);

// Set Render context to handle url and image locataions
final HTMLRenderContext renderContext = new HTMLRenderContext();
// Set the Base URL for all actions
renderContext.setBaseURL("http://localhost/");
// Tell the Engine to prepend all images with this URL - Note this
// requires using the HTMLServerImageHandler
renderContext.setBaseImageURL("http://localhost/myimages");
// Tell the Engine where to write the images to
renderContext.setImageDirectory("C:/xampplite/htdocs/myimages");
// Tell the Engine what image formats are supported. Note you must
// have SVG in the string
// to render charts in SVG.
renderContext.setSupportedImageFormats("JPG;PNG;BMP;SVG");
final HashMap<String, HTMLRenderContext> contextMap = new HashMap<String, HTMLRenderContext>();
contextMap.put(EngineConstants.APPCONTEXT_HTML_RENDER_CONTEXT,
renderContext);
rtask.setAppContext(contextMap);

// Set rendering options - such as file or stream output,
// output format, whether it is embeddable, etc
final HTMLRenderOption options = new HTMLRenderOption();

// Remove HTML and Body tags
// options.setEmbeddable(true);

// Set ouptut location
options.setOutputFileName("C:/temp/outputReport.html");

// Set output format
options.setOutputFormat("html");
rtask.setRenderOption(options);

rtask.render();

// render the report and destroy the engine
// Note - If the program stays resident do not shutdown the Platform
// or the Engine
rtask.close();
} catch (final EngineException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

/**
* render just a reportlet
*/
public void renderReportlet() {
try {
final IReportDocument rptdoc = rengine
.openReportDocument("C:/temp/tempDoc.rptDocument");

// Create Render Task
final IRenderTask rtask = rengine.createRenderTask(rptdoc);

// Set Render context to handle url and image locataions
final HTMLRenderContext renderContext = new HTMLRenderContext();
// Set the Base URL for all actions
renderContext.setBaseURL("http://localhost/");
// Tell the Engine to prepend all images with this URL - Note this
// requires using the HTMLServerImageHandler
renderContext.setBaseImageURL("http://localhost/myimages");
// Tell the Engine where to write the images to
renderContext.setImageDirectory("C:/xampplite/htdocs/myimages");
// Tell the Engine what image formats are supported. Note you must
// have SVG in the string
// to render charts in SVG.
renderContext.setSupportedImageFormats("JPG;PNG;BMP;SVG");
final HashMap<String, HTMLRenderContext> contextMap = new HashMap<String, HTMLRenderContext>();
contextMap.put(EngineConstants.APPCONTEXT_HTML_RENDER_CONTEXT,
renderContext);
rtask.setAppContext(contextMap);

// Set rendering options - such as file or stream output,
// output format, whether it is embeddable, etc
final HTMLRenderOption options = new HTMLRenderOption();

// Remove HTML and Body tags
// options.setEmbeddable(true);

// Set ouptut location
options.setOutputFileName("C:/temp/outputReportlet.html");

// Set output format
options.setOutputFormat("html");
rtask.setRenderOption(options);

rtask.setReportlet("ChartToRender");
rtask.render();

// render the report and destroy the engine
// Note - If the program stays resident do not shutdown the Platform
// or the Engine
rtask.close();
} catch (final EngineException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

}

/**
* Run report
*/
public void runReport() {
try {
final IReportRunnable design = rengine.openReportDesign("C:/Temp/"
+ "Example Chart" + ".rptdesign");
final IRunTask runTask = rengine.createRunTask(design);

// use the default locale
runTask.setLocale(Locale.getDefault());

runTask.run("C:/temp/tempDoc.rptDocument");
runTask.close();
} catch (final EngineException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

public void shutdown() {
Platform.shutdown();
}
}