Swing + iReport + TopLink

Caros,

Segue aqui minha colaboração na criação de uma aplicação Swing usando iReport.

Após muitas horas de estudo, correção de bugs, buscas intermináveis no Google e testes, consegui resolver o meu problema de abrir um relatório iReport, em uma aplicação Swing, usando JPA TopLink.

I. Bibliotecas utilizadas

Para essa aplicação, estou usando o JDK1.6 e configurei o uso das seguintes bilbiotecas:

beansbinding-1.2.1.jar
mysql-connector-java-5.1.7-bin.jar
toplink-essentials-agent.jar
toplink-essentials.jar
commons-beanutils-1.7.jar
commons-collections-2.1.jar
commons-digester-1.7.jar
commons-javaflow-20060411.jar
commons-logging-1.0.2.jar
iReport.jar
itext-1.3.1.jar
jasperreports-3.0.0.jar
jcommon-1.0.0.jar
slf4j-simple-1.5.6.jar
slf4j-log4j12-1.5.6.jar
slf4j-api-1.5.6.jar

II. Classe Reports

Essa classe tem por objetivo chamar o relatório a partir da sua aplicação, compilando o relatório (aqui neste exemplo, chama-se Clientes.jxml) e apresentando-o no JasperViewer.

Para que isso funcione corretamente, crie o seu relatório usando o iReports (usei a versão 3.0.0 - explicarei mais adiante), e salve-o na mesma pasta do seu projeto, para ser armazenado dentro do arquivo JAR, quando o projeto for compilado.

A unidade de persistência neste exemplo chama-se MechOfficePU, mas basta substituir pela usada na sua aplicação.

O arquivo jxml também pode apresentar alguns problemas com as tags xml, e para isso estou usando uma classe chamada LegacyJasperInputStream, que corrige esse problema. Apresentarei o código mais adiante.

Segue abaixo o código da classe Reports:

public class Reports {

    private Cadastro cadastro;
    private EntityManager entityManager;
    private Query cadastroQuery;
    private List cadastroList;

    public Reports() {

   entityManager = java.beans.Beans.isDesignTime() ? null : javax.persistence.Persistence.createEntityManagerFactory("MechOfficePU").createEntityManager();

    }

  public void Clientes(){

        try{        

        InputStream rclientes = Reports.class.getResourceAsStream("Clientes.jrxml");
        cadastroQuery = java.beans.Beans.isDesignTime() ? null : entityManager.createQuery("SELECT c FROM Cadastro c WHERE c.idpermissao = :idpermissao");
        Permissao idpermissao = new Permissao(5);
        cadastroQuery.setParameter("idpermissao", idpermissao);
        cadastroList = java.beans.Beans.isDesignTime() ? java.util.Collections.emptyList() : cadastroQuery.getResultList();
        Map parameters = getParameters(entityManager);
        JasperDesign design = JRXmlLoader.load(new LegacyJasperInputStream(rclientes));
        JRBeanCollectionDataSource ds = new JRBeanCollectionDataSource(cadastroList);
        JasperReport compilado = JasperCompileManager.compileReport(design);
        JasperPrint jp = JasperFillManager.fillReport(compilado, new HashMap(), ds);
        JasperViewer.viewReport(jp, false);
       } catch (Exception e) {
       System.out.print(e.getMessage());
      }

    }
}

III. Classe LegacyJasperInputStream

Essa classe tem por objetivo incluir algumas tags xml, quando o relatório for gerado no iReports, corrigindo alguns problemas de compatibilidade, se houver.

Ela pode ser desnecessária, mas no meu projeto foi a garantia para que o relatório seja compilado com sucesso.

Segue abaixo o código desta classe:

package <o_nome_do_seu_projeto>;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * This is a decorator around an InputStream, which will convert a modern XSD based Jasper design into the legacy DTD
 * version. This is desirable since modern tools i.e. the iReport plugin for NetBeans 6.5 generates designs based on the
 * modern XSD schema, whereas the Jasper engine itself that we use apparently can not handle XSD but instead require DTD's.
 * 
 * All you have to do to use this converting decorator, is to pipe your Jasper design InputStream through this one, and
 * it will automatically take care of adding a DTD docType and remove attributes which will typically cause a legacy
 * Jasper version to choke.
 *
 * @author Casper Bang
 */
public class LegacyJasperInputStream extends FilterInputStream
{
    /**
     * @param is        The InputStream with the modern XSD based Jasper design
     */
    public LegacyJasperInputStream(final InputStream is) {
        super( convertToLegacyFormat(is) );
    }

