Cómo crear archivos XML en Java con JAXB

En esta ocasión vamos a presentar cómo crear archivos XML con JAXB, y cómo estar seguros de que son válidos de acuerdo a determinado archivo xsd.

Primero, se presenará una breve descripción de lo que son los archivos XML, para luego ver cómo se crea un archivo de este tipo con JAXB, finalmente se mostrará cómo validar este archivo contra un XSD.

¿Qué son los archivos XML?

De acuerdo a vogella.com, un archivo XML está conformado por elementos, donde cada elemento está formado por una etiqueta de inicio, el contenido y una etiqueta de cierre.

Estos archivos tienen varios requisitos que deben ser cumplidos para que se consideren como válidos, entre los cuales se encuentran los siguientes:
  1. Deben tener uno y solo un elemento raíz.
  2. Siempre deben iniciar con un prólogo.
  3. Cada etiqueta que inicia un elemento debe contener su correspondiente etiqueta que cierra.
  4. Todas las etiquetas deben estar completamente anidadas

¿Qué es JAXB?

JAXB son las iniciales de Java Architecture for XML Binding. Este es un estándar de Java que define cómo los objetos Java son convertidos de y hacia XML. Este estándar define un API para convertir objetos Java a XML y viceversa.

Este es un buen tutorial de JAXB, que te muestra de manera muy sencilla sus bondades.

Ejemplo Práctico

En este caso en particular, nuestro objetivo era lograr generar el formato XML de los recibos de nómina de acuerdo a los nuevos requerimientos del SAT en México.

Así que lo primero que hicimos fue crear el elemento raíz:
 import javax.xml.bind.annotation.XmlAttribute;

import javax.xml.bind.annotation.XmlRootElement;

//This statement means that class "Comprobante.java" is the root-element of our example
@XmlRootElement(namespace = "http://www.sat.gob.mx/cfd/3", name="Comprobante")
public class Comprobante {
    private Emisor emisor;
    
    private String version;
    private String serie;
    private String folio;
    private String fecha;
    private String tipoDeComprobante;
    private String formaDePago;
    private String condicionesDePago;
    private String metodoDePago;
    private String noCertificado;
    private String subTotal;
    private String descuento;
    private String motivoDescuento;
    private String TipoCambio;
    private String Moneda;
    private String total;
    private String LugarExpedicion;
    private String sello;

   
    public Emisor getEmisor() {
        return emisor;
    }

    public void setEmisor(Emisor emisor) {
        this.emisor = emisor;
    }
    
    

    @XmlAttribute
    public String getVersion() {
        return version;
    }

    public void setVersion(String version) {
        this.version = version;
    }
    @XmlAttribute
    public String getSerie() {
        return serie;
    }

    public void setSerie(String serie) {
        this.serie = serie;
    }
    @XmlAttribute
    public String getFolio() {
        return folio;
    }

    public void setFolio(String folio) {
        this.folio = folio;
    }
    @XmlAttribute
    public String getFecha() {
        return fecha;
    }
    
    public void setFecha(String fecha) {
        this.fecha = fecha;
    }
    @XmlAttribute
    public String getTipoDeComprobante() {
        return tipoDeComprobante;
    }

    public void setTipoDeComprobante(String tipoDeComprobante) {
        this.tipoDeComprobante = tipoDeComprobante;
    }
    @XmlAttribute
    public String getFormaDePago() {
        return formaDePago;
    }

    public void setFormaDePago(String formaDePago) {
        this.formaDePago = formaDePago;
    }
    @XmlAttribute
    public String getCondicionesDePago() {
        return condicionesDePago;
    }

    public void setCondicionesDePago(String condicionesDePago) {
        this.condicionesDePago = condicionesDePago;
    }
    @XmlAttribute
    public String getMetodoDePago() {
        return metodoDePago;
    }

    public void setMetodoDePago(String metodoDePago) {
        this.metodoDePago = metodoDePago;
    }
    @XmlAttribute
    public String getNoCertificado() {
        return noCertificado;
    }

