Beispiel: Ausdrucken mittels JasperReports


Das folgende Beispiel verwendet "JasperReports", um Berichte basierend auf einer Java Bean und einer XML-Datei auszugeben.

Inhalt:

Installation
Anlegen einer Report-Definition
Report goes Programm
Beispiel 1: JavaBean als Datenquelle
Beispiel 2: XML-DataSource


Hier gibt es die Sourcen (gesamtes Eclipse-Projekt): JasperReportsTeste.zip
Nach einem Import in Eclipse muss auf jeden Fall der "Build Path" für die JasperReports-Jars angepaßt werden. Das gleiche muss in "build.xml" in der "path"-Deklaration" geschehen.


Installation

Homepage von JasperReports: http://jasperforge.org/ (falls man von hier aus herunterladen will: das Registrierungsformular hat auch einen Link "Ohne Registrierung weiter").

Download-Seite: http://sourceforge.net/projects/jasperreports/files/jasperreports (wir benötigen "jasperreports-3.7.3-project.zip")

Download des Report-Designers "IReports" 3.7.2: http://jasperforge.org/plugins/project/project_home.php?projectname=ireport (ich würde den "Other platforms"-Download "iReport-3.7.2.zip" empfehlen, besser als ein Installer)
Die Installation besteht darin, beide Komponenten zu entpacken.

Starten der Beispiele:
Für die Beispiele aus dem JasperReports-Download ist "Ant" nötig (http://ant.apache.org/).
Anmerkung: Eclipse enthält bereits Ant, d.h. für das Ausführen von "build.xml" aus dem Projekt heraus ist kein separates Ant nötig, nur für das Compilieren der Beispiele.

Um eines der Beispiele (z.B. das Beispiel für eine XML-Datenquelle aus "demo\samples\xmldatasource") zu starten, sind folgende vier Ant-Aufrufe im jeweiligen Beispiel-Verzeichnis (dort, wo "build.xml" liegt) nötig:

Einbinden in Eclipse-Projekt
Für meine Simpel-Beispiele waren folgende JAR-Dateien aus dem JasperReports-Paket nötig:

Libraries
Wenn man "java.lang.NoClassDefFoundError"-Fehler erhält, dann könnte noch etwas mehr fehlen ;-).


Anlegen einer Report-Definition

Am einfachsten geht das Anlegen einer Report-Definition über "IReport", allerdings haben die beiden im Folgenden gezeigten Varianten beide den Nachteil, dass IReport keine Feldliste aus Bean- oder XML-Datenquelle extrahieren kann.

Schritt 1: Datenquelle
Vorbereitung: wenn man als Datenquelle eine JavaBean verwenden will, muss man "IReport" die class-Dateien dieser Datenquelle bekannt machen. Dies geht über "Extras" => "Optionen", Karteireiter "IReport", Unterkarteireiter "Classpath":
Classpath

Als nächsten Schritt legt man eine neue Datenquelle an. Dies kann man entweder über die "Welcome page" machen, oder über den markierten Toolbar-Button:
DataSource
Jetzt wählt man den Provider: im folgenden "JavaBean als Datenquelle" wäre das der "JRDataSourceProvider", im XML-Beispiel eine "XML file datasource".
Provider
Leider wurde in beiden Fällen keine Feldliste anhand der DataSource erkannt. Aber da wohl je nach Provider gewisse Daten in die Reportdatei generiert werden, sollte man hier trotzdem einen passenden wählen!
Provider

Schritt 2: Report
Man legt einen neuen Report an (Menü "Datei" => "New...") oder die entsprechende Funktion in der "Welcome page":
Neuer Report
Man wählt Namen und Ort der Report-Datei:
Name/Location
Im folgenden Schritt wählt man die eben angelegte Data Source:
DataSource
Im nächsten Schritt erwartet uns leider eine leere Feldliste:
DataSource
Das heißt leider auch, dass wir im Schritt "Group by" nichts wählen können (wäre ein tolles Feature...)
Und damit sind wir mit der Erstellung des Reports fertig.