    private static InputStream convertToLegacyFormat(final InputStream is){
        Document document = convertInputStreamToDOM( is );
        document.getDocumentElement().removeAttribute("xmlns");
        document.getDocumentElement().removeAttribute("xmlns:xsi");
        document.getDocumentElement().removeAttribute("xsi:schemaLocation");
        return convertStringToInputStream( addDocTypeAndConvertDOMToString(document) );
    }


    private static Document convertInputStreamToDOM(final InputStream is){
        Document document = null;
        BufferedInputStream bis = new BufferedInputStream(is);
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = null;

        try {
            builder = factory.newDocumentBuilder();
        }
        catch (ParserConfigurationException ex) {
            LoggerFactory.getLogger(LegacyJasperInputStream.class).error(ex.getMessage(), ex);
        }

        try {
            document = builder.parse(bis);
        } catch (SAXException ex) {
            LoggerFactory.getLogger(LegacyJasperInputStream.class).error(ex.getMessage(), ex);
        } catch (IOException ex) {
            LoggerFactory.getLogger(LegacyJasperInputStream.class).error(ex.getMessage(), ex);
        }

        return document;
    }

    private static String addDocTypeAndConvertDOMToString(final Document document){

        TransformerFactory transfac = TransformerFactory.newInstance();
        Transformer trans = null;
        try {
            trans = transfac.newTransformer();
        } catch (TransformerConfigurationException ex) {
            LoggerFactory.getLogger(LegacyJasperInputStream.class).error(ex.getMessage(), ex);
        }

        trans.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
        trans.setOutputProperty(OutputKeys.DOCTYPE_PUBLIC, "//JasperReports//DTD Report Design//EN");
        trans.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, "http://jasperreports.sourceforge.net/dtds/jasperreport.dtd");

        StringWriter sw = new StringWriter();
        StreamResult result = new StreamResult(sw);
        DOMSource source = new DOMSource(document);
        try {
            trans.transform(source, result);
        } catch (TransformerException ex) {
            LoggerFactory.getLogger(LegacyJasperInputStream.class).error(ex.getMessage(), ex);
        }

        return sw.toString();
    }

    private static InputStream convertStringToInputStream(final String template){
        InputStream is = null;
        try {
            is = new ByteArrayInputStream(template.getBytes("UTF-8"));
        } catch (UnsupportedEncodingException ex) {
           LoggerFactory.getLogger(LegacyJasperInputStream.class).debug(ex.getMessage(), ex);
        }
        return is;
    }
}

IV. Relatório Clientes.jxml

Segue o código XML do meu relatório. Ele apenas recebe as informações de nome, endereco, complemento, ddd e telefone que vem da tabela de clientes do banco de dados. Esse código pode ser alterado facilmente, se você criar um arquivo .jxml e abri-lo com o iReport. Depois é só salvar na pasta do seu projeto, sem precisar compilar (formato .jasper). A classe Reports faz o resto do trabalho para você.

Segue abaixo o código do relatório:

