Chapter 12. Additional Report Executors

12. Additional Report Executors

In this chapter we go through the necessary steps to add custom additional report exporters, for example to export dynamic lists into a custom text-based format. When talking about adding custom exporting options we need to distinguish between two things. On the one hand we need the server side code to actually implement the exporting logic. This already allows you to use the exporter, for example, when executing the report via URL. If you also want to add the option to the UI we need to take additional steps. In this chapter we will discuss which steps are necessary for both the server side integration and the client side integration.

12.1. Background

Reporting engines are structured similarly and each reporting engine comes with one or more so called OutputGenerators. As usual you can add additional generators via implementing the correct Hook interface. Following is a list of reporting engines and the corresponding Hook interface needed to be implemented in order to add a custom output generator for the particular engine.

Reporting Engine: Dynamic Lists
Hook Interface: {net.datenwerke.rs.base.service.reportengines.table.hooks.TableOutputGeneratorProviderHook}
Output Generator Interface: {net.datenwerke.rs.base.service.reportengines.table.output.generator.TableOutputGenerator}
Reporting Engine: JasperReports
Hook Interface: {net.datenwerke.rs.base.service.reportengines.jasper.hooks.JasperOutputGeneratorProviderHook}
Output Generator Interface: {net.datenwerke.rs.base.service.reportengines.jasper.output.generator.JasperOutputGenerator}
Reporting Engine: Birt
Hook Interface: {net.datenwerke.rs.birt.service.reportengine.hooks.BirtOutputGeneratorProviderHook}
Output Generator Interface: {net.datenwerke.rs.birt.service.reportengine.output.generator.BirtOutputGenerator}
Reporting Engine: Crystal
Hook Interface: {net.datenwerke.rs.crystal.service.crystal.reportengine.hooks.CrystalOutputGeneratorProviderHook}
Output Generator Interface: {net.datenwerke.rs.crystal.service.crystal.reportengine.output.generator.CrystalOutputGenerator}
Reporting Engine: Script Reports
Hook Interface: {net.datenwerke.rs.scriptreport.service.scriptreport.hooks.ScriptReportOutputGeneratorProvider}
Output Generator Interface: {net.datenwerke.rs.scriptreport.service.scriptreport.generator.ScriptReportOutputGenerator}
Reporting Engine: Saiku
Hook Interface: {net.datenwerke.rs.saiku.service.saiku.reportengine.hooks.SaikuOutputGeneratorProviderHook}
Output Generator Interface: {net.datenwerke.rs.saiku.service.saiku.reportengine.output.generator.SaikuOutputGenerator}
Reporting Engine: JXLS
Hook Interface: {net.datenwerke.rs.jxlsreport.service.jxlsreport.reportengine.hooks.JxlsOutputGeneratorProviderHook}
Output Generator Interface: {net.datenwerke.rs.jxlsreport.service.jxlsreport.reportengine.output.generator.JxlsOutputGenerator}
Reporting Engine: GridEditor
Hook Interface: {net.datenwerke.rs.grideditor.service.grideditor.reportengine.hooks.GridEditorOutputGeneratorProviderHook}
Output Generator Interface: {net.datenwerke.rs.grideditor.service.grideditor.reportengine.output.generator.GridEditorOutputGenerator}

All output generators extend the interface net.datenwerke.rs.core.service.reportmanager.output.ReportOutputGenerator which specifies defines the following methods

String[] getFormats();

boolean isCatchAll();

CompiledReport getFormatInfo();

The first method (getFormats) returns an array of string constants that specify the formats this output generator recognizes. The catchAll method allows to a implement fall back mechanism, that is to implement an output generator that is called in case no other output generator recognizes the format specified by the user. The last method getFormatInfo should return an object of type net.datenwerke.rs.core.service.reportmanager.engine.CompiledReport. A compiled report is the result of an output generator, that is, it is for example a PDF document containing the finished report. The CompiledReport object contains, besides the actual report, information about the format such as the file's mime type, extension etc. The getFormatInfo() method should return an empty CompiledReport object, that is, the object does not contain any data but should specify the format. Let us look at the CompiledReport object in more detail. It is an interface specifying the following methods:

