Eclipse BIRT : Create your own SWT Emitter


As you probably know several emitters are available to render BIRT reports into PDF, HTML, EXCEL, WORD, etc... But why can't we integrate them natively into a RCP Application ? I will show you how to do that by creating a SWT Emitter.

1. Requirements

First of all, you need to download and install the BIRT Report Designer All-In-One 2.3.2.1.
To avoid wasting time, I provide you a Report Design example that can be used (SwtEmitter.zip) Obviously, you can create your own report (just add a table that contains some data).

2. Create a simple RCP Application

Before we create the SWT Emitter, we need to build a RCP Application to "display" the report. Let's be lazy for this part and use the default RCP Client wizard :

  • Create a "Plug-in Project"
  • Answer "Yes" to the question "Would you like to create a rich client application?" in the "Plug-in Content" settings
  • Select the "RCP Application with a View" in the "Template" settings

Once you get your RCP Application generated, you can notice several classes. Try to launch the application by clicking on "Launch an Eclipse Application" in the "Overview" part of the plugin.xml.

If you plan to use the "Classic Models Inc, Sample DataBase" in your BIRT report, you must add the corresponding plug-in in your launch configuration. To do so, open your "Run Configuration", add the plug-in "org.eclipse.birt.report.data.oda.sampledb" and click on "Add Required Plug-in".

3. Render the Design Report

Now that the application is built, we can customize it to render the BIRT report. First, copy the provided report directly into the project. Then clean up the code of createPartControl() and setFocus() methods in the View class because we don't need any TreeViewer. Now create the view content :

  • create a ReportEngine which will create a task to render the report
  • set up the URL according to the report path
  • set the output format of the task in order to render the report in SWT
  • submit the parent composite in which we would like to display the report
  • render the report in the ViewPart with the given composite
  1. public View extends ViewPart {
  2. public static final String ID = "com.zenika.emitter.view";
  3. public static final String OUTPUT_FORMAT_SWT = "swt";
  4. public static final String RENDER_OPT_SWT_VIEWER = "swtViewer";
  5.  
  6. private static final String reportName = "/products.rptdesign";
  7.  
  8. private IReportEngine engine = null;
  9. private EngineConfig config = null;
  10. private IReportRunnable design = null;
  11. private IRunAndRenderTask task;
  12. private IRenderOption options;
  13.  
  14. public void createPartControl(Composite parent) {
  15.  
  16. // We initialize the engine configuration
  17. config = new EngineConfig();
  18. IReportEngineFactory factory = (IReportEngineFactory) Platform
  19. .createFactoryObject(IReportEngineFactory.EXTENSION_REPORT_ENGINE_FACTORY);
  20.  
  21. // We create the report engine
  22. engine = factory.createReportEngine(config);
  23.  
  24. // We get the right URL for our report design
  25. URL reportUrl = FileLocator.find(Activator.getDefault().getBundle(),
  26. new Path(reportName), null);
  27.  
  28. // We open the Report Design from the url
  29. try {
  30. design = engine.openReportDesign(reportUrl.openStream());
  31. } catch (EngineException e) {
  32. e.printStackTrace();
  33. } catch (IOException e) {
  34. // TODO Auto-generated catch block
  35. e.printStackTrace();
  36. }
  37.  
  38. // We create a task to run and render the report design
  39. task = engine.createRunAndRenderTask(design);
  40.  
  41. // We set the options
  42. options = new RenderOption();
  43. /*
  44. * It's very important to set the output format declared in the
  45. * extension org.eclipse.birt.report.engine.emitters in the plugin.xml.
  46. */
  47. options.setOutputFormat(OUTPUT_FORMAT_SWT);
  48.  
  49. /*
  50. * We need to give a composite in order to retrieve it in our swt
  51. * emitter.
  52. */
  53. options.setOption(View.RENDER_OPT_SWT_VIEWER, parent);
  54. task.setRenderOption(options);
  55.  
  56. // We run and render the task
  57. try {
  58. task.run();
  59. } catch (EngineException e) {
  60. e.printStackTrace();
  61. }
  62.  
  63. // We close the task and destroy the engine once we have finished
  64. task.close();
  65. engine.destroy();
  66.  
  67. }
  68.  
  69. public void setFocus() {
  70. }
  71. }

4. Declare a BIRT Emitter

