Páginas

viernes, 22 de marzo de 2013

Validacion de certificados mediante OCSP en Java

Introducción.

En esta entrada vamos a mostrar como realizar una petición a un servidor OCSP para determinar el estado de revocación de un certificado.

En el ejemplo se han usado las librerías Bouncy Castle (JDK 1.5 - JDK 1.7) disponibles en los repositorios MAVEN.

Dependencias.

Las dependencias del proyecto son:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>es.ine.sgtic</groupId>
  <artifactId>cliente-ocsp</artifactId>
  <version>1.0</version>
  <dependencies>
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcprov-jdk15on</artifactId>
          <version>1.48</version>
          <type>jar</type>
          <scope>compile</scope>
      </dependency>
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcprov-ext-jdk15on</artifactId>
          <version>1.48</version>
          <type>jar</type>
          <scope>compile</scope>
      </dependency>
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcpkix-jdk15on</artifactId>
          <version>1.48</version>
          <type>jar</type>
          <scope>compile</scope>
      </dependency>
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcmail-jdk15on</artifactId>
          <version>1.48</version>
          <type>jar</type>
          <scope>compile</scope>
      </dependency>
      <dependency>
          <groupId>org.bouncycastle</groupId>
          <artifactId>bcpg-jdk15on</artifactId>
          <version>1.48</version>
          <type>jar</type>
          <scope>compile</scope>
      </dependency>
  </dependencies>
</project>


Codigo.

El código fuente del ejemplo es el siguiente:

package es.ine.sgtic;

import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.ocsp.BasicOCSPResp;
import org.bouncycastle.cert.ocsp.CertificateID;
import org.bouncycastle.cert.ocsp.CertificateStatus;
import org.bouncycastle.cert.ocsp.OCSPException;
import org.bouncycastle.cert.ocsp.OCSPReq;
import org.bouncycastle.cert.ocsp.OCSPReqBuilder;
import org.bouncycastle.cert.ocsp.OCSPResp;
import org.bouncycastle.cert.ocsp.SingleResp;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;

public class TestOCSP {

//    private static final String URL_OCSP_RESPONDER = "http://apus.cert.fnmt.es/appsUsuario/ocsp/OcspResponder"; 

//    private static final String URL_OCSP_RESPONDER = "http://ocspape.cert.fnmt.es/ocspape/OcspResponder";
//    private static final String URL_OCSP_RESPONDER = "http://ocsp.dnie.es";
    private static final String URL_OCSP_RESPONDER = "http://ocsp.dnielectronico.es/";
  