Object getReport();
	
String getMimeType();
	
String getFileExtension();

boolean hasData();
	
boolean isStringReport();

A CompiledReport is thus a simple bean containing information. GetReport should return the actual report (e.g., PDF document), getMimeType should return a mime type specifying the mime type of the report. GetFileExtension returns a proper file extension (e.g., pdf in case the report is of the PDF format). HasData should return true if the report contains any data, that is, if the underlying datasource provided data for the report. Finally, isStringReport specifies whether the report is of a string format (such as HTML) or of a binary format (such as PDF).

For convenience we have a default implementation of CompiledReport called CompiledReportImpl (in the same package) which takes all the data as a constructor argument. Additionally, we have CompiledReport objects for many standard file formats. These can be found in the package

net.datenwerke.rs.core.service.reportmanager.engine.basereports

The following objects are available

  • CompiledCsvReport
  • CompiledDocReport
  • CompiledDocxReport
  • CompiledXHtmlReport
  • CompiledJsonReport
  • CompiledPdfReport
  • CompiledTxtReport
  • CompiledXlsReport
  • CompiledXlsxReport
  • CompiledXmlReport

With this in mind, we can now add a simple output generator for a dynamic list (note that the corresponding object in ReportServer is called TableReport). The output generator will create a simple HTML page that displays the number of data rows in the result. For this purpose let us look at the TableOutputGenerator interface.

void initialize(OutputStream os, TableDefinition td, boolean withSubtotals, TableReport report, TableReport originalReport, CellFormatter[] cellFormatters, ParameterSet parameters, User user, ReportExecutionConfig... configs) throws IOException;
	
void nextRow() throws IOException;
	
void addField(Object field, CellFormatter cellFormatter) throws IOException;
	
void close() throws IOException;
	
CompiledReport getTableObject();

void addGroupRow(int[] subtotalIndices, Object[] subtotals, int[] subtotalGroupFieldIndices, Object[] subtotalGroupFieldValues, int rowSize, CellFormatter[] cellFormatters) throws IOException;

The initialize method is called before the first data entry is processed. NextRow informs the generator that the current row is complete and that a new row is about to begin. AddField adds a new data cell and close is called upon completion. One thing to keep in mind is that the initialize method can be called in two modes. Either with an OutputStream or without one. In the first case the actual data is to be streamed directly into the output stream. Otherwise the report is to be generated in memory. Usually data is directly streamed and you do not need to worry about in memory generation. GetTableObject returns a CompiledReport object (without any actual data, in case of streaming) and addGroupRow is used for reports that have subtotals enabled. We will ignore this possibility here.

In order to implement our simple row counter, all we need to do is to keep track of the calls to nextRow().

import net.datenwerke.rs.base.service.reportengines.table.output.generator.TableOutputGenerator
import net.datenwerke.rs.base.service.reportengines.table.hooks.TableOutputGeneratorProviderHook
import net.datenwerke.rs.base.service.reportengines.table.hooks.adapter.TableOutputGeneratorProviderHookAdapter
import net.datenwerke.rs.core.service.reportmanager.engine.basereports.CompiledXHtmlReport

import java.io.OutputStream

import net.datenwerke.rs.base.service.reportengines.table.entities.Column.CellFormatter
import net.datenwerke.rs.base.service.reportengines.table.entities.TableReport
import net.datenwerke.rs.base.service.reportengines.table.output.object.TableDefinition
import net.datenwerke.rs.core.service.reportmanager.engine.CompiledReport
import net.datenwerke.rs.core.service.reportmanager.engine.config.ReportExecutionConfig
import net.datenwerke.rs.core.service.reportmanager.output.ReportOutputGenerator
import net.datenwerke.security.service.usermanager.entities.User
import net.datenwerke.rs.core.service.reportmanager.parameters.ParameterSet

def HOOK_NAME = "MY_ADDITIONAL_GENERATOR" 

/* specify the generator */
class MyGenerator implements TableOutputGenerator {
	def writer = null
	int rows = 0
	
	void initialize(OutputStream os, TableDefinition td, boolean withSubtotals, TableReport report, TableReport originalReport, CellFormatter[] cellFormatters, ParameterSet parameters, User user, ReportExecutionConfig... configs ){
	    writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(os)));
		writer.append("<?xml version=\"1.0\" encoding=\"ISO-8859-1\" ?>" +
				       "<html xmlns=\"http://www.w3.org/1999/xhtml\">" +
				       "<head></head>" +
				       "<body><b>Number of rows:</b>");
	}
	
    void nextRow(){
    	rows++;
    }
    
    void close() {  
    	writer.append(" ").append((rows+1) as String).append("</body></html>");
    	writer.close();
    }
    
    boolean supportsStreaming(){
       return true;
    }
 
    void addField( Object field, CellFormatter cellFormatter ){}
    	
    CompiledXHtmlReport getTableObject() {
    	return new CompiledXHtmlReport(null);
    }

    void addGroupRow (int[] subtotalIndices, Object[] subtotals, int[] subtotalGroupFieldIndices, Object[] subtotalGroupFieldValues, int rowSize, CellFormatter[] cellFormatters ){ }
    
    String[] getFormats() { 
      String[] formats = ['MY_CUSTOM_FORMAT']
      return formats
    }

	boolean isCatchAll() { return false; }

	CompiledXHtmlReport getFormatInfo() {  
    	return new CompiledXHtmlReport(null);		
	}
    
}

/* specify provider */
def provider = [
	provideGenerators : { -> 
		return [new MyGenerator()]
	}
] as TableOutputGeneratorProviderHookAdapter

/* plugin hook */
GLOBALS.services.callbackRegistry.attachHook(HOOK_NAME, TableOutputGeneratorProviderHook.
class, provider)

There are few things to explain. In order to make the new output generator available, we need to install our new generator object in the TableOutputGeneratorProviderHook which returns a list of generator objects. Note that we should return a new instance upon every call to method "provideGenerators". Otherwise one would need to ensure that the output generator is implemented in a thread safe manner.

If we execute our script (assume it is called customDynamicListExporter.groovy) then you should see the following terminal response.

reportserver$ exec generator.groovy
MY_ADDITIONAL_GENERATOR

You can now use your custom exporter when exporting dynamic lists, for example, if you export the dynamic list via URL:

http://.../reportserver/reportexport?key=customer&format=MY_CUSTOM_FORMAT

Note that the output generator will only be available as long as ReportServer is running. If you want this output generator added permanently you should add the script to the list of startup scripts (the startup.d folder, see Section 8.2.).

12.2. Adding the Output Generator to the Client

So far we have added the new output generator, but you can only use it when exporting reports via the URL, that is, normal users would not know that the output generator exists since it doesn't show in the user interface. To change this we will use the ClientExtensionService (see Section 9.3.). Following is the script needed to add the exporter as an option to the client. The ClientExtensionService offers a method "addReportExportOutputFormat which we need to supply with an instance of a report object, a string to be displayed, the actual format, and an optional path to an icon. The object instance in our case is TableReportDtoDec. Remember that each server object has a corresponding client object called ServerObjectDto. The code of most of these objects is generated automatically during the compile process and thus, in order to manually add code most objects come with a corresponding decorator. You will always find the corresponding decorator in the sub-package decorator and by convention it is called ServerObjectDtoDec, thus, in our case we deal with TableReportDtoDec.

import net.datenwerke.rs.base.client.reportengines.table.dto.decorator.TableReportDtoDec

/* obtain ClientExtensionService */
def ces = GLOBALS.services['clientExtensionService']

/* register format */
ces.addReportExportOutputFormat new TableReportDtoDec(), "My Format", "MY_CUSTOM_FORMAT", ""

""

Again, remember that this script needs to be run after a user has logged in and should thus be placed in the onlogin.d directory.

12.3. Skipping file download

Further, you can configure your output generator to skip the file download of the generated file. This may be useful if you want to export information about your report execution without having to return a file to the user. As an example, the following script tracks the number of rows in the report and saves this information to an external SQL Server database by using the Groovy Sql class. A file download would be unnecessary in this case. Install the following script into your onstartup.d directory.

import net.datenwerke.rs.base.service.reportengines.table.output.generator.TableOutputGenerator
import net.datenwerke.rs.base.service.reportengines.table.hooks.TableOutputGeneratorProviderHook
import net.datenwerke.rs.base.service.reportengines.table.hooks.adapter.TableOutputGeneratorProviderHookAdapter
import net.datenwerke.rs.core.service.reportmanager.engine.basereports.CompiledXHtmlReport

import java.io.OutputStream

import net.datenwerke.rs.base.service.reportengines.table.entities.Column.CellFormatter
import net.datenwerke.rs.base.service.reportengines.table.entities.TableReport
import net.datenwerke.rs.base.service.reportengines.table.output.object.TableDefinition
import net.datenwerke.rs.core.service.reportmanager.engine.CompiledReport
import net.datenwerke.rs.core.service.reportmanager.engine.config.ReportExecutionConfig
import net.datenwerke.rs.core.service.reportmanager.output.ReportOutputGenerator
import net.datenwerke.security.service.usermanager.entities.User
import net.datenwerke.rs.core.service.reportmanager.parameters.ParameterSet
import groovy.sql.Sql

def HOOK_NAME = "MY_ADDITIONAL_GENERATOR"

/* specify the generator */
class MyGenerator implements TableOutputGenerator {
  
  	def db = [url:'jdbc:sqlserver://IP;databaseName=myDb', user:'myUser', password:'password', driver:'com.microsoft.sqlserver.jdbc.SQLServerDriver']
 	def sql = Sql.newInstance(db.url, db.user, db.password, db.driver)
  
  	def reportName = null
  
	int rows = 0
  
	void initialize(OutputStream os, TableDefinition td, boolean withSubtotals, TableReport report, TableReport originalReport, CellFormatter[] cellFormatters, ParameterSet parameters, User user, ReportExecutionConfig... configs ){
		reportName = report.name
    }

    void nextRow(){
      	rows++;
    }

    void close() {
        def params = [reportName, rows+1]
        sql.execute 'insert into execution_counts (report_name, number_of_rows) values (?, ?)', params
    }

    boolean supportsStreaming(){
       return false;
    }

    void addField( Object field, CellFormatter cellFormatter ){}

    CompiledXHtmlReport getTableObject() {
    	return new CompiledXHtmlReport("");
    }

    void addGroupRow (int[] subtotalIndices, Object[] subtotals, int[] subtotalGroupFieldIndices, Object[] subtotalGroupFieldValues, int rowSize, CellFormatter[] cellFormatters ){ }

    String[] getFormats() {
      	String[] formats = ['MY_CUSTOM_FORMAT']
      	return formats
    }

	boolean isCatchAll() { return false; }

	CompiledXHtmlReport getFormatInfo() {
    	return new CompiledXHtmlReport("");
	}

}

/* specify provider */
def provider = [
	provideGenerators : { ->
		return [new MyGenerator()]
	}
] as TableOutputGeneratorProviderHookAdapter

/* plugin hook */
GLOBALS.services.callbackRegistry.attachHook(HOOK_NAME, TableOutputGeneratorProviderHook.
class, provider)

In order to install this script into the client interface, you can use a similar script as in the previous example, with the difference that the skipDownload option is set in this case. Install the following script into your onlogin.d directory.

import net.datenwerke.rs.base.client.reportengines.table.dto.decorator.TableReportDtoDec
import net.datenwerke.rs.scripting.service.scripting.extensions.AddReportExportFormatProvider

import net.datenwerke.rs.base.client.reportengines.table.dto.TableReportVariantDto
import net.datenwerke.rs.base.client.reportengines.table.dto.decorator.TableReportVariantDtoDec

/* obtain ClientExtensionService */
def ces = GLOBALS.services['clientExtensionService']

/* register format */
AddReportExportFormatProvider provider = new AddReportExportFormatProvider(new TableReportDtoDec(), "My Format", "MY_CUSTOM_FORMAT", "")
provider.skipDownload = true
ces.addReportExportOutputFormat provider 

""