It's very easy to create/declare a new emitter in BIRT, we just need to add an extension in the plugin.xml file.

  • Open your plugin.xml file and in the "Dependencies" tab, add the "org.eclipse.birt.report.engine" plug-in in the "Required Plug-ins" section.
  • Create a class named "SwtEmitter" which extends the class ContentEmitterAdapter : this class will be in charge of rendering the report.
  • In the "Extensions" tab, contribute to the extension point "org.eclipse.birt.report.engine.emitters" and create an "emitter" element.
    • Specify the fully qualified name of your SWT Emitter in the "class" field.
    • In the "format" field, type "swt". The value of this field must match the argument of options.setOutputFormat("swt") in the View class. So when we set the output format of the task in the View class, it will use that new SWT Emitter class to render the report (instead of HTML or PDF, ...).
    • Finally in the "mimeType" field, type "text/html".


5. Creation of the SWT Emitter

Let's now focus on the SWT Emitter class, which will render our report as a SWT component. To understand how an emitter works, we need to think that we are parsing an XML file (do not forget that BIRT Report Design and Document are XML files). By extending the ContentEmitterAdapter class, our class automatically benefits from many methods. These methods are triggered during the report rendering phase (like a SAX Parsing). That's why we get methods such as :

  • startTable(ITableContent table) and endRow(IRowContent row)
  • startRow(IRowContent row) and endTable(ITableContent table)
  • startCell(ICellContent cell) and endCell(ICellContent cell)

If the report contains a table, the rendering phase will trigger a startTable(ITableContent table) and a endRow(IRowContent row) events. So, to render a report containing a table, we will overwrite these methods (those concerning the table, rows, cells and the text element contained in a cell).
These first level features are provided by overwritting the following methods :

  • initialize(IEmitterServices) : retrieve the parent composite given by the View class
  • startTable(ITableContent table) : create a new SWT Table from parent composite
  • startRow(IRowContent row) : create a new TableItem for each parsed row and assess if it is the header row
  • startCell(ICellContent cell) : create a new TableColumn for each cell, only if we are parsing a header row and we got that information from the previous method and we increment a cell count
  • startText(ITextContent) : set the text contained in the cell into the current TableItem or current TableColumn in the right cell according to the cell count.
  1. /*
  2. * The main composite submitted by the view
  3. */
  4. private Composite mainComposite;
  5.  
  6. /*
  7. * The current Table we are processing
  8. */
  9. private Table currentTable;
  10.  
  11. /*
  12. * The current TableItem we are processing
  13. */
  14. private TableItem currentTableItem;
  15.  
  16. /*
  17. * The current TableColumn we are processing
  18. */
  19. private TableColumn currentColumn;
  20.  
  21. /*
  22. * The index of the cell we are processing
  23. */
  24. private int cellIndex = 0;
  25.  
  26. /*
  27. * Boolean to know if we are building the header of the Table
  28. */
  29. private boolean header = false;
  30.  
  31. @Override
  32. public void initialize(IEmitterServices service) {
  33. super.initialize(service);
  34.  
  35. // Important : we retrieve the composite from the View class
  36. mainComposite = (Composite) service.getRenderOption().getOption(
  37. View.RENDER_OPT_SWT_VIEWER);
  38. }
  39.  
  40. @Override
  41. public void startTable(ITableContent table) {
  42. // We create the new Table from the current element
  43. currentTable = new Table(mainComposite, SWT.BORDER);
  44.  
  45. // We want to see lines and headers
  46. currentTable.setLinesVisible(true);
  47. currentTable.setHeaderVisible(true);
  48.  
  49. }
  50.  
  51. @Override
  52. public void endTable(ITableContent table) {
  53. // We resize the table inside the view
  54. resizeTable();
  55. }
  56.  
  57. @Override
  58. public void startCell(ICellContent cell) {
  59. if (header) {
  60. // If we have a header, we create a new TableColumn
  61. currentColumn = new TableColumn(currentTable, SWT.BORDER);
  62. }
  63. }
  64.  
  65. @Override
  66. public void endCell(ICellContent cell) {
  67. // Once we have set the cell content we increment the index
  68. cellIndex++;
  69. }
  70.  
  71. @Override
  72. public void startRow(IRowContent row) {
  73. // Everytime we begin to parse a row, we reset the cellIndex
  74. cellIndex = 0;
  75.  
  76. // We retrieve the Band Type of the row to know what type of row we are
  77. // processing
  78. int bandType = row.getBand().getBandType();
  79.  
  80. // Header of the table
  81. if (bandType == IBandContent.BAND_HEADER) {
  82. // We set the header attribute to true, we will use it to create the
  83. // TableColumn in the startRow method
  84. header = true;
  85. }
  86. // Detail Row of the table or Footer of the table
  87. else if (bandType == IBandContent.BAND_DETAIL
  88. || bandType == IBandContent.BAND_FOOTER) {
  89.  
  90. // We create a new TableItem every time we start to parse a row
  91. currentTableItem = new TableItem(currentTable, SWT.NONE);
  92.  
  93. // We retrieve the background color of the row
  94. String birtColor = row.getStyle().getBackgroundColor();
  95.  
  96. // We get the RGB color from the BIRT Color
  97. RGB rgb = computeColor(birtColor);
  98.  
  99. // We set it onto the background of the TableItem
  100. if (rgb != null) {
  101. currentTableItem.setBackground(new Color(currentTable
  102. .getDisplay(), rgb));
  103. }
  104. }
  105. }
  106.  
  107. @Override
  108. public void endRow(IRowContent row) {
  109. // Once we have finished the parse of the header, we reset the header
  110. // attribute
  111. if (header) {
  112. header = false;
  113. }
  114. }
  115.  
  116. @Override
  117. public void startText(ITextContent text) {
  118. if (header) {
  119. // If we are on header row, we set the text of the current
  120. // TableColumn
  121. currentColumn.setText(text.getText());
  122. } else {
  123. // If we are a detail or footer row, we set the text on the current
  124. // TableItem
  125. currentTableItem.setText(cellIndex, text.getText());
  126. }
  127. }
  128.  
  129. /*
  130. * Sets the size of the table to see the columns.
  131. */
  132. private void resizeTable() {
  133. for (TableColumn column : currentTable.getColumns()) {
  134. column.setWidth(200);
  135. }
  136. currentTable.pack();
  137. }