    public void setNoCertificado(String noCertificado) {
        this.noCertificado = noCertificado;
    }
    @XmlAttribute
    public String getSubTotal() {
        return subTotal;
    }

    public void setSubTotal(String subTotal) {
        this.subTotal = subTotal;
    }
    @XmlAttribute
    public String getDescuento() {
        return descuento;
    }

    public void setDescuento(String descuento) {
        this.descuento = descuento;
    }
    @XmlAttribute
    public String getMotivoDescuento() {
        return motivoDescuento;
    }

    public void setMotivoDescuento(String motivoDescuento) {
        this.motivoDescuento = motivoDescuento;
    }
    @XmlAttribute
    public String getTipoCambio() {
        return TipoCambio;
    }

    public void setTipoCambio(String TipoCambio) {
        this.TipoCambio = TipoCambio;
    }
    @XmlAttribute
    public String getMoneda() {
        return Moneda;
    }

    public void setMoneda(String Moneda) {
        this.Moneda = Moneda;
    }
    @XmlAttribute
    public String getTotal() {
        return total;
    }

    public void setTotal(String total) {
        this.total = total;
    }
    @XmlAttribute
    public String getLugarExpedicion() {
        return LugarExpedicion;
    }

    public void setLugarExpedicion(String LugarExpedicion) {
        this.LugarExpedicion = LugarExpedicion;
    }
    @XmlAttribute
    public String getSello() {
        return sello;
    }

    public void setSello(String sello) {
        this.sello = sello;
    } 
}

Como se puede ver, es una clase Java común con algunas anotaciones propias del JAXB. La anotación de @XmlRootElement le indica a Java que este es el elemento raíz del archivo XML.
Por su parte, la anotación @XmlAttribute es para que se muestren estos elementos como atributos de la etiqueta, y no como elementos anidados.

Por ejemplo, con la anotación @XmlAttribute el archivo XML se desplegará así:

y sin la anotación, el xml se vería así (solo incluí los primeros tres atributos):

CONTADO864.212013-12-12T11:46:543
 
Ahora, creamos uno de los elementos que deben ir anidados en el componente raíz Comprobante
@XmlRootElement(name = "emisor")
// If you want you can define the order in which the fields are written
// Optional
@XmlType(propOrder = {"rfc", "nombre", "domicilioFiscal", "expedidoEn", "regimenFiscal"})
public class Emisor {
    private String rfc;
    private String nombre;
    private DomicilioFiscal domicilioFiscal;
    private RegimenFiscal regimenFiscal;
    private ExpedidoEn expedidoEn;

    public Emisor() {
    }

    public Emisor(String rfc, String nombre) {
        this.rfc = rfc;
        this.nombre = nombre;
    }

    @XmlAttribute
    public String getRfc() {
        return rfc;
    }

    public void setRfc(String rfc) {
        this.rfc = rfc;
    }

    @XmlAttribute
    public String getNombre() {
        return nombre;
    }

    public void setNombre(String nombre) {
        this.nombre = nombre;
    }
    @XmlElement(name = "DomicilioFiscal")
    public DomicilioFiscal getDomicilioFiscal() {
        return domicilioFiscal;
    }

    public void setDomicilioFiscal(DomicilioFiscal domicilioFiscal) {
        this.domicilioFiscal = domicilioFiscal;
    }
    @XmlElement(name = "RegimenFiscal")
    public RegimenFiscal getRegimenFiscal() {
        return regimenFiscal;
    }

    public void setRegimenFiscal(RegimenFiscal regimenFiscal) {
        this.regimenFiscal = regimenFiscal;
    }
    @XmlElement(name = "ExpedidoEn")
    public ExpedidoEn getExpedidoEn() {
        return expedidoEn;
    }

    public void setExpedidoEn(ExpedidoEn expedidoEn) {
        this.expedidoEn = expedidoEn;
    }   
}
Como se puede apreciar, la anotación @XmlRootElement no lleva namespace; y en este caso especificamos el orden en que queremos que los atributos se desplieguen al declarar @XmlType.