Nacharbeiten:
"IReports" setzt die Scripting-Language merkwürdigerweise auf "Groovy". Da meine folgenden Beispiele mit Java-Ausdrücken arbeiten, sollte dies umgestellt werden. Dies geht entweder in den Eigenschaften des Reports, oder indem man im "jasperReport"-Element der Reportdatei nacharbeitet:
Language

Felder für den Report
Um eine Feldliste in den Report zu packen, trägt man die Feldliste entweder direkt in der XML-Datei ein, oder wählt im "Report Inspector" den Knoten "Fields" und fügt per Contextmenü die richtigen Felder ein:
Add Field

Die händisch zugefügten Felder aus meinem "beandatasourcereport.jrxml" sehen so aus:
<?xml version="1.0" encoding="UTF-8"?>
<jasperReport ...
	....
	<field name="nachname" class="java.lang.String">
	</field>
	<field name="vorname" class="java.lang.String">
	</field>


Öffnen der Report-Definition in Eclipse:
Die JavaEE-Developer-Edition von Eclipse (Plugin "Web Tools Platform") bietet einen schicken XML-Editor, allerdings wird beim Versuch, eine "jrxml"-Datei zu öffnen (Rechtsklick => "Open with" => "Other" => "XML Editor"), diese Meldung kommen:
JRXML-Fehler
Auf den Link "Content Types Preference Page" klicken dort im Baum "Content Types" unter "Text" => "XML" einen Eintrag auf die Endung "*.jrxml" zufügen:
JRXML Content Type


Report goes Programm

Um eine JRXML-Datei ins Programm zu packen, gibt es mehrere Möglichkeiten.

Compile durch das Programm
Hier liegt nur die JRXML-Datei vor, das Programm compiliert selbst. Das Codefragment ist dieses:
      net.sf.jasperreports.engine.JasperReport js = 
        net.sf.jasperreports.engine.JasperCompileManager.compileReport("beandatasourcereport.jrxml");
Bei meinem ersten Versuch gab es hier eine Fehlermeldung "javac nicht gefunden", diese konnte ich allerdings nicht mehr reproduzieren. Tritt sich erneut auf, dann sollte man darauf achten, dass in den Projekt-Properties ein "JDK 1.6" referenziert wird und keine Runtime.


Vorcompilierung mittels Ant
Im JasperReports-Paket liegt ein Tool, mittels dem aus der "*.jrxml"-Datei eine "*.jasper"-Datei compiliert werden kann. Diese kann das Programm dann ohne weiteren Compilevorgang verwenden.
Ich habe in obigem Beispiel eine "build.xml" gebaut, die diesen Vorgang startet:
<project name="JasperReportsTeste" default="compile" basedir=".">
	<description>Compiliert *.jasper-Dateien aus allen *.jrxml-Dateien.</description>

	<path id="classpath">
		<!--Alle JASPER-Jars einbinden! -->
		<fileset dir="C:/temp/jasperreports/lib">
			<include name="**/*.jar"/>
		</fileset>
		<fileset dir="C:/temp/jasperreports/dist">
			<include name="**/*.jar"/>
		</fileset>
	</path>


	<target name="compile">
		<taskdef name="jrc" classname="net.sf.jasperreports.ant.JRAntCompileTask"> 
			<classpath refid="classpath"/>
		</taskdef>
		<jrc 
			destdir="."
			tempdir="."
			keepjava="false">
			<src>
				<fileset dir=".">
					<include name="**/*.jrxml"/>
				</fileset>
			</src>
			<classpath refid="classpath"/>
		</jrc>
	</target>
</project>
Wichtig ist hier, dass im Classpath alle JARs des Jasper-Pakets eingebunden werden. Das Target "compile" verwendet den Task "jrc", der in einem übergebenen Verzeichnis alle *.jrxml-Dateien durchcompiliert.
Anmerkung: es werden scheinbar Änderungen erkannt, d.h. der Task compiliert nicht immer alle Reports - hier wäre eine Erweiterung meines Ant-Tasks um ein Löschen der "*.jasper"-Dateien angebracht.

Das Laden des Reports ist jetzt einfacher:
      net.sf.jasperreports.engine.JasperPrint jp = 
          net.sf.jasperreports.engine.JasperFillManager.fillReport("beandatasourcereport.jasper", new HashMap(), dataSource);
Der Parameter "dataSource" wird weiter unten erklärt.