6. Example of processing a style : color of a row

Let's make our table a little more "appealing" by changing the rows' background color. To do so, we need to add some more code in the startRow(IRowContent row) method. The style of a row can be obtained by calling row.getStyle() ; from that style, we can then retrieve several properties such as the background color, the font or the size of the row for example.
Once we have retrieved the row color, we only have to set the background color of the corresponding TableItem. However, when we retrieve a color property from BIRT, we get it in a String format, so we need to create the correct SWT object corresponding to that property. For example, we can have "rgb(233, 233, 233)" or "Blue" for a color, we have to create a SWT Color representing that color.

  1. /*
  2. * Returns a RGB object from a BIRT color.
  3. *
  4. * There are two type of formats in BIRT :
  5. * - "rgb(233, 233, 233)"
  6. * - "Blue","Red","Yellow",etc ...
  7. *
  8. * We just process the first format in that method.
  9. *
  10. */
  11. private RGB computeColor(String birtColor) {
  12. int[] rgb = new int[3];
  13.  
  14. if (birtColor != null && birtColor.startsWith("rgb(")) {
  15. String numbers = birtColor.substring(4, birtColor.length() - 1);
  16. String[] split = numbers.split(",");
  17. for (int i = 0; i < 3; i++) {
  18. rgb[i] = Integer.parseInt(split[i].trim());
  19. }
  20. return new RGB(rgb[0], rgb[1], rgb[2]);
  21. }
  22. return null;
  23. }

Now run your RCP Application again and enjoy the result : you have created a basic working SWT Emitter !

The original Birt Report Design :


BIRT_RptDesign.PNG

In PDF :


BIRT_PDF_Emitter.PNG

In SWT :


BIRT_SWT_Emitter.PNG

Obvioulsy, it's easy to imagine how powerful that solution can be, especially once embedded into a RAP Application ...

You can find all the sources to realise this SWT Emitter in the archive file SwtEmitter.zip.


Annexes

Commentaires

1. Le mercredi 30 septembre 2009, 17:32 par Mouquet Sylvain

Quand on a un format bien défini, il est plus sympa d'utiliser des regex que de s'amuser à découper des chaînes :

private RGB computeColorRegex(String birtColor) {

       Pattern pattern = Pattern.compile("^rgb\\((\\d+),\\s(\\d+),\\s(\\d+)\\)$");
       Matcher matcher = pattern.matcher(birtColor);
       if (matcher.find()) {
           return new RGB(Integer.valueOf(matcher.group(1)), Integer.valueOf(matcher.group(2)), Integer.valueOf(matcher.group(3)));
       }
       return null;
   }

Fil des commentaires de ce billet

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.