En esta clase Java, también se utilizó la anotación @XmlElement para indicarle al JAXB cómo se debe desplegar en nombre del atributo en el archivo XML.

Si regresamos a la clase Comprobante, se verá que esta tiene un atributo Emisor. Es a través de estas relaciones 'has-a' que se lleva a cabo la magia. Así es como JAXB entiende que Emisor está anidado en Comprobante.

Definiendo un 'namespace' personalizado 

Cuando es necesario cumplir con ciertos requerimientos, aparentemente se complican las cosas. En nuestro caso, era necesario definir un namespace que se adaptara a las especificaciones del SAT.

Para ello fue necesario seguir las recomendaciones de este blog y de esta página. Nuestro archivo package-info quedó así:
    @XmlSchema(namespace = "http://www.sat.gob.mx/cfd/3",  
        xmlns = {               
            @XmlNs(namespaceURI = "http://www.sat.gob.mx/cfd/3", prefix = "cfdi"),  
            @XmlNs(namespaceURI = "http://www.w3.org/2001/XMLSchema-instance", prefix = "xsi")
        },  
        elementFormDefault = javax.xml.bind.annotation.XmlNsForm.QUALIFIED)
      
    package sapnomina;
      
    import javax.xml.bind.annotation.XmlNs;  
    import javax.xml.bind.annotation.XmlSchema;  

Para generar el archivo XML, se crea una clase Java que inicialice de manera apropiada todos los objetos:
public class NominaMain {

    private static final String COMPROBANTE_XML = "./sapNomina.xml";

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

        Comprobante comprobante = new Comprobante();
        
        comprobante.setVersion("3.2");
        comprobante.setSerie("HDS");
        comprobante.setFolio("3");
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss");
        comprobante.setFecha(sdf.format(new Date()));
        comprobante.setTipoDeComprobante("egreso");
        comprobante.setFormaDePago("PAGO EN UNA SOLA EXHIBICIÓN");
        comprobante.setCondicionesDePago("CONTADO");
        comprobante.setMetodoDePago("Efectivo");
        comprobante.setNoCertificado("20001000000200001745");
        comprobante.setSubTotal("4716.08");
        comprobante.setDescuento("864.21");
        comprobante.setMotivoDescuento("Deducciones nómina");
        comprobante.setTipoCambio("1.00");
        comprobante.setMoneda("MXP");
        comprobante.setTotal("3506.00");
        comprobante.setLugarExpedicion("MONTEMORELOS, NUEVO LEÓN");
        
        Emisor emisor = new Emisor("UMO8409105C1", "UNIVERSIDAD DE MONTEMORELOS, A.C.");
        DomicilioFiscal domicilio = new DomicilioFiscal("Libertad Pte.", "1300", "Barrio Matamoros", "Montemorelos");
        emisor.setDomicilioFiscal(domicilio);
        RegimenFiscal regimen = new RegimenFiscal("PERSONAS MORALES DEL REGIMEN FISCAL");
        emisor.setRegimenFiscal(regimen);
        emisor.setExpedidoEn(new ExpedidoEn());
        comprobante.setEmisor(emisor);
        
        // create JAXB context and instantiate marshaller
        JAXBContext context = JAXBContext.newInstance(Comprobante.class);
        Marshaller m = context.createMarshaller();
        
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, "http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd");
        
        // Write to System.out
        m.marshal(comprobante, System.out);

        // Write to File
        m.marshal(comprobante, new File(COMPROBANTE_XML));
}
Al ejecutar este código, nos arroja el siguiente archivo XML:

    
        
        
        
    



Validando el archivo XML

Para validar el archivo XML que se quiere generar con JAXB, existen al menos dos formas: una es validarlo justo antes de que se escriba al disco duro, y otra es validar el archivo que ya se generó.

Veamos primero la primera manera, la cual es explicada de gran manera por Blaise Doughan en este post.

Para nuestro ejemplo en particular, esta es nuestra clase que manejará y responderá cualquier error en el XML.