<?xml version="1.0" encoding="UTF-8"  ?>
<!-- Created with iReport - A designer for JasperReports -->
<!DOCTYPE jasperReport PUBLIC "//JasperReports//DTD Report Design//EN" "http://jasperreports.sourceforge.net/dtds/jasperreport.dtd">
<jasperReport
		 name="Clientes"
		 columnCount="1"
		 printOrder="Vertical"
		 orientation="Portrait"
		 pageWidth="595"
		 pageHeight="842"
		 columnWidth="535"
		 columnSpacing="0"
		 leftMargin="30"
		 rightMargin="30"
		 topMargin="20"
		 bottomMargin="20"
		 whenNoDataType="NoPages"
		 isTitleNewPage="false"
		 isSummaryNewPage="false">
	<property name="ireport.scriptlethandling" value="0" />
	<property name="ireport.encoding" value="UTF-8" />
	<import value="java.util.*" />
	<import value="net.sf.jasperreports.engine.*" />
	<import value="net.sf.jasperreports.engine.data.*" />


	<field name="nome" class="java.lang.String"/>
	<field name="endereco" class="java.lang.String"/>
	<field name="complemento" class="java.lang.String"/>
	<field name="dddtel1" class="java.lang.String"/>
	<field name="telefone1" class="java.lang.String"/>

		<background>
			<band height="0"  isSplitAllowed="true" >
			</band>
		</background>
		<title>
			<band height="49"  isSplitAllowed="true" >
				<staticText>
					<reportElement
						x="0"
						y="0"
						width="535"
						height="49"
						key="staticText"/>
					<box></box>
					<textElement textAlignment="Center" verticalAlignment="Middle">
						<font size="26"/>
					&lt;/textElement&gt;
				&lt;text&gt;&lt;![CDATA[Clientes]]&gt;&lt;/text&gt;
				&lt;/staticText&gt;
			&lt;/band&gt;
		&lt;/title&gt;
		&lt;pageHeader&gt;
			&lt;band height="16"  isSplitAllowed="true" &gt;
			&lt;/band&gt;
		&lt;/pageHeader&gt;
		&lt;columnHeader&gt;
			&lt;band height="20"  isSplitAllowed="true" &gt;
				&lt;staticText&gt;
					&lt;reportElement
						x="6"
						y="-1"
						width="100"
						height="20"
						key="staticText"/&gt;
					&lt;box&gt;&lt;/box&gt;
					&lt;textElement verticalAlignment="Middle"&gt;
						<font size="12" />
					&lt;/textElement&gt;
				&lt;text&gt;&lt;![CDATA[Nome]]&gt;&lt;/text&gt;
				&lt;/staticText&gt;
				&lt;staticText&gt;
					&lt;reportElement
						x="198"
						y="0"
						width="100"
						height="20"
						key="staticText"/&gt;
					&lt;box&gt;&lt;/box&gt;
					&lt;textElement verticalAlignment="Middle"&gt;
						<font size="12" />
					&lt;/textElement&gt;
				&lt;text&gt;&lt;![CDATA[Endereco]]&gt;&lt;/text&gt;
				&lt;/staticText&gt;
				&lt;staticText&gt;
					&lt;reportElement
						x="400"
						y="0"
						width="111"
						height="20"
						key="staticText"/&gt;
					&lt;box&gt;&lt;/box&gt;
					&lt;textElement verticalAlignment="Middle"&gt;
						<font size="12" />
					&lt;/textElement&gt;
				&lt;text&gt;&lt;![CDATA[Telefone]]&gt;&lt;/text&gt;
				&lt;/staticText&gt;
			&lt;/band&gt;
		&lt;/columnHeader&gt;
		&lt;detail&gt;
			&lt;band height="19"  isSplitAllowed="true" &gt;
				&lt;textField isStretchWithOverflow="false" isBlankWhenNull="false" evaluationTime="Now" hyperlinkType="None"  hyperlinkTarget="Self" &gt;
					&lt;reportElement
						x="6"
						y="1"
						width="173"
						height="18"
						key="textField"/&gt;
					&lt;box&gt;&lt;/box&gt;
					&lt;textElement verticalAlignment="Middle"&gt;
						<font/>
					&lt;/textElement&gt;
				&lt;textFieldExpression   class="java.lang.String"&gt;&lt;![CDATA[$F{nome}]]&gt;&lt;/textFieldExpression&gt;
				&lt;/textField&gt;
				&lt;textField isStretchWithOverflow="false" isBlankWhenNull="false" evaluationTime="Now" hyperlinkType="None"  hyperlinkTarget="Self" &gt;
					&lt;reportElement
						x="198"
						y="1"
						width="148"
						height="18"
						key="textField"/&gt;
					&lt;box&gt;&lt;/box&gt;
					&lt;textElement verticalAlignment="Middle"&gt;
						<font/>
					&lt;/textElement&gt;
				&lt;textFieldExpression   class="java.lang.String"&gt;&lt;![CDATA[$F{endereco}]]&gt;&lt;/textFieldExpression&gt;
				&lt;/textField&gt;
				&lt;textField isStretchWithOverflow="false" isBlankWhenNull="false" evaluationTime="Now" hyperlinkType="None"  hyperlinkTarget="Self" &gt;
					&lt;reportElement
						x="346"
						y="1"
						width="36"
						height="18"
						key="textField"/&gt;
					&lt;box&gt;&lt;/box&gt;
					&lt;textElement textAlignment="Left" verticalAlignment="Middle"&gt;
						<font/>
					&lt;/textElement&gt;
				&lt;textFieldExpression   class="java.lang.String"&gt;&lt;![CDATA[$F{complemento}]]&gt;&lt;/textFieldExpression&gt;
				&lt;/textField&gt;
				&lt;textField isStretchWithOverflow="false" isBlankWhenNull="false" evaluationTime="Now" hyperlinkType="None"  hyperlinkTarget="Self" &gt;
					&lt;reportElement
						x="420"
						y="1"
						width="100"
						height="18"
						key="textField"/&gt;
					&lt;box&gt;&lt;/box&gt;
					&lt;textElement textAlignment="Left" verticalAlignment="Middle"&gt;
						<font/>
					&lt;/textElement&gt;
				&lt;textFieldExpression   class="java.lang.String"&gt;&lt;![CDATA[$F{telefone1}]]&gt;&lt;/textFieldExpression&gt;
				&lt;/textField&gt;
				&lt;textField isStretchWithOverflow="false" isBlankWhenNull="false" evaluationTime="Now" hyperlinkType="None"  hyperlinkTarget="Self" &gt;
					&lt;reportElement
						x="400"
						y="1"
						width="19"
						height="18"
						key="textField"/&gt;
					&lt;box&gt;&lt;/box&gt;
					&lt;textElement textAlignment="Left" verticalAlignment="Middle"&gt;
						<font/>
					&lt;/textElement&gt;
				&lt;textFieldExpression   class="java.lang.String"&gt;&lt;![CDATA[$F{dddtel1}]]&gt;&lt;/textFieldExpression&gt;
				&lt;/textField&gt;
			&lt;/band&gt;
		&lt;/detail&gt;
		&lt;columnFooter&gt;
			&lt;band height="0"  isSplitAllowed="true" &gt;
			&lt;/band&gt;
		&lt;/columnFooter&gt;
		&lt;pageFooter&gt;
			&lt;band height="0"  isSplitAllowed="true" &gt;
			&lt;/band&gt;
		&lt;/pageFooter&gt;
		&lt;summary&gt;
			&lt;band height="0"  isSplitAllowed="true" &gt;
			&lt;/band&gt;
		&lt;/summary&gt;