Programmatisches Vorcompilieren
Statt eines Ant-Tasks könnte man das Compilieren in die "*.jasper"-Datei auch selbst vornehmen:
      net.sf.jasperreports.engine.JasperCompileManager.compileReportToFile("beandatasourcereport.jrxml","beandatasourcereport.jasper");

Zu empfehlen ist vermutlich die Variante über Ant-Task.


Beispiel 1: JavaBean als Datenquelle

In dem ersten Teil des Beispielprojekts wird eine Liste von JavaBeans als Datenquelle verwendet, d.h. pro Bean erzeugt der Report eine Zeile, und die Properties der Bean (also getter/setter-Paare) werden als Reportfelder angeboten. Quelle des Beispiels:
http://www.jasperassistant.com/docs/guide/ch03s02.html ("Example 3.3")

Die Bean-Klasse sieht so aus:
public class Person implements Serializable
{
  private static final long serialVersionUID = 1L;

  private String sNachname;
  private String sVorname;

  public Person(String nachname, String vorname)
  {
    this.sNachname = nachname;
    this.sVorname = vorname;
  }

  public String getNachname()
  {
    return this.sNachname;
  }

  public void setNachname(String nachname)
  {
    this.sNachname = nachname;
  }

  public String getVorname()
  {
    return this.sVorname;
  }

  public void setVorname(String vorname)
  {
    this.sVorname = vorname;
  }
}
Jetzt muss man sich eine Subklasse von net.sf.jasperreports.engine.data.JRAbstractBeanDataSourceProvider definieren, die in meinem Beispiel nichts weiter macht, als in der überladenen Methode create eine Liste von Person-Beans zu bauen (mitsamt der Namen aus der oben verlinkten Quelle) und diese in eine net.sf.jasperreports.engine.data.JRBeanCollectionDataSource zu packen:

import java.util.*;
import net.sf.jasperreports.engine.*;
import net.sf.jasperreports.engine.data.*;

public class PersonDataSourceProvider extends JRAbstractBeanDataSourceProvider
{
  public PersonDataSourceProvider()
  {
    super(Person.class);
  }

  @Override
  public JRDataSource create(JasperReport report) throws JRException
  {
    List<Person> collection = new ArrayList<Person>();
    collection.add(new Person("Teodor", "Danciu"));
    collection.add(new Person("Peter", "Severin"));
    return new JRBeanCollectionDataSource(collection);
  }

  @Override
  public void dispose(JRDataSource dataSource) throws JRException
  {
  } 

}
Laden des Reports:
Der Report wird aus der vorcompilierten Datei "beandatasourcereport.jasper" geladen:

      PersonDataSourceProvider dataSourceProvider = new PersonDataSourceProvider();
      JRDataSource jrDataSource = dataSourceProvider.create(null);
      
      JasperPrint jp = JasperFillManager.fillReport("beandatasourcereport.jasper",
          new HashMap(), jrDataSource);
      
      JRViewer viewer = new JRViewer(jp);
      
      //GridBagLayout: 100% füllen in alle Richtungen.
      GridBagConstraints gridBagConstraints3 = new GridBagConstraints();
      gridBagConstraints3.fill = java.awt.GridBagConstraints.BOTH;
      gridBagConstraints3.gridy = 0;
      gridBagConstraints3.insets = new java.awt.Insets(5, 5, 5, 5);
      gridBagConstraints3.gridx = 0;
      gridBagConstraints3.weightx = 1.0;
      gridBagConstraints3.weighty = 1.0;
      
      jContentPane.add(viewer, gridBagConstraints3);

JasperFillManager.fillReport erwartet als Parameter den Namen der Jasper-Datei, eine Map mit Parameter (hier: leer), und eine net.sf.jasperreports.engine.JRDataSource. Letztere erzeugen wir aus dem PersonDataSourceProvider. Dessen "create"-Methode erwartet zwar normalerweise einen JasperReport als Parameter, da unsere Implementierung aber nix damit macht, können wir "NULL" reinstecken.