public class NominaValidationEventHandler implements ValidationEventHandler {

    public boolean handleEvent(ValidationEvent event) {
        System.out.println("\nEVENT");
        System.out.println("SEVERITY:  " + event.getSeverity());
        System.out.println("MESSAGE:  " + event.getMessage());
        System.out.println("LINKED EXCEPTION:  " + event.getLinkedException());
        System.out.println("LOCATOR");
        System.out.println("    LINE NUMBER:  " + event.getLocator().getLineNumber());
        System.out.println("    COLUMN NUMBER:  " + event.getLocator().getColumnNumber());
        System.out.println("    OFFSET:  " + event.getLocator().getOffset());
        System.out.println("    OBJECT:  " + event.getLocator().getObject());
        System.out.println("    NODE:  " + event.getLocator().getNode());
        System.out.println("    URL:  " + event.getLocator().getURL());
        return true;
    }
}

Y esta sería la clase principal antes mostrada, pero con el codigo correspondiente de validación justo en la línea 43.
public class NominaMain {

    private static final String COMPROBANTE_XML = "/home/osoto/sapNomina.xml";

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

        // create bookstore, assigning book
        Comprobante comprobante = new Comprobante();
        
        comprobante.setVersion("3.2");
        comprobante.setSerie("HDS");
        comprobante.setFolio("3");
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss");
        comprobante.setFecha(sdf.format(new Date()));
        comprobante.setTipoDeComprobante("egreso");
        comprobante.setFormaDePago("PAGO EN UNA SOLA EXHIBICIÓN");
        comprobante.setCondicionesDePago("CONTADO");
        comprobante.setMetodoDePago("Efectivo");
        comprobante.setNoCertificado("20001000000200001437");
        comprobante.setSubTotal("4716.08");
        comprobante.setDescuento("864.21");
        comprobante.setMotivoDescuento("Deducciones nómina");
        comprobante.setTipoCambio("1.00");
        comprobante.setMoneda("MXP");
        comprobante.setTotal("3506.00");
        comprobante.setLugarExpedicion("ZAPOPAN, JALISCO");
        
        Emisor emisor = new Emisor("UMO8409105C4", "UNIVERSIDAD DE MONTEMORELOS, A.C.");
        DomicilioFiscal domicilio = new DomicilioFiscal("Libertad Pte.", "1300", "Barrio Matamoros", "Montemorelos");
        emisor.setDomicilioFiscal(domicilio);
        RegimenFiscal regimen = new RegimenFiscal("PERSONAS MORALES DEL REGIMEN FISCAL");
        emisor.setRegimenFiscal(regimen);
        emisor.setExpedidoEn(new ExpedidoEn());
        comprobante.setEmisor(emisor);
        
        // create JAXB context and instantiate marshaller
        JAXBContext context = JAXBContext.newInstance(Comprobante.class);
        Marshaller m = context.createMarshaller();
        
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, "http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd");

        //Validation Event Handler
        try { 
            SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
            Schema schema = sf.newSchema(new File("/home/osoto/Descargas/satNomina/nomina11.xsd"));
            m.setSchema(schema);
        } catch (SAXException ex) {
            Logger.getLogger(BookMain.class.getName()).log(Level.SEVERE, null, ex);
        }
        m.setEventHandler(new NominaValidationEventHandler());
        
        // Write to System.out
        m.marshal(comprobante, System.out);

        // Write to File
        m.marshal(comprobante, new File(COMPROBANTE_XML));

Como se puede apreciar, es sumamente sencillo configurar la validación de un archivo XML, siempre y cuando se tenga el correspondiente archivo xsd.

El segundo método de validación, es propuesto por Blaise Doughan también.  La diferencia con la forma de validación antes expuesta es que en esta se valida un archivo XML que ya fue creado con anterioridad, el cual es necesario leer para validarlo.

Esta es la clase que se encarga de manejar y responder a cualquier error en el archivo XML.
public class NominaValidationErrorHandler implements ErrorHandler {