    public static void main (String args[]) throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, OCSPException, OperatorCreationException{

        //Cargamos el almacen de certificados que contiene el certificado a verificar
        final InputStream fis = TestOCSP.class.getClassLoader().getResourceAsStream("almacen.p12");
        final KeyStore ks = KeyStore.getInstance("PKCS12");
        ks.load(fis,"passwordAlmacen".toCharArray());

        //Sacamos el certificado del que necesitamos conocer su estado de revocacion
        final X509Certificate certAnalizable = (X509Certificate)ks.getCertificate("
aliasCert");

        //Sacamos el certificado raíz de la cadena de certificación
        final Certificate[] chain = ks.getCertificateChain("
aliasCert");
        final X509Certificate certRaiz = (X509Certificate)chain[1];

        //Se carga el proveedor necesario para la petición OCSP
        Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());

        //Generamos el ID del cetificado que estamos buscando
        final CertificateID id = new CertificateID(new JcaDigestCalculatorProviderBuilder().setProvider("BC").build().get(CertificateID.HASH_SHA1), new X509CertificateHolder(certRaiz.getEncoded()), certAnalizable.getSerialNumber());

        //Instanciamos el generador de requestOCSP
        final OCSPReqBuilder ocspPeticionBuilder = new OCSPReqBuilder();
        ocspPeticionBuilder.addRequest(id);
//
//        //create details for nonce extension
//        final BigInteger nonce = BigInteger.valueOf(System.currentTimeMillis());
//        final Extension ext = new Extension(OCSPObjectIdentifiers.id_pkix_ocsp_nonce, true, new DEROctetString(nonce.toByteArray()));
//        ocspPeticionBuilder.setRequestExtensions(new Extensions(new Extension[] { ext }));

        //Se genera la petición con el certificado a verificar y su número de serie
        final OCSPReq ocspPeticion = ocspPeticionBuilder.build();

        //Se establece la conexión HTTP con el ocsp del DNIe
        final URL url = new URL(URL_OCSP_RESPONDER);
        final HttpURLConnection con = (HttpURLConnection)url.openConnection();

        //Se configuran las propiedades de la petición HTTP
        con.setRequestProperty("Content-Type", "application/ocsp-request");
        con.setRequestProperty("Accept", "application/ocsp-response");
        con.setDoOutput(true);
        final OutputStream out = con.getOutputStream();
        final DataOutputStream dataOut = new DataOutputStream(new BufferedOutputStream(out));

        //Se obtiene la respuesta del servidos OCSP del DNIe
        dataOut.write(ocspPeticion.getEncoded());
        dataOut.flush();
        dataOut.close();

        //Se parsea la respuesta y se obtiene el estado del certificado retornado por el OCSP
        final InputStream in = (InputStream)con.getContent();
        final OCSPResp ocspRespuesta = new OCSPResp(in);
        int estado = ocspRespuesta.getStatus();

        //Analizamos la respuesta
        if (estado==OCSPResp.SUCCESSFUL) {
            System.out.println("OCSP-RESPONDER: PETICION TRATADA CORRECTAMENTE");
            System.out.println(tratarRespuestaOK(ocspRespuesta));
        } else if (estado==OCSPResp.MALFORMED_REQUEST) {
            System.out.println("OCSP-RESPONDER: ERROR. PETICION MAL FORMADA");
        } else if (estado==OCSPResp.INTERNAL_ERROR) {
            System.out.println("OCSP-RESPONDER: ERROR. ERROR INTERNO DEL SERVIDOR");
        } else if (estado==OCSPResp.TRY_LATER) {
            System.out.println("OCSP-RESPONDER: ERROR. REPETIR LA PETICION MAS TARDE");
        } else if (estado==OCSPResp.SIG_REQUIRED) {
            System.out.println("OCSP-RESPONDER: ERROR. PETICION DEBE SER FIRMADA");
        } else if (estado==OCSPResp.UNAUTHORIZED) {
            System.out.println("OCSP-RESPONDER: ERROR. PETICION OCSP NO ESTA AUTORIZADA");
        } else {
            System.out.println("OCSP-RESPONDER: ERROR. TIPO DE RESPUESTA DESCONOCIDO");
        }
    }

    private static String tratarRespuestaOK(final OCSPResp ocspRespuesta) throws OCSPException, IOException {

        //TODO:Afinar este método
      
        final Object responseObject = ocspRespuesta.getResponseObject();
        final BasicOCSPResp basicOCSPResp = (BasicOCSPResp) responseObject;
      
        final SingleResp[] responses = basicOCSPResp.getResponses();
        if (responses.length == 1) {
            final SingleResp resp = responses[0];
            final Object status = resp.getCertStatus();
            if (status == CertificateStatus.GOOD) {
                return "ESTADO DEL CERTIFICADO: VALIDO";
            }
            else if (status instanceof org.bouncycastle.ocsp.RevokedStatus) {
                return "ESTADO DEL CERTIFICADO: REVOCADO";
            }
            else {
                return "ESTADO DEL CERTIFICADO: DESCONOCIDO";
            }
        }
        return "VARIAS RESPUESTAS. AFINAR EL CODIGO";
    }
}


Alternativas.

Una posible alternativa a picar código en Java es usar openssl.

Las peticiones a los cuatro servidores OCSP indicados en el código equivaldrían a:

openssl ocsp -url http://apus.cert.fnmt.es/appsUsuario/ocsp/OcspResponder -resp_text -issuer FNMT.pem -cert aliasCert.pem
openssl ocsp -url http://ocspape.cert.fnmt.es/ocspape/OcspResponder -resp_text -issuer FNMT.pem -cert
aliasCert.pem
openssl ocsp -url http://ocsp.dnie.es -resp_text -issuer FNMT.pem -cert aliasCert.pem
openssl ocsp -url http://ocsp.dnielectronico.es/ -resp_text -issuer FNMT.pem -cert aliasCert.pem

Pendiente.

Sería interesante realizar las pruebas con un certificado válido, uno revocado y uno caducado (expirado) para analizar a fondo la respuesta contenida en el objeto BasicOCSPResp. En las pruebas, con un certificado expirado tenemos la salida:

OCSP-RESPONDER: PETICION TRATADA CORRECTAMENTE
ESTADO DEL CERTIFICADO: DESCONOCIDO


Hay que profundizar también en el mecanismo para firmar la petición, dado que por ejemplo el servidor: http://apus.cert.fnmt.es/appsUsuario/ocsp/OcspResponder pide la petición firmada.

Por último conviene aclarar si cualquier servidor OCSP ofrece información sobre cualquier certificado o solo por los emitidos por ciertas CA, de ahí el exceso de respuestas con: "ESTADO DEL CERTIFICADO: DESCONOCIDO".

Enlaces de interes:

http://tools.ietf.org/html/rfc2560
http://www.bouncycastle.org/latest_releases.html

sábado, 16 de marzo de 2013

Subreports con JasperReports usando JRBeanCollectionDataSource

Introducción

Siempre que usamos Jasperreports para montar informes, pasamos todos los parámetros, incluso las etiquetas, desde el aplicativo Java. Esto ofrece varias ventajas:
  • Si por ejemplo cambia el motor de la BBDD de un aplicativo en producción, únicamente ajustamos la configuración de Hibernate sin regenerar los informes.
  • Si necesitamos el informe en varios idiomas, controlamos el idioma del usuario desde el aplicativo Java (por ejemplo recogiendo las preferencias del navegador con JSF).
Hasta la fecha siempre montábamos informes simples, es decir, con una única banda de detalle. Ahora que hemos necesitado montar algunos informes con varias bandas de detalle hemos necesitado incluir "subreports" y, como en mecanismo de envío de las distintas "JRBeanCollectionDataSource" asociadas a cada banda de detalle no me ha parecido trivial, dejo resumidos los pasos a realizar.

Clase Java para generar el informe

package org.dune.app;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.sf.jasperreports.engine.JRException;
import net.sf.jasperreports.engine.JasperExportManager;
import net.sf.jasperreports.engine.JasperFillManager;
import net.sf.jasperreports.engine.JasperPrint;
import net.sf.jasperreports.engine.JasperReport;
import net.sf.jasperreports.engine.data.JRBeanCollectionDataSource;
import net.sf.jasperreports.engine.util.JRLoader;

import org.dune.pojo.Coche;
import org.dune.pojo.Telefono;

public class TestReport {