Report-Datei
Die Report-Datei sieht im wesentlichen so aus:
<?xml version="1.0" encoding="UTF-8"?>
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" 
	name="beandatasourcereport" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20">
	
	<field name="nachname" class="java.lang.String">
	</field>
	<field name="vorname" class="java.lang.String">
	</field>
	
	...
	<detail>
		<band height="20" splitType="Stretch">
			<staticText>
				<reportElement x="10" y="0" width="100" height="20"/>
				<textElement/>
				<text><![CDATA[Nachname: ]]></text>
			</staticText>
			
			<textField>
				<reportElement x="100" y="0" width="135" height="20"/>
				<textElement/>
				<textFieldExpression class="java.lang.String"><![CDATA[$F{nachname}]]></textFieldExpression>
			</textField>
			
			<staticText>
				<reportElement x="200" y="0" width="100" height="20"/>
				<textElement/>
				<text><![CDATA[Vorname: ]]></text>
			</staticText>
			
			<textField>
				<reportElement x="300" y="0" width="135" height="20"/>
				<textElement/>
				<textFieldExpression class="java.lang.String"><![CDATA[$F{vorname}]]></textFieldExpression>
			</textField>
		</band>
	</detail>
	...
</jasperReport>
Wichtig ist hier, dass alle im Report zu verwendenden Felder deklariert sind.
Das eigentliche Textfeld referenziert in einer Expression "$F{nachname}" ein Field (deshalb "F", es gäbe auch "P" für Parameter) namens "nachname" aus der aktuellen DataSource.


Beispiel 2: XML-DataSource

In diesem Beispiel wird eine XML-Datei als Datenquelle verwendet, und außerdem wird gezeigt, wie eine Gruppierung über ein Unterelement als Subreport umgesetzt werden kann.

Die XML-Datei stammt aus der Anwendung von Gruppe 1, ich habe allerdings die XML-Struktur modifiziert (alle "answer"-Elemente in ein Subelement "answers" gepackt). Ob dies allerdings wirklich nötig ist, kann ich nicht sagen, ich habe ziemlich lange experimentiert, bis ich den Subreport am Laufen hatte.
Die Struktur ist:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<questioncatalogue CREATOR_ID="42" ID="73537353" NAME="Romane">
  <questionset ID="13371337">
    <question>Wie heißen die Autoren von Märchenmond?</question>
    <answers>
      <answer ID="17841234___37" TRUTH="false">Die Gebrüder Grimm - 13371337</answer>
      <answer ID="12341454___37" TRUTH="true">Wolfgang und Heike Hohlbein - 13371337</answer>
      <answer ID="123445634___37" TRUTH="false">Richard von Weizäcker - 13371337</answer>
      <answer ID="121234___37" TRUTH="false">Terry Pratchett - 13371337</answer>
    </answers>
  </questionset>
   <questionset ID="13371338">
    <question>...</question>
	...

Report anlegen:
In "IReport" wird diesmal eine "XML file datasource" angelegt.
XML file datasource
Es wird die Quelldatei ausgewählt, außerdem wird die "Query" definiert. Bei einer XML-DataSource ist das eine XPath-Expression, die die Liste von Daten definiert, über die die oberste Report-Detailebene laufen soll. In diesem Fall ist das "/questioncatalogue/questionset". Diese XPath-Expressions müssen immer das letztes Element die Ebene im DOM referenzieren, über das gelaufen werden soll, hier also "questionset", weil wir über alle "questionset"-Elemente laufen wollen.
XML file datasource
Leider sehen wir auch in dieser Variante keine Feldliste. Bei Verwendung einer XSD als Datenquelle war ich ebenfalls erfolglos. Mag aber auch daran liegen, dass ich einen Fehler gemacht hatte, die XPath-Ausdrücke scheinen zu keinerlei Fehlermeldungen zu führen.

Es wird ein Report namens "romanereport.jrxml" angelegt.

Jetzt öffnen wir diese "jrxml"-Datei mit einem XML-Editor und passen sie so an (Language außerdem wieder von "Groovy" auf "Java" umstellen):
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
		xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" 
		name="romanereport"...">
	...
	<queryString language="xPath"><![CDATA[/questioncatalogue/questionset]]></queryString>

	<field name="question" class="java.lang.String">
		<fieldDescription><![CDATA[question]]></fieldDescription>
	</field>

	<field name="questionsetid" class="java.lang.String">
		<fieldDescription><![CDATA[@ID]]></fieldDescription>
	</field>
	...
	