    public void error(SAXParseException arg0) throws SAXException {
        System.out.println("ERROR");
        arg0.printStackTrace(System.out);
    }

    public void fatalError(SAXParseException arg0) throws SAXException {
        System.out.println("FATAL ERROR");
        arg0.printStackTrace(System.out);
    }

    public void warning(SAXParseException arg0) throws SAXException {
        System.out.println("WARNING ERROR");
        arg0.printStackTrace(System.out);
    }
}

Esta otra clase, es la clase principal que como se puede apreciar a partir de la línea 52 está el código de validación.
public class NominaMain {

    private static final String COMPROBANTE_XML = "/home/osoto/sapNomina.xml";

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

        // create bookstore, assigning book
        Comprobante comprobante = new Comprobante();
        
        comprobante.setVersion("3.2");
        comprobante.setSerie("HDS");
        comprobante.setFolio("3");
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss");
        comprobante.setFecha(sdf.format(new Date()));
        comprobante.setTipoDeComprobante("egreso");
        comprobante.setFormaDePago("PAGO EN UNA SOLA EXHIBICIÓN");
        comprobante.setCondicionesDePago("CONTADO");
        comprobante.setMetodoDePago("Efectivo");
        comprobante.setNoCertificado("20001000000200001437");
        comprobante.setSubTotal("4716.08");
        comprobante.setDescuento("864.21");
        comprobante.setMotivoDescuento("Deducciones nómina");
        comprobante.setTipoCambio("1.00");
        comprobante.setMoneda("MXP");
        comprobante.setTotal("3506.00");
        comprobante.setLugarExpedicion("ZAPOPAN, JALISCO");
        
        Emisor emisor = new Emisor("UMO8409105C4", "UNIVERSIDAD DE MONTEMORELOS, A.C.");
        DomicilioFiscal domicilio = new DomicilioFiscal("Libertad Pte.", "1300", "Barrio Matamoros", "Montemorelos");
        emisor.setDomicilioFiscal(domicilio);
        RegimenFiscal regimen = new RegimenFiscal("PERSONAS MORALES DEL REGIMEN FISCAL");
        emisor.setRegimenFiscal(regimen);
        emisor.setExpedidoEn(new ExpedidoEn());
        comprobante.setEmisor(emisor);
        
        // create JAXB context and instantiate marshaller
        JAXBContext context = JAXBContext.newInstance(Comprobante.class);
        Marshaller m = context.createMarshaller();
        
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, "http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv32.xsd");        
        
        // Write to System.out
        m.marshal(comprobante, System.out);

        // Write to File
        m.marshal(comprobante, new File(COMPROBANTE_XML));
        
        System.out.println();
        System.out.println("Output from our XML File: ");
        
        //Validate xml file with ErrorHandler
        SAXSource source = new SAXSource(new InputSource(COMPROBANTE_XML));

        Schema schema;
        try {
            SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
            schema = sf.newSchema(new File("/home/osoto/Descargas/satNomina/nomina11.xsd"));
            Validator validator = schema.newValidator();
            validator.setErrorHandler(new NominaValidationErrorHandler());

            validator.validate(source);
        } catch (SAXException ex) {
            Logger.getLogger(BookMain.class.getName()).log(Level.SEVERE, null, ex);
        }

        // get variables from our xml file, created before
        Unmarshaller um = context.createUnmarshaller();
        Comprobante bookstore2 = (Comprobante) um.unmarshal(new FileReader(COMPROBANTE_XML));
        
    }

Conclusión

Los archivos XML son de gran utilidad y muy comunes en los sistemas computacionales hoy día para el intercambio de información. Es por ello que se hace necesario para todo programador conocer cómo generarlos y leerlos.
Gracias a JAXB es muy sencillo crear archivos XML en Java que a través de anotaciones permite manipular de manera muy sencilla las distintas opciones para generar un archivo XML totalmente a la medida.

Escrito por Ing. Omar Otoniel Soto Romero

Comentarios

Entradas populares de este blog

Batch File como Servicio de Windows

SQL y los acentos