Notice: This Wiki is now read only and edits are no longer possible. Please see: https://gitlab.eclipse.org/eclipsefdn/helpdesk/-/wikis/Wiki-shutdown-plan for the plan.
RAP/BIRT Integration
Introduction
This article illustrates how to integrate BIRT into RAP applications. BIRT is an open source Eclipse-based reporting system that integrates with Java/J2EE applications to produce compelling reports. BIRT integrates with classic RCP applications as well as web applications. Since RAP uses almost the same API as RCP, BIRT can be integrated into single-sourced applications running on both platforms. Topics covered include how to setup the environment to let BIRT and RAP play well together; how to use reports inside RAP applications and some troubleshooting.
Target
The first thing we need to do is to mix up a target which contains both runtimes: RAP and BIRT. We need to download both runtimes first:
When merging the two runtimes together, we should be sure that no bundle is available twice as this results in same strange errors during runtime. So first step is to remove all the duplicate bundles.
Fixing ICU
The com.ibm.icu
bundle is a set of Java libraries that provides more comprehensive support for Unicode, software globalization, and internationalization.
RAP provides the lightweight replacement bundle named com.ibm.icu.base
in version 4, the BIRT runtime needs it as full version in version 3. So be sure that you include version 3 of the com.ibm.icu
in order to get the constraints resolved for both runtimes. To be on the safe side you should remove the com.ibm.icu.base
from the RAP runtime directory.
Additional hint
The BIRT runtime contains a fragment org.eclipse.birt.api
. It exports many packages which should already be available from other bundles.
If your application throws unexpected errors like NoClassDefFoundError
for classes exported by this fragment, try to remove the "org.eclipse.birt.api.jar" from the runtime.
Charts
Under normal circumstances you create your chart object, fill it with some data and draw it on some surface. As RAP does not support any drawing capabilities we need to adjust the last step a little bit.
Create the chart object
Creating a new chart is done exactly as you would do it in the SWT/RCP case. For more information how to use the Chart API of BIRT, please refer to one of the BIRT tutorials (eg. Using the BIRT Chart Engine in Your Plug-in We only included an example here for the sake of completeness.
private Chart createBarChart() { ChartWithAxes chart = ChartWithAxesImpl.create(); chart.setDimension( ChartDimension.TWO_DIMENSIONAL_WITH_DEPTH_LITERAL ); Plot plot = chart.getPlot(); plot.setBackground( ColorDefinitionImpl.WHITE() ); plot.getClientArea().setBackground( ColorDefinitionImpl.WHITE() ); Legend legend = chart.getLegend(); legend.setItemType( LegendItemType.CATEGORIES_LITERAL ); legend.setVisible( true ); Text caption = chart.getTitle().getLabel().getCaption(); caption.setValue( "Distribution of Chart Column Heights" ); Axis xAxis = ( ( ChartWithAxes )chart ).getPrimaryBaseAxes()[ 0 ]; xAxis.getTitle().setVisible( true ); xAxis.getTitle().getCaption().setValue( "" ); Axis yAxis = ( ( ChartWithAxes )chart ).getPrimaryOrthogonalAxis( xAxis ); yAxis.getTitle().setVisible( true ); yAxis.getTitle().getCaption().setValue( "" ); yAxis.getScale().setStep( 1.0 ); TextDataSet categoryValues = TextDataSetImpl.create( new String[]{ "short", "medium", "tall" } ); Series seCategory = SeriesImpl.create(); seCategory.setDataSet( categoryValues ); SeriesDefinition sdX = SeriesDefinitionImpl.create(); sdX.getSeriesPalette().shift( 1 ); xAxis.getSeriesDefinitions().add( sdX ); sdX.getSeries().add( seCategory ); NumberDataSet orthoValuesDataSet1 = NumberDataSetImpl.create( new double[]{ 1, 2, 3 } ); BarSeries bs1 = ( BarSeries )BarSeriesImpl.create(); bs1.setDataSet( orthoValuesDataSet1 ); SeriesDefinition sdY = SeriesDefinitionImpl.create(); yAxis.getSeriesDefinitions().add( sdY ); sdY.getSeries().add( bs1 ); return chart; }
Naturally you can also import your existing chart definitions in order to create the chart.
Use the right renderer
Instead of using the SWT renderer, we just use a simple file renderer (eg. PNG) to draw the chart.
In RAP enviroments we should never use the org.eclipse.birt.chart.device.swt
. Instead we can use the simple image renderes found in org.eclipse.birt.chart.device.extension
.
Here you can see the dependencies of one of the example projects:
The most important items are:
- org.eclipse.birt.chart.engine - The chart engine itself
- org.eclipse.birt.chart.engine.extension - All chart types
- org.eclipse.birt.chart.device.extension - The image file renderers
Display the chart
As mentioned above we need a way to display the chart. Here are the steps we need to do:
- Generate the chart with a image renderer
- Create a SWT image object from the file
- Display the image in the UI
To encapsulate the problem of displaying charts in the UI we create a ChartCanvas
to handle all this stuff.
/******************************************************************************* * Copyright (c) 2009 EclipseSource and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * EclipseSource - initial API and implementation ******************************************************************************/ package org.eclipse.rap.birt.charts; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import org.eclipse.birt.chart.device.IDeviceRenderer; import org.eclipse.birt.chart.factory.GeneratedChartState; import org.eclipse.birt.chart.factory.Generator; import org.eclipse.birt.chart.model.Chart; import org.eclipse.birt.chart.model.attribute.Bounds; import org.eclipse.birt.chart.model.attribute.impl.BoundsImpl; import org.eclipse.birt.chart.util.PluginSettings; import org.eclipse.rap.rwt.RWT; import org.eclipse.rap.rwt.service.ResourceManager; import org.eclipse.swt.SWT; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Canvas; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Label; public class ChartCanvas extends Canvas { private Label chartLabel; private Chart chart; public Chart getChart() { return chart; } public void setChart( Chart chart ) { this.chart = chart; } public ChartCanvas( Composite parent, int style ) { super( parent, style ); setLayout( new FillLayout() ); chartLabel = new Label( this, SWT.NONE ); chartLabel.setToolTipText( "Chart" ); chartLabel.setData( RWT.MARKUP_ENABLED, Boolean.TRUE ); addControlListener( new SmartControlAdapter() { protected void handleControlResized( ControlEvent event ) { try { updateChart(); } catch( Exception e ) { e.printStackTrace(); } } } ); } private void updateChart() { try { drawChart( chart ); } catch( Exception e ) { e.printStackTrace(); } } private void drawChart( Chart chart ) throws Exception { Point size = getSize(); if( !isCached( chart, size ) ) { IDeviceRenderer render = null; PluginSettings ps = PluginSettings.instance(); render = ps.getDevice( "dv.PNG" ); Bounds bounds = BoundsImpl.create( 0, 0, size.x, size.y ); int resolution = render.getDisplayServer().getDpiResolution(); bounds.scale( 72d / resolution ); GeneratedChartState state; Generator gr = Generator.instance(); state = gr.build( render.getDisplayServer(), chart, bounds, null, null, null ); File tmpFile = null; tmpFile = File.createTempFile( "birt" + chart.hashCode(), "_" + size.x + "_" + size.y ); render.setProperty( IDeviceRenderer.FILE_IDENTIFIER, tmpFile ); gr.render( render, state ); String url = getImageURL( tmpFile ); chartLabel.setText( "<img src='"+url+"' width='"+size.x +"' height='"+size.y+"'/>"); //Image img; //InputStream inputStream = null; //try { // inputStream = new FileInputStream( tmpFile ); // img = Graphics.getImage( tmpFile.getName(), inputStream ); // tmpFile.delete(); //} finally { // if( inputStream != null ) { // inputStream.close(); // } //} //if( img != null ) { // chartLabel.setImage( img ); //} else { // chartLabel.setText( "Chart generation failed!" ); //} } } private boolean isCached( Chart chart, Point size ) { // should be implemented by using a useful cache implementation // depending on the use-case it may be enough to just cache images // based on the chart object, if the chart is dynamic you need to // implement another strategy return false; } private String getImageURL(File imageFile) { ResourceManager resourceManager = RWT.getResourceManager(); String resourceKey = imageFile.getPath(); if (!resourceManager.isRegistered(resourceKey)) { try { InputStream inputStream = new FileInputStream(imageFile); try { resourceManager.register(resourceKey, inputStream); } finally { inputStream.close(); } } catch (IOException e) { } } return resourceManager.getLocation(resourceKey); } }
The drawChart
method is the most interesting part of the whole story. It builds the chart to display trough the BIRT chart engine first and uses the dv.PNG
renderer to render the chart into a .png file.
Afterwards we create an instance of Image by utilzing the data stream of the image file.
Finally the image is set as image of a SWT label. This approach to use a label is that the layout managers can determinate the right sizes for layouting the rest of the UI correctly.
Caching the images
As you may have noticed already, RAP caches all images once initialized. Be aware of this fact as calling
Graphics.getImage( tmpFile.getName(), inputStream );
with the same name will not update the image with the contents of the InputStream
.
As rendering charts can be a quite resource-intensive operation you should think about strategies for caching the generated charts. Depending on your use-case it may be suffice to only generate each chart instance once. If you're charts are changing dynamically you should also think about a way to regenerate the chart.
Be aware of the text size determination
The text size determination, short TSD, of RWT is normally nothing you need to care about. In the case of generating images on the fly depending on the size of the surround control we need to be a little carefull.
In order to relayout the whole UI correctly after determinating the real text sizes, RWT currently fires two resize events with a temporary size. As we do no want to generate images for this case we need work around this by using a specialized version of a ControlAdapter
as you can see here:
package org.eclipse.rap.birt.charts; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Shell; /** * This is an extended version of the regular ControlAdapter. * It is used to get only the real resize events and excludes all resize * events of the text size determination. * */ public abstract class SmartControlAdapter extends ControlAdapter { public void controlResized( ControlEvent e ) { Shell shell = ( ( Control )e.widget ).getShell(); Point shellSize = shell.getSize(); Point previousSize = ( Point )e.widget.getData( "previousShellSize" + this.hashCode() ); e.widget.setData( "previousShellSize" + this.hashCode(), shellSize ); if( previousSize != null ) { int dx = Math.abs( Math.abs( shellSize.x - previousSize.x ) - 1000 ); int dy = Math.abs( Math.abs( shellSize.y - previousSize.y ) - 1000 ); if( ( dx <= 2 || dy <= 2 ) ) { // This came from the TextSizeDetermination return; } } handleControlResized( e ); } protected abstract void handleControlResized( ControlEvent e ); }
The full example plugin can be found in the Examples section.
Reports
When using the BIRT Report engine the only thing to consider is to provide access to the generated artifacts to the outside world. We will show two approaches how to generate reports based on an existing report design.
PDF reports
Generating PDF reports is quite easy with RAP. Even the delivery to the client is more than trivial. The only downside of PDF reports are that a client machine needs to have a PDF viewer installed. Furthermore does loading the browser plugin for PDF support and loading the PDF file itself may take a little longer than plain HTML reports. But it is nonetheless a common use-case.
/******************************************************************************* * Copyright (c) 2009 EclipseSource and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * EclipseSource - initial API and implementation ******************************************************************************/ package org.eclipse.rap.birt.reports; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import org.eclipse.birt.report.engine.api.PDFRenderOption; import org.eclipse.rwt.RWT; import org.eclipse.rwt.service.IServiceHandler; public class PDFView extends ReportViewPart { private static final String PDF_HANDLER_ID = PDFServiceHandler.class.getName(); private static class PDFServiceHandler implements IServiceHandler { private String absolutePath; public PDFServiceHandler( String absolutePath ) { super(); this.absolutePath = absolutePath; } public void service() throws IOException, ServletException { InputStream fileInputStream = null; InputStream dataInputStream = null; HttpServletResponse response = RWT.getResponse(); OutputStream outputStream = response.getOutputStream(); try { File file = new File( absolutePath ); int fileSize = ( int )file.length(); fileInputStream = new FileInputStream( file ); response.setContentType( "application/pdf" ); response.setContentLength( fileSize ); response.setHeader( "Content-Disposition", "attachment; filename=\"" + file.getName() + "\"" ); byte[] buffer = new byte[ fileSize ]; dataInputStream = new DataInputStream( new FileInputStream( file ) ); while( dataInputStream.read( buffer ) != -1 ) { outputStream.write( buffer ); } } catch( Exception e ) { e.printStackTrace(); } finally { if( fileInputStream != null ) { fileInputStream.close(); } if( dataInputStream != null ) { dataInputStream.close(); } outputStream.flush(); outputStream.close(); } } } public void doReport() throws Exception { String url = createPDFReport(); getBrowser().setUrl( url ); } private String createPDFReport() throws Exception { File tmpFile = File.createTempFile( "birt", ".pdf" ); String absolutePath = tmpFile.getAbsolutePath(); PDFRenderOption renderOptions = null; renderOptions = new PDFRenderOption(); renderOptions.setOutputFormat( PDFRenderOption.OUTPUT_FORMAT_PDF ); renderOptions.setOutputFileName( absolutePath ); runReport( renderOptions ); RWT.getServiceManager() .registerServiceHandler( PDF_HANDLER_ID, new PDFServiceHandler( absolutePath ) ); StringBuffer url = new StringBuffer(); url.append( "?" ); url.append( IServiceHandler.REQUEST_PARAM ); url.append( "=" ); url.append( PDF_HANDLER_ID ); String encodedURL = RWT.getResponse().encodeURL( url.toString() ); return encodedURL; } }
Resource delivery
When delivering resources you can either let Equinox do the work for you (see org.eclipse.equinox.http.registry.resources extension point and Tim Pietrusky BIRT tutorial) or you can use an IServiceHandler
. As an alternative you can also use the IResourceManager
of RWT itself.
They all have pros and cons.
- org.eclipse.equinox.http.registry.resources
- + Easy to use
- - No access restrictions
- org.eclipse.rwt.service.IServiceHandler
- + Access restrictions (eg. specific files can only be access by this session)
- - Small code overhead to stream the files
- RWT.getResourceManager()
- + Really easy to use
- - Registers the resources for application lifetime
- - No access restrictions
Depending on your use-case you need to decide for one solution.
In the case your client uses Adobe Reader, you can even add additional parameters in the URL to control the behavior of the Adobe Reader (eg. scroll to a specific page).
HTML reports
For HTML reports we need to register all rendered images (static images and report items) in order to be available to the outside world.
This can be done by using the approaches mentioned above. In this case we just reuse the ResourceManager for Images for RWT itself.
After the report is rendered, we grab the raw html source of the report and set it as text of a org.eclipse.swt.browser.Browser
widget.
/******************************************************************************* * Copyright (c) 2009 EclipseSource and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * EclipseSource - initial API and implementation ******************************************************************************/ package org.eclipse.rap.birt.reports; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import org.eclipse.birt.report.engine.api.HTMLRenderOption; import org.eclipse.birt.report.engine.api.HTMLServerImageHandler; import org.eclipse.birt.report.engine.api.IImage; import org.eclipse.birt.report.engine.api.script.IReportContext; import org.eclipse.rwt.graphics.Graphics; public class HTMLView extends ReportViewPart { private String createHTMLReport() { ByteArrayOutputStream htmlOutputStream = new ByteArrayOutputStream(); HTMLRenderOption renderOptions = new HTMLRenderOption(); renderOptions.setOutputFormat( HTMLRenderOption.HTML ); renderOptions.setOutputStream( htmlOutputStream ); renderOptions.setImageDirectory( System.getProperty( "java.io.tmpdir" ) ); renderOptions.setSupportedImageFormats( "PNG" ); HTMLServerImageHandler imageHandler = new HTMLServerImageHandler() { private String registerImage( IImage image, Object context ) { byte[] imageData = image.getImageData(); ByteArrayInputStream imageDatainputStream = new ByteArrayInputStream( imageData ); String fileName = image.getID(); Graphics.getImage( fileName, imageDatainputStream ); try { imageDatainputStream.close(); } catch( IOException e ) { e.printStackTrace(); } return fileName; } public String onDocImage( IImage image, IReportContext context ) { super.onDocImage( image, context ); return "/" + registerImage( image, context ); } public String onDesignImage( IImage image, IReportContext context ) { super.onDocImage( image, context ); return "/" + registerImage( image, context ); } public String onCustomImage( IImage image, Object context ) { return registerImage( image, context ); } }; renderOptions.setImageHandler( imageHandler ); runReport( renderOptions ); return htmlOutputStream.toString(); } public void doReport() { String content = createHTMLReport(); getBrowser().setText( content ); } }
I18n and I10n
In order to deliver your reports in language of the user you need to do two things:
- provide the resource translations for BIRT (see FAQ)
- set the correct locale in your IRunAndRender task
... IRunAndRenderTask task = ...; task = engine.createRunAndRenderTask( design ); task.setLocale( RWT.getLocale() );...
Examples
Currently there are two examples available to show the techniques described above. They both include a launch configuration to get started immediately (you need to have the RAP Tooling installed).