Zuerst wird in einem Element "queryString" ein xPath-Ausdruck definiert, der die bereits beim Report-Erstellen angegebene Filterbedingung enthält. Wichtig: Alle folgenden relativen XPath-Ausdrücke beziehen sich auf diese Ebene im XML-Dokument, also auf Elemente "question".

Danach werden zwei Felder deklariert. Die "fieldDescription" enthält diesmal ebenfalls XPath-Ausdrücke. Diese beziehen sich in meinem Beispiel relativ auf den "queryString", man könnte aber wohl auch absolute Pfade angeben. Fraglich nur, ob dann die Zuordnung klappt...
Der erste Ausdruck "question" liefert den String-Inhalt des Elements <question>.
Der zweite Ausdruck "@ID" verweist auf ein Attribut, und zwar ebenfalls relativ zum Element des "queryString", also referenziert er die "ID" des "questionSet".

Auf diese Felder wird im Report dann wie bekannt zugegriffen:

	<textField isStretchWithOverflow="true">
		<reportElement positionType="Float" x="0" y="0" width="200" height="20"/>
		<textElement/>
		<textFieldExpression class="java.lang.String"><![CDATA[$F{answerid}]]></textFieldExpression>
	</textField>


Feld-Editor
Nach dem Anlegen des Reports ist es möglich, einen Feldeditor aufzurufen.
Hierzu Rechtsklick auf den erzeugten Report im "Report Inspector" und "Edit Query":
Edit Query
Es öffnet sich dieses Fenster:
Report Query


Subreport anlegen:
Alle n Antworten sollen innerhalb der Detailsektion "/questioncatalogue/questions" als Subreport mit variablener Höhe ausgegeben werden. Hierzu wird eine neue Report-Datei (im Beispiel: "answerreport.jrxml") angelegt. Man kann wie oben "IReport" und die gleiche DataSource verwenden.

Einbinden des Subreports in "romanereport.jrxml":

	<subreport isUsingCache="false">
		<reportElement x="32" y="25" width="445" height="20" key="subreport-1" />
		
		<subreportParameter name="XML_DATA_DOCUMENT">
			<subreportParameterExpression>$P{XML_DATA_DOCUMENT}</subreportParameterExpression>
		</subreportParameter>


		<subreportParameter name="QuestionSetID">
			<subreportParameterExpression>$F{questionsetid}</subreportParameterExpression>
		</subreportParameter>


		<subreportExpression class="java.lang.String"><![CDATA["answerreport.jasper"]]></subreportExpression>
	</subreport>
Wie üblich sind hier z.B. im "subreport"-Element einige Attribute gesetzt, von denen ich nicht weiß, ob sie nötig sind. Internet-Fundstücke ;-).

Wichtig ist:

Subreport "answers.jrxml":
Der Subreport hat zwei Besonderheiten: Der Rest ist identisch mit den bisherigen Report-Erkenntnissen. Ich habe allerdings in der jrxml-Datei alle Sektionen bis auf die "detail"-Sektion gekillt.


Report laden: Das ist ziemlich einfach:
      Map<String, Object> params = new HashMap<String, Object>();
      Document document = JRXmlUtils.parse(JRLoader.getLocationInputStream("romane.xml"));
      params.put(JRXPathQueryExecuterFactory.PARAMETER_XML_DATA_DOCUMENT, document);
      
      JasperPrint print = JasperFillManager.fillReport("romanereport.jasper", params);
      
      JRViewer viewer = new JRViewer(print);
Hier wird mittels der Klasse net.sf.jasperreports.engine.util.JRXmlUtils ein XML-Dokument geladen, und dieses Dokument als Parameter "XML_DATA_DOCUMENT" in den Report gepackt (steht dort unter diesem Namen in der "Parameters"-Liste zur Verfügung, siehe oben.

Ich hoffe die Generics-Deklaration für die "params", die ich mir zusammengetüftelt hatte (Key = String, Value = Object), stimmt so ;-).

Stand 15.06.2010
Historie:
07.06.2010: Erstellt
15.06.2010: "Präzisierungen" beim XPath, "Report Query"-Editor