    //Datos cabecera
    private static Map<String,Object> datosCabeceraReport;
   
    //Datos detalle
    private static List<Coche> datosDetalleReport;
    private static List<Telefono> datosDetalleSubReport;
   
    public static void main(String args[]) throws JRException{
       
        //Preparamos los datos de cabecera del report, cuando en
        //el report.jrxml añadamos los Parameter "titulo" y "fecha"
        //tendremos que asegurarnos que sean de tipo "String"
        datosCabeceraReport = new HashMap<String,Object>();
        datosCabeceraReport.put("titulo", "Inventario de bienes");
        datosCabeceraReport.put("fecha", "15/03/2013");
       
        //Preparamos los datos de detalle del report
        datosDetalleReport = new ArrayList<Coche>();
        datosDetalleReport.add(new Coche("1234-AAA", "Peugeot 207"));
        datosDetalleReport.add(new Coche("5678-AAA", "Peugeot 307"));
        datosDetalleReport.add(new Coche("9012-AAA", "Peugeot 407"));
       
        //Preparamos los datos de detalle del subreport
        datosDetalleSubReport = new ArrayList<Telefono>();
        datosDetalleSubReport.add(new Telefono("111111111", "Nokia 1000"));
        datosDetalleSubReport.add(new Telefono("222222222", "Nokia 2000"));
        datosDetalleSubReport.add(new Telefono("333333333", "Nokia 3000"));
       
        //Añadimos los datos de detalle del subreport al Map de parametros del
        //padre y montandolos sobre un JRBeanCollectionDataSource. Cuando en
        //el report.jrxml añadamos el Parameter "datosSubRep" tendremos que
        //forzar el tipo a: "net.sf.jasperreports.engine.data.JRBeanCollectionDataSource"
        datosCabeceraReport.put("datosSubRep", new JRBeanCollectionDataSource(datosDetalleSubReport));
       
        //Montamos el objeto JRMapCollectionDataSource con los datos de la bandas de detalle del report
        final JRBeanCollectionDataSource datosReport = new JRBeanCollectionDataSource(datosDetalleReport);
       
        //Generamos el informe compilado
        creaInforme(datosCabeceraReport, datosReport);
    }
   