&lt;/jasperReport&gt;

V. Abrindo o relatório

Agora, basta colocar na sua aplicação um botão ou menu que faça a chamada do relatório:

private void mniRClientesActionPerformed(java.awt.event.ActionEvent evt) {                                             

    Reports rclientes = new Reports();
    rclientes.Clientes();
    
} 

No entanto, existe um detalhe importante a ser observado. Como o relatório está sendo compilado usando o arquivo JXML, para abri-lo OBRIGATORIAMENTE é necessário que o computador esteja conectado à Internet. Existe uma diretiva no cabeçalho do arquivo XML que será acessada por uma das classes para compilar em tempo de execução o arquivo .jasper. Essa diretiva é a seguinte:

<!DOCTYPE jasperReport PUBLIC “//JasperReports//DTD Report Design//EN” “http://jasperreports.sourceforge.net/dtds/jasperreport.dtd”>

Essa diretiva aparece em todos os arquivos JXML e até o momento não descobri um modo de ignorá-la.

Então, se compilar o arquivo .jxml para .jasper usando o iReport, basta colocar na pasta do projeto e alterar o código da classe Reports, o que veremos na próxima seção.

VI. Arquivo jasper e a classe Reports alterada

Para resolver o problema do uso do arquivo JXML, que obriga a estação estar conectada à Internet (nem todos os usuários da sua aplicação podem ter esse direito ou acesso), então você deve gerar o arquivo .jasper usando o iReport (eu usei a versão 3.0.0), e salvar o arquivo na sua pasta de projeto.

Com isso, a classe LegacyJasperInputStream será dispensada.

Altere a classe Reports no método Clientes(), como segue:

public void Clientes(){

        try{        

        InputStream rclientes = Reports.class.getResourceAsStream("Clientes.jasper");
        cadastroQuery = java.beans.Beans.isDesignTime() ? null : entityManager.createQuery("SELECT c FROM Cadastro c WHERE c.idpermissao = :idpermissao");
        Permissao idpermissao = new Permissao(5);
        cadastroQuery.setParameter("idpermissao", idpermissao);
        cadastroList = java.beans.Beans.isDesignTime() ? java.util.Collections.emptyList() : cadastroQuery.getResultList();
        Map parameters = getParameters(entityManager);        
        JRBeanCollectionDataSource ds = new JRBeanCollectionDataSource(cadastroList);
        JasperPrint jp = JasperFillManager.fillReport(rclientes, new HashMap(), ds);
        JasperViewer.viewReport(jp, false);
       } catch (Exception e) {
       System.out.print(e.getMessage());
      }

    }

Por fim, sobre a unidade de persistência, eu criei todas as classes facilmente usando o NetBeans 6.5.0. Mas acho que quanto a isso, não tenho muito o que escrever, visto que já existe um tutorial bem mais qualificado do que eu colocaria aqui. Então segue ao menos a referência sobre como criar essa persistência, usando TopLink: http://www.netbeans.org/kb/docs/java/gui-db-custom_pt_BR.html

Abs