Samstag, 22. Mai 2010

Reporterstellung mit BIRT API

Mit den Business Intelligence and Reporting Tools (BIRT) steht der Java-Welt ein robustes Framework zur Verfügung, das für die Reporterstellung in Business Intelligence (BI) gedacht ist. Das Werkzeug besteht aus mehreren Komponenten, die den Entwicklern unterschiedliche Reportvarianten mit Hilfe offener Schnittstellen anfertigen lässt. In diesem Artikel wird es darum gehen, die Verwendung einiger dieser Schnittstellen in der Form eines Tutorials vorzustellen.


Für jeden der Daten sammelt kann es interessant sein diese im Nachhinein auszuwerten. Egal ob sich dabei um den Umsatzstärksten Kunden, den Umsatzschwächsten Monat oder die Anzahl der gescheiterten JUnit-Test pro Monat handelt, können über Reports Entscheidungen über weiteres Vorgehen getroffen werden.

Mit dem BIRT-Framework existiert eine Sammlung von Tools, mit der auf unterschiedliche Datenquelle zugegriffen werden kann, Datensätze definiert und nach bearbeitet werden können und diese Daten dann nach Belieben, in den unterschiedlichsten Formaten darstellen und exportieren kann.

Um einen Report mit Java zu erstellen, bedient man sich so genannter Engines. Diese BIRT-Engines sind Software-Komponenten, die ihre Funktionsweise und die Schritte für die Erfüllung ihre Aufgabe selbst bestimmen und steuern. Die Design Engine erlaubt es das Layout eines Reports und dessen Elemente festzulegen. Darauf aufbauend kann die Chart-Engine unterschiedliche Diagramme darstellen. Die Report Engine ist für Integration eines Reportings in eine Applikation zuständig. Darüber hinaus hat BIRT noch Schnittstellen vorgesehen, die eine benutzerspezifische Erweiterung des Frameworks ermöglicht.

Design Engine API

Das Hauptziel der Design Engine ist es dafür sorgen, dass die unterschiedlichen Elemente eines Reports – angefangen bei den Datenquellen bis hin zu den Datensätzen – und deren Darstellung und Layout festgelegt werden. Damit wird nicht nur geregelt welche Datenquellen welche Datensätze liefern sollen sondern auch der exakte Ort bezogen auf die darzustellenden Datensätze festgelegt.

Die Design Engine hat dazu eine Schnittstelle, mit der man Report-Design-Objekte erzeugen und manipulieren kann und diese als Report-Object-Model (ROM) [3] in einem als Report-Design-Datei (eine XML-Datei mit der Endung .rptDesign) abspeichern kann oder auf ein bestehendes zuzugreifen und zu ändern. Dieses Objekt beschreibt also den gesamten Inhalt sowohl der sichtbaren als auch der unsichtbaren Bestandteile eines Reports.

Der Eclipse BIRT-Designer, der in der Elipse IDE als Plugin installierbar ist, nimmt unter anderen die Dienste dieser API in Anspruch, sowie die des Eclipse Modelling Frameworks (EMF) und bietet den Entwicklern eine angenehme Möglichkeit an, Reports nach dem WYSIWYG-Prinzip [4] festzulegen. Dabei hilft ... es nicht nur um das Layout für die Darstellung sondern auch um die Festlegung der Datenquellen und -sätze. Hauptziel des Report Designer ist, ein Report Design Objekt zu erzeugen und es als rptDesign-Datei abzuspeichern.

Report Engine API

Das durch die Design-Engine erzeugte Report-Design-Datei wird von der Report-Engine weiter verwendet, um einen Report zu generieren und in dem entsprechenden Format zu rendern. Die Report-Engine ist die wichtigste Komponente, wenn es darum geht, BIRT in eine andere Plattform zu integrieren. Das, weil sie für die Laufzeitumgebung der Reports verantwortlich ist.

Neben der Datei mit der Endung rptDesign, worauf sich die Report Design Engine bezieht, gibt es Reportdokumente (mit der Endung rptDocument) und sind eine binäre Form des Reportinhaltes. Diese werden von der Report Engine nach der Generierungsphase erzeugt und können mit Hilfe der Report Engine API weiter verwendet werden, um u.a. das Rendernformat des Reports programmatisch festzulegen oder Export im gewünschten Format zu exportieren.


Chart Engine API

Aufgabe der Chart Engine ist es, die Generierung von Charts zu ermöglichen, sowie eine Assoziation der Charts zu den entsprechenden Daten zu realisieren. Die Chart Engine API dient dazu, Zugriff auf Chart-Elemente eines Report-Designs zu ermöglichen. Ein großer Vorteil der Chart Engine API ist, dass sie losgelöst von BIRT verwendet werden kann.

Extensions API

BIRT ermöglicht eine Erweiterung oder anders ausgedrückt ein Personalisieren der Reporterstellung.

Es ist zum Beispiel möglich, die Natur der Elemente des Reports zu bestimmen, sowie eine nicht von BIRT vorgesehene Datenquelle und Datensätze, sowie ein Renderformat zu verwenden, das anders ist als PDF oder HTML.

Installation

Um die APIs verwenden zu können, müssen Sie die BIRT-Runtime von der Eclipse-Seite herunterladen [5]. Das heruntergeladene Verzeichnis enthält unter /ReportEngine/lib die nötigen JAR-Dateien, die in dem Buildpath des Projektes eingefügt werden müssen, worin BIRT verwendet werden sollen. Möchte man eine andere Datenquelle als die in BIRT enthaltende Apache Derby verwenden, ist das Kopieren des entsprechenden Datenbanktreibers in das Verzeichnis

ReportEngine/plugins/org...jdbc/drivers

erforderlich.

Einsatz

Als Beispiel soll aus einer bestehenden PostgreSQL-Datenbank eines Unternehmens die Umsatzverhältnissen bezogen auf Kunden dargestellt werden. Wir werden alle notwendige Schritte kennenlernen um einen Report mit Hilfe der BIRT API zu realisieren.Nach dem Downloaden der Runtime und Kopieren des Progresql-Drivers in das entsprechenden Verzeichnis(s. Installation) können wir ein Java-Projekt in Eclipse sowie eine Klasse willkürlicher Name anlegen.


public class Demo

{

DesignConfig config = null;

SessionHandle session = null;

ReportDesignHandle designHandle = null;

ElementFactory elementFactory= null;


Es werden als aller erstes ein paar globale Variablen deklariert, die in unterschiedliche Methoden genutzt werden. Das DesignConfig-Objekt stellt die Konfiguration der Design Engine dar. Ein SessionHandle-Objekt wird dazu gebraucht, um ein designHandle-Objekt mit der gegebenen Konfiguration(config-Objekt) zu erzeugen. Der ReportDesignHandle repräsentiert den den Zustand des ReportDesign-Objekt und hilft dazu es zu manipulieren und schließlich als rptDesign-Datei zu speichern. Für die Erzeugung der verschiedenen Elementen, die in irgendeiner Form in einem Report vorkommen soll ist das ElementFactory zuständig.


public static void main(String [] args) throws SemanticException, IOException{

Demo demo = new Demo();

demo.buildReport();

}


Die Methode buildReport() löst folgende Methoden aus, die nach Aufgabe aufgeteilt wurden:


public void buildReport() throws SemanticException, IOException{

setConfig();

createDataSource();

createDataSets();

createMasterPage();

createBody();

saveReport();

}


Als erstes wollen wir uns die Methode setConfig ansehen.


private void setConfig() {

config = new DesignConfig( );

config.setBIRTHome("/birt-runtime-2_5_2/ReportEngine");

IDesignEngine engine = null;

try{

Platform.startup(config);

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

engine = factory.createDesignEngine(config);

}catch( Exception ex){

ex.printStackTrace();

}

session =engine.newSessionHandle(ULocale.GERMAN);

designHandle = session.createDesign();

elementFactory = designHandle.getElementFactory();

}


Die Klasse Platform aus dem BIRT Core-Framework sorgt dann dafür, dass BIRT sowohl in der Eclipse- als in einer Server-Umgebung läuft. Sie wird mit der gesetzten Konfiguration gestartet und es wird eine Factory für die Erzeugung einer Design-Engine angelegt. Die Session legt unter anderen die ULocale der Design Engine fest und ein neues Design-Objekt wird mit createDesign() erzeugt. Für das spätere Anlegen der Elemente des Design-Objekt wird das elementFactory in Anspruch genommen.

Soweit so gut. Jetzt werden die Elemente des Reports erzeugt. Die zweite Methode die in der buildReport-Methode aufgerufen wurde ist createDatasource. Sie beschreibt die Datenquelle, woraus Daten gelesen werden sollen.


public void createDataSource() throws SemanticException{

OdaDataSourceHandle dsHandle = elementFactory.newOdaDataSource("Kunden DB, "org.eclipse.birt.report.data.oda.jdbc" );

dsHandle.setProperty("odaDriverClass","org.postgresql.Driver");

dsHandle.setProperty( "odaURL", "jdbc:postgresql://localhost:5432/Baeckerei_db" );

dsHandle.setProperty( "odaUser", "mathema" );

dsHandle.setProperty( "odaPassword", "secret");

designHandle .getDataSources( ).add( dsHandle );


}


Bei der Methode createDataSource() geht es darum, BIRT die zu verwendete Datenquelle mitzuteilen. Diese Informationen werden Teil des Design-Objekts und schließlich der rptDesign-Datei, die von der Report-Engine gebraucht werden, um die Data-Engine(eine von dem User verborgene BIRT-Engine, verantwortlich unter anderen für das Herauslesen der Daten aus einer Datenquelle) einzureichen. Diese Informationen sind der Driver-Typ, sowie die postgresql-Klasse, die für den Verbindungsaufbau verantwortlich sind. Ebenfalls müssen Verbindungsparameter wie Benutzer, das Passwort und die URL der Datenbank. Neben SQL-Datenquellen, können u.a. XML-, Webservices, CSV-Dateien angesprochen werden. Danach wird der Datasource-Handle, der all dieser Informationen beinhaltet dem DesignHandle zugewiesen. Als nächstes wird die Methode createDataSets aufgerufen. Sie sieht folgendermaßen aus


public void createDataSets() throws SemanticException{

OdaDataSetHandle dsHandle = elementFactory.newOdaDataSet("dataset","org.eclipse.birt.report.data.oda.jdbc.JdbcSelectDataSet" );

dsHandle.setDataSource("Kunden DB" );

String query = "Select * from kunden_schema.\"kunde\"";

dsHandle.setQueryText( query );

designHandle.getDataSets( ).add( dsHandle );

}


Die Aufgabe dieser Methode ist die Beschreibung der Datensätze zu realisieren. Hier wird eine SQL-Abfrage beschrieben(da in der createDataSource() eine SQL-Datenquelle angekündigt wurde), die das Herauslesen der Daten aus der Datenbank übernimmt. Am Ende der Methode wird die Abfrage dem dataSetHandle zugewiesen, das wiederum dem designHandle eingefügt wird.


Bisher haben wir eine Beschreibung der Datasource und Datasets festgelgt, also die nicht-visuelle Elemente eines Reports. Jetzt widmen wir uns den visuellen Elementen sprich wie die Daten darzustellen sind. Am Anfang muss man eine so genannte Masterpage anlegen. Die MasterPage kann man sich wie ein weißes Blatt Papiervorstellen, worauf die Elemente des vorhin zugewiesenen Elemente des Report-Designs gezeichnet werden. Die Methode createMasterPage()erzeugt dieses Blatt, ohne das es unmöglich ist ein Report zu darzustellen.


public void createMasterPage() throws ContentException, NameException {

DesignElementHandle simpleMasterPage = elementFactory.newSimpleMasterPage("Master Page");

designHandle.getMasterPages( ).add( simpleMasterPage );

}


In der darauf folgenden Methode createBody werden graphische Elemente wie Text, Labels oder Charts in den ReportDesign eingefügt. Darüber hinaus werden in dieser Methode diverse Assoziationen zwischen diesen Elementen ggf. zu entsprechenden Daten bewerkstelligt (z.B. bei einem Balkendiagramm: Zuordnung der Daten an die X- und Y-Achsen). Ein ReportDesign-Objekt enthält ein Body, dem globale visuelle Elemente (z.B. Tabelle) einzufügen ist.

Eine Tabelle im BIRT-Sinne hat (mindestens) eine Header-, eine Detail- und eine Footer-Zeile.

Der Header ist für Titel oder Spaltennamen, die Detail-Zeile für Reportsdaten vorgesehen und Footer für zusätzliche Reportsinformationen gedacht, wobei dieses Konzept umgegangen werden darf. Folglich die Methode createBody() unter die Lupe genommen


public void createBody() throws SemanticException{


TableHandle table = elementFactory.newTableItem( null, 1, 1, 1, 1);

table.setProperty(

IStyleModel.TEXT_ALIGN_PROP, DesignChoiceConstants.TEXT_ALIGN_CENTER);


table.setWidth("100%");

table.setProperty( IReportItemModel.DATA_SET_PROP, "dataset" );

//Header für den Reporttitel

RowHandle header = (RowHandle) table.getHeader( ).get( 0 );

CellHandle tcell =(CellHandle) header.getCells( ).get( 0 );

LabelHandle label1 =

elementFactory.newLabel( null );

label1.setText( "Kunden Report" );

tcell.getContent( ).add( label1 );




Am Anfang wird eine Tabelle angelegt, die eine Spalte, eine Footer-, ein Detail- und eine Header-Zeile enthält. In der Header-Zeile ist ein Label mit dem Reporttitel Kunden Report angelegt worden. Würden wir eine tabellarische Datenrepräsentation haben wollen, wäre es natürlich sinnvoll eine Tabelle mit der Anzahl der darzustellenden Spalten zu erzeugen. In unserem Fall möchten wir ein Balkendiagramm in eine einfache Tabelle einbauen. Dafür wird ein ExtendedItemHandle benötigt. Ein extended-Element stellt ein Element dar, das von Skripten oder benutzerdefinierten Funktionen erzeugt wird.

ExtendedItemHandle eih = elementFactory.newExtendedItem( null, "Chart");


Wie bereits erwähnt können in der unterschiedlichen Tabellenzeile beliebige Elemente eingefügt werden. Falls in den Detail-Zeile z.B. eine spaltenweise Ausgabe der Daten dargestellt werden soll könnte man in der Footer-Zeile die Chart wie folgt einbauen. Die Eigenschaften(u.a. Größe, Format des Elements) des ExtendedItemHandles, der die einzubauende Chart repräsentiert werden ebenfalls festgelegt


//Footer der Tabelle

RowHandle footer = (RowHandle) table.getFooter().get(0);

tcell = (CellHandle) footer.getCells(). get(0);


try{

eih.setHeight( "175pt" );

eih.setWidth( "450pt" );

eih.setProperty (ExtendedItemHandle.DATA_SET_PROP, "");

eih.setProperty("outputFormat", "PNG");


Die folgenden ComputedColumns name und umsatz sind die Bezeichner, die dafür gebraucht werden, die entsprechenden Spalten in der Datenbank anzusprechen. Mit Hilfe der PropertyHandle ist es möglich beim Einfügen dieser Bezeichner einer Spaltenzuordnung in der ExtendedItemHandle zu realisieren


PropertyHandle cs = eih.getColumnBindings();

name = StructureFactory.createComputedColumn();

umsatz = StructureFactory.createComputedColumn(); name.setName("name");

name .setDataType("string");

umsatz .setName("preis");

umsatz .setDataType("float");

name.setExpression("dataSetRow[\"name\"]");

umsatz .setExpression("dataSetRow[\"umsatz\"]");

cs.addItem(name);

cs.addItem(umsatz);

eih.getReportItem().setProperty("chart.instance", createBarChart());

eih.setProperty("outputFormat", "PNG");

}catch(ExtendedElementException e) {...}



Die SetProperty()-Funktion des Reportselements, das an dem ExtendedItemHandle gebunden wird erwartet als zweites Argument, dass man ihr ein Parameterobjekt übergibt. Hier wird die Methode createBarChart(), die ein Chart-Objekt zurück gibt, als Argument übergeben.

Schließlich wird wieder das ExtendedItemHandle der Tabellenzelle eingefügt, die wiederum dem ReportDesign bzw. designHandle zugewiesen wird.


tcell.getContent().add( eih );

designHandle .getBody( ).add( table );

}


Die letzte aufgerufene Methode heißt saveReport() und speichert das ReportDesign-Objekt als rptdesign-Datei auf dem Filesystem. Diese ist in XML Formatiert und wird zukünftig von dem Report-Viewer in dem spezifizierten Format darstellt. Wird die Engines nicht mehr gebraucht muss die Methode ShutDown() aufgerufen werden, die das Abbinden der geladenen Eclipse-Extensions übernimmt.

public void saveReport() throwsIOException{

designHandle .saveAs("/Users/ws/mathema/reports/MonatUmsatz.rptdesign");

designHandle.close( );

Platform.shutdown();

}


Am umfangreichsten ist der Aufbau der Methode createBarChart(), die in createBody() aufgerufen wurde. Wie ihr Name verrät ist sie für die Erzeugung eines Chart-Objekt verantwortlich.

Es wird als erstes ein ChartWithAxes-Objekt erzeugt. Dies weist darauf hin, dass Achsen für die Skalierung verwenden werden. Die allgemeine Dimension des Blocks wird von der Methode setBounds übernommen und setzt das allgemeinde sichtbare Feld des Charts, sprich die Grafik, die Legende und alles was zur Visualisierung gehört. Es kommen einschließlich Einstellungen für eine orthogonale Datenrepräsentation der Werte und als Basis bzw. auf der horizontalen Ebene eine Verbindung mit den orthogonalen Daten. Plot-Einstellungen werden ebenfalls durchgeführt. Ein Plot ist die Ebene, in der die Balken und ihre Skalierung dargestellt werden und ist Teil des Blocks. Man kann die Hintergrundfarbe des Blocks und Plots, sowie die Farbe der Balken individuell anpassen. Die Farbe der Legende wird automatisch von der Balkenfarbe übernommen. Die Größe und die Position der Legende werden einschließlich gesetzt (s. Bild D). Der Code der Methode createBarChart sieht wie folgt aus

public Chart createBarChart()

{

//Erzeugung des Chart Objekts

ChartWithAxes cwaBar = ChartWithAxesImpl.create( );

cwaBar.setType("Bar Chart");


//Dimension des Blocks

cwaBar.getBlock().setBounds( BoundsImpl.create( 0, 0, 450, 175));


//Hintergrundfarbe des Blocks

cwaBar.getBlock().setBackground(ColorDefinitionImpl.WHITE());

cwaBar.getBlock().getOutline().setVisible(true);


//Chartart: 2D, 3D, etc

cwaBar.setDimension(

ChartDimension.TWO_DIMENSIONAL_LITERAL);



// Plot

//Hintergrundfarbe des Plots

cwaBar.getBlock( )

.setBackground( ColorDefinitionImpl.WHITE());

Plot p = cwaBar.getPlot();


//Hintergrundfarbe des Balken

p.getClientArea().setBackground( GradientImpl.create(ColorDefinitionImpl.create( 225, 225, 255 ),

ColorDefinitionImpl.create( 255, 255, 225 ), -90, false ) );

p.getOutline( ).setVisible( true);


// Title

cwaBar.getTitle( ) .getLabel( ) .getCaption( ).setValue( "Bilanz Geschäftsjahr 2010");


Für die Datenrepräsentation auf X- und Y-Acshen werden zwei Series-Arten benötigt. Unter Series ist ein Chart-Element zu verstehen, das mit Daten in Verbindung gebracht werden soll. Ein Objekt der Klasse Series wird erzeugt und ihm wird ein Bezeichner

zugewiesen (z.B. seCategory.setSeriesIdentifier("name")). Dieser Name muss dem Namen einer der in der createBody() definiertenComputedColumns entsprechen. Die X-Series dienen also zur Datenbindung zwischen den X-Achsen und den zu repräsentierten Daten. Um an diese Daten zu kommen wird eine Abfrage errzeugt. Der Bezeichner name wird gebraucht, um die Daten der Spalte name zu lesen. Das gleiche passiert mit der vertikalen Datenrepräsentation, mit dem Unterschied, dass ein BarSeries-Objekt anstatt eines „einfachen“ Series benötigt wird. Darzustellen sind die Jahresumsätze pro Kunde. Danach werden die Achsen mit dem Chart-Series verbunden und das Chart-Objekt, das alle diese Informationen beinhaltet wird am Ende der Methode zurück gegeben.


// Legend: Größe und Position

Legend lg = cwaBar.getLegend();

lg.getText().getFont().setSize(16);

lg.getInsets().set( 10, 5, 0, 0);

lg.setAnchor(Anchor.EAST_LITERAL);


// X-Axis: Type, Position der X Werten

Axis xAxisPrimary =

cwaBar.getPrimaryBaseAxes()[0];

xAxisPrimary.setType(AxisType.TEXT_LITERAL);

xAxisPrimary.getMajorGrid( )

.setTickStyle(TickStyle.BELOW_LITERAL);

xAxisPrimary.getOrigin()

.setType(IntersectionType.VALUE_LITERAL);

xAxisPrimary.setLabelPosition(Position.BELOW_LITERAL);

xAxisPrimary.setTitlePosition(Position.BELOW_LITERAL);


// Y-Axis: Typ, Position der Y-Werten

Axis yAxisPrimary = cwaBar

.getPrimaryOrthogonalAxis( xAxisPrimary);

yAxisPrimary.getMajorGrid( )

.setTickStyle(TickStyle.LEFT_LITERAL);

yAxisPrimary.getTitle().getCaption()

.setValue("Kundenumsatz");

// X-Series: Verbindung mit Bezeichner name, für die

//Zuordnung der X-Werten. Eine Category verbindet

//Bezeichner und Datenabfrage

Series seCategory = SeriesImpl.create();

seCategory.setSeriesIdentifier("name");

Query query =

QueryImpl.create( "row[\"name\"]" );

seCategory.getDataDefinition( ).add(query);

SeriesDefinition sdX =

SeriesDefinitionImpl.create( );

xAxisPrimary.getSeriesDefinitions( ).add( sdX );

sdX.getSeries( ).add( seCategory );


// Y-Series: Verbindung mit Bezeichner umsatz, für die

//Zuordnung der Y-Werten. Analog zu X-Series

BarSeries bs = (BarSeries) BarSeriesImpl.create( );

bs.setSeriesIdentifier( "umsatz" );

Query query1 = QueryImpl.create("row[\"umsatz\"]");

bs.getDataDefinition( ).add( query1 );

bs.setRiserOutline( null );

bs.getLabel().setVisible(true);

bs.setLabelPosition( Position.INSIDE_LITERAL );


//Farbe der Balken mit update setzen

SeriesDefinition sdY1 = SeriesDefinitionImpl.create( );

sdY1.getSeriesPalette( ).update(0);

yAxisPrimary.getSeriesDefinitions( ).add( sdY1 );

sdY1.getSeries( ).add( bs );


//Assoziation der X und Y-Axen

AxisImpl xas = (AxisImpl) cwaBar.getAxes().get(0);

AxisImpl yas =

(AxisImpl) xas.getAssociatedAxes().get(0);


return cwaBar;


}

Die gespeicherte Datei (Siehe saveReport()-Methode) Namens MonatUmsatz.rptdesign kann von einem Report-Viewer (eine BIRT Webkomponente) in HTML-Format angezeigt zu werden. Zum Testzwecken kann sich diese Datei in ein vorher angelegtes Report-Projekt importieren lassen und von dem in dem BIRT-Plugin vorhandenen Report-Previewer (eine in dem Report Designer enthaltende Komponente) in dem Browser anzeigen lassen.




Fazit

Wie wir sehen konnten verfügen die Design, die Chart und Report Engine API's über ein umfangreiches Methoden-Repertoire, dessen einige Funktionalitäten zur Nutze gebracht werden konnten. Es wurde gezeigt, wie man diese API's verwendet, um Reports in einer Java-Anwendung zu erzeugen. Die notwendigen Schritte für die Zeichnung eines Reports und das Einbetten einer Chart für die graphische Darstellung wurde ebenfalls demonstriert. In dem Teil 2 dieser Reihe werden wir sehen, wie sich BIRT in der Application Server Landschaft verhält und wie eine Integration in eine bestehenden Webanwendung lösen lässt.


Links und Referenzen:

[1] http://www.actuate.com/products/all-products/

[2] Integrating and Extending BIRT, J. Weathersby, D. French, Tom Bondur, J. Tatchell, l. Chatalbasheva, Seite 48.

[3] http://www.eclipse.org/birt/ref/ROM_Layout_SPEC.pdf

[4] http://de.wiktionary.org/wiki/WYSIWYG

[5] http://download.eclipse.org/birt/downloads/