    private static void creaInforme(final Map<String,Object> datosCabeceraReport, final JRBeanCollectionDataSource datosReport) throws JRException{
       
        //Cargamos el informe previamente compilado
        final String urlReport = TestReport.class.getResource("/report.jasper").getFile();
        final JasperReport informe = (JasperReport) JRLoader.loadObject(urlReport);

        //Añadimos los datos
        final JasperPrint informePrint = JasperFillManager.fillReport(informe, datosCabeceraReport, datosReport);

        //Exportamos a pdf
        JasperExportManager.exportReportToPdfFile(informePrint,"./src/main/resources/report.pdf");
    }
}


Cuestiones fundamentales

Los detalles fundamentales se han introducido como comentarios en el código de la clase anterior, pero merece la pena repasarlos:
  • Los "parámetros estáticos" del report padre, esto es, todos aquellos que no aparecen en la banda de detalle del informe padre, se pasan al JasperFillManager dentro del objeto: final Map<String,Object> datosCabeceraReport.
  • Los datos de detalle del subreport (lista de telefonos) se pasan al JasperFillManager como un parámetro de tipo JRBeanCollectionDataSource dentro del objeto: final Map<String,Object> datosCabeceraReport .
  • Los datos de detalle del report (lista de coches) padre se pasan al JasperFillManager dentro del objeto: final JRBeanCollectionDataSource datosReport.
  • En el report.jrxml hemos definido los parámetros: titulo, fecha y datosSubRep. Los dos primeros, de tipo String, se usarán en la banda título del report padre. El tercer parámetro, de tipo JRBeanCollectionDataSource, se usará en la propiedad "Data Source Expression" del subreport: $P{datosSubRep}.
  • En el report.jrxml hemos definido los "Fields": matricula y modelo, que son las columnas del área de detalle y que son los atributos de la clase Coche.java.
  • En el subreport.jrxml hemos definido los "Fields": numero y tipo, que son las columnas del área de detalle y que son los atributos de la clase Telefono.java.
Clases java con los "POJO" de las bandas de detalle

La clase Coche.java se usará en la banda de detalle del informe padre.

package org.dune.pojo;

public class Coche {

    private String matricula;
    private String modelo;
       
    public Coche(final String matricula, final String modelo) {
        super();
        this.matricula = matricula;
        this.modelo = modelo;
    }

    public String getMatricula() {
        return matricula;
    }

    public void setMatricula(String matricula) {
        this.matricula = matricula;
    }

    public String getModelo() {
        return modelo;
    }

    public void setModelo(String modelo) {
        this.modelo = modelo;
    }
}


La clase Telefono.java se usará en la banda de detalle del subinforme.

package org.dune.pojo;

public class Telefono {

    private String numero;
    private String tipo;

    public Telefono(final String numero, final String tipo) {
        this.numero = numero;
        this.tipo = tipo;
    }

    public String getNumero() {
        return numero;
    }

    public void setNumero(String numero) {
        this.numero = numero;
    }

    public String getTipo() {
        return tipo;
    }

    public void setTipo(String tipo) {
        this.tipo = tipo;
    }
}


report.jrxml

El aspecto general del informe padre en el diseñador es:


El código xml del informe padre es:

<?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="report1" language="groovy" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20">
    <property name="ireport.zoom" value="1.4641000000000008"/>
    <property name="ireport.x" value="0"/>
    <property name="ireport.y" value="0"/>
    <parameter name="titulo" class="java.lang.String"/>
    <parameter name="fecha" class="java.lang.String"/>
    <parameter name="datosSubRep" class="net.sf.jasperreports.engine.data.JRBeanCollectionDataSource"/>
    <field name="matricula" class="java.lang.String"/>
    <field name="modelo" class="java.lang.String"/>
    <title>
        <band height="51" splitType="Stretch">
            <rectangle radius="5">
                <reportElement mode="Transparent" x="2" y="4" width="547" height="44"/>
            </rectangle>
            <textField>
                <reportElement x="61" y="26" width="488" height="20"/>
                <textElement verticalAlignment="Middle"/>
                <textFieldExpression class="java.lang.String"><![CDATA[$P{fecha}]]></textFieldExpression>
            </textField>
            <staticText>
                <reportElement x="6" y="6" width="55" height="20"/>
                <textElement verticalAlignment="Middle">
                    <font isBold="true"/>
                </textElement>
                <text><![CDATA[TITULO:]]></text>
            </staticText>
            <staticText>
                <reportElement x="6" y="26" width="55" height="20"/>
                <textElement verticalAlignment="Middle">
                    <font isBold="true"/>
                </textElement>
                <text><![CDATA[FECHA:]]></text>
            </staticText>
            <textField>
                <reportElement x="61" y="6" width="488" height="20"/>
                <textElement verticalAlignment="Middle"/>
                <textFieldExpression class="java.lang.String"><![CDATA[$P{titulo}]]></textFieldExpression>
            </textField>
        </band>
    </title>
    <pageHeader>
        <band height="73">
            <rectangle radius="5">
                <reportElement positionType="Float" mode="Opaque" x="2" y="50" width="547" height="22" backcolor="#C68B8B"/>
            </rectangle>
            <rectangle radius="5">
                <reportElement mode="Opaque" x="2" y="1" width="547" height="22" forecolor="#010101" backcolor="#C68B8B"/>
            </rectangle>
            <staticText>
                <reportElement x="6" y="2" width="100" height="20"/>
                <textElement verticalAlignment="Middle">
                    <font isBold="true"/>
                </textElement>
                <text><![CDATA[TELEFONOS:]]></text>
            </staticText>
            <staticText>
                <reportElement positionType="Float" x="6" y="51" width="100" height="20"/>
                <textElement verticalAlignment="Middle">
                    <font isBold="true"/>
                </textElement>
                <text><![CDATA[COCHES:]]></text>
            </staticText>
            <subreport>
                <reportElement x="0" y="24" width="555" height="24"/>
                <dataSourceExpression><![CDATA[$P{datosSubRep}]]></dataSourceExpression>
                <subreportExpression class="java.lang.String"><![CDATA["/home/eduardo/trabajo/workspaces-helios/wksTestJasper/TestJasper/src/main/resources/subreport.jasper"]]></subreportExpression>
            </subreport>
        </band>
    </pageHeader>
    <columnHeader>
        <band height="25" splitType="Stretch">
            <rectangle radius="5">
                <reportElement mode="Transparent" x="2" y="2" width="547" height="22"/>
            </rectangle>
            <staticText>
                <reportElement x="104" y="3" width="445" height="20"/>
                <textElement verticalAlignment="Middle">
                    <font isBold="true"/>
                </textElement>
                <text><![CDATA[MODELO]]></text>
            </staticText>
            <staticText>
                <reportElement x="6" y="3" width="100" height="20"/>
                <textElement verticalAlignment="Middle">
                    <font isBold="true"/>
                </textElement>
                <text><![CDATA[MATRICULA]]></text>
            </staticText>
        </band>
    </columnHeader>
    <detail>
        <band height="27" splitType="Stretch">
            <rectangle radius="5">
                <reportElement stretchType="RelativeToBandHeight" isPrintRepeatedValues="false" mode="Transparent" x="2" y="2" width="547" height="22" isPrintWhenDetailOverflows="true"/>
            </rectangle>
            <textField>
                <reportElement x="6" y="3" width="100" height="20"/>
                <textElement verticalAlignment="Middle"/>
                <textFieldExpression class="java.lang.String"><![CDATA[$F{matricula}]]></textFieldExpression>
            </textField>
            <textField>
                <reportElement x="104" y="3" width="445" height="20"/>
                <textElement verticalAlignment="Middle"/>
                <textFieldExpression class="java.lang.String"><![CDATA[$F{modelo}]]></textFieldExpression>
            </textField>
        </band>
    </detail>
</jasperReport>


subreport.jrxml

El aspecto general del subinforme en el diseñador es:


El código xml del subinforme padre es:

<?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="subreport" language="groovy" pageWidth="555" pageHeight="100" columnWidth="555" leftMargin="0" rightMargin="0" topMargin="0" bottomMargin="0">
    <property name="ireport.zoom" value="1.464100000000001"/>
    <property name="ireport.x" value="0"/>
    <property name="ireport.y" value="0"/>
    <field name="numero" class="java.lang.String"/>
    <field name="tipo" class="java.lang.String"/>
    <columnHeader>
        <band height="25" splitType="Stretch">
            <rectangle radius="5">
                <reportElement mode="Transparent" x="2" y="2" width="547" height="22"/>
            </rectangle>
            <staticText>
                <reportElement x="6" y="3" width="100" height="20"/>
                <textElement verticalAlignment="Middle">
                    <font isBold="true"/>
                </textElement>
                <text><![CDATA[NUMERO]]></text>
            </staticText>
            <staticText>
                <reportElement x="106" y="3" width="441" height="20"/>
                <textElement verticalAlignment="Middle">
                    <font isBold="true"/>
                </textElement>
                <text><![CDATA[TIPO]]></text>
            </staticText>
        </band>
    </columnHeader>
    <detail>
        <band height="25" splitType="Stretch">
            <rectangle radius="5">
                <reportElement stretchType="RelativeToTallestObject" isPrintRepeatedValues="false" mode="Transparent" x="2" y="1" width="547" height="22" isPrintWhenDetailOverflows="true"/>
            </rectangle>
            <textField>
                <reportElement x="106" y="2" width="441" height="20"/>
                <textElement/>
                <textFieldExpression class="java.lang.String"><![CDATA[$F{tipo}]]></textFieldExpression>
            </textField>
            <textField>
                <reportElement x="6" y="2" width="100" height="20"/>
                <textElement/>
                <textFieldExpression class="java.lang.String"><![CDATA[$F{numero}]]></textFieldExpression>
            </textField>
        </band>
    </detail>
</jasperReport>


Aspecto del informe generado



Dependencias

Las dependencias Maven del proyecto son:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.dune</groupId>
  <artifactId>TestJasper</artifactId>
  <version>1.0</version>
  <dependencies>
      <dependency>
          <groupId>net.sf.jasperreports</groupId>
          <artifactId>jasperreports</artifactId>
          <version>4.0.0</version>
          <type>jar</type>
          <scope>compile</scope>
      </dependency>
      <dependency>
          <groupId>org.codehaus.groovy</groupId>
          <artifactId>groovy-all</artifactId>
          <version>1.7.5</version>
          <type>jar</type>
          <scope>compile</scope>
      </dependency>
  </dependencies>
</project>