mx.bigdata.sat.cfdi.CFDv33.java Source code

Java tutorial

Introduction

Here is the source code for mx.bigdata.sat.cfdi.CFDv33.java

Source

/*
 *  Copyright 2011 BigData.mx
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package mx.bigdata.sat.cfdi;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import mx.bigdata.sat.cfdi.v33.schema.Comprobante;
import mx.bigdata.sat.cfdi.v33.schema.ObjectFactory;
import mx.bigdata.sat.common.ComprobanteBase;
import mx.bigdata.sat.common.NamespacePrefixMapperImpl;
import mx.bigdata.sat.common.URIResolverImpl;
import mx.bigdata.sat.security.KeyLoaderEnumeration;
import mx.bigdata.sat.security.factory.KeyLoaderFactory;
import org.apache.commons.codec.binary.Base64;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.ErrorHandler;

import javax.xml.XMLConstants;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.util.JAXBSource;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.Result;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.*;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public final class CFDv33 implements CFDI {

    private static final String XSLT = "/xslt/cadenaoriginal_3_3.xslt";

    private static final String[] XSD = new String[] { "/xsd/common/tdCFDI.xsd", "/xsd/common/catCFDI.xsd",
            "/xsd/v33/cfdv33.xsd", "/xsd/v33/TimbreFiscalDigitalv11.xsd", "/xsd/common/ecc/v11/ecc11.xsd",
            "/xsd/common/donat/v11/donat11.xsd", "/xsd/common/divisas/divisas.xsd",
            "/xsd/common/implocal/implocal.xsd", "/xsd/common/leyendasFisc/leyendasFisc.xsd",
            "/xsd/common/pfic/pfic.xsd", "/xsd/common/TuristaPasajeroExtranjero/TuristaPasajeroExtranjero.xsd",
            "/xsd/common/spei/spei.xsd", "/xsd/common/detallista/detallista.xsd",
            "/xsd/common/cfdiregistrofiscal/cfdiregistrofiscal.xsd", "/xsd/common/nomina/catNomina.xsd",
            "/xsd/common/nomina/v12/nomina12.xsd", "/xsd/common/pagoenespecie/pagoenespecie.xsd",
            "/xsd/common/valesdedespensa/valesdedespensa.xsd",
            "/xsd/common/consumodecombustibles/consumodecombustibles.xsd", "/xsd/common/aerolineas/aerolineas.xsd",
            "/xsd/common/notariospublicos/notariospublicos.xsd", "/xsd/common/vehiculousado/vehiculousado.xsd",
            "/xsd/common/servicioparcialconstruccion/servicioparcialconstruccion.xsd",
            "/xsd/common/renovacionysustitucionvehiculos/renovacionysustitucionvehiculos.xsd",
            "/xsd/common/certificadodedestruccion/certificadodedestruccion.xsd",
            "/xsd/common/obrasarteantiguedades/obrasarteantiguedades.xsd", "/xsd/common/ine/v11/INE11.xsd",
            "/xsd/common/ComercioExterior/catComExt.xsd", "/xsd/common/ComercioExterior/v11/ComercioExterior11.xsd",
            "/xsd/common/Pagos/catPagos.xsd", "/xsd/common/Pagos/Pagos10.xsd", "/xsd/common/iedu/iedu.xsd",
            "/xsd/common/ventavehiculos/v11/ventavehiculos11.xsd", "/xsd/common/terceros/terceros11.xsd",
            "/xsd/common/AcreditamientoIEPS/AcreditamientoIEPS10.xsd", "/xsd/common/ecb/ecb.xsd",
            "/xsd/common/psgcfdsp/psgcfdsp.xsd", "/xsd/common/psgecfd/psgecfd.xsd" };

    private static final String XML_HEADER = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";

    private static final String BASE_CONTEXT = "mx.bigdata.sat.cfdi.v33.schema";

    private final static Joiner JOINER = Joiner.on(':');

    private static Map<List<String>, JAXBContext> contextMap = new HashMap<>();
    private final JAXBContext context;

    public static final ImmutableMap<String, String> PREFIXES = ImmutableMap.of(
            "http://www.w3.org/2001/XMLSchema-instance", "xsi", "http://www.sat.gob.mx/cfd/3", "cfdi",
            "http://www.sat.gob.mx/TimbreFiscalDigital", "tfd");

    private final Map<String, String> localPrefixes = Maps.newHashMap(PREFIXES);

    private TransformerFactory tf;

    final Comprobante document;

    public CFDv33(InputStream in, String... contexts) throws Exception {
        this.context = getContext(contexts);
        this.document = load(in);
    }

    public CFDv33(Comprobante comprobante, String... contexts) throws Exception {
        this.context = getContext(contexts);
        this.document = copy(comprobante);
    }

    @Override
    public void addNamespace(String uri, String prefix) {
        localPrefixes.put(uri, prefix);
    }

    @Override
    public void setTransformerFactory(TransformerFactory tf) {
        this.tf = tf;
        tf.setURIResolver(new URIResolverImpl());
    }

    @Override
    public void sellar(PrivateKey key, X509Certificate cert) throws Exception {
        String nc = new String(cert.getSerialNumber().toByteArray());
        cert.checkValidity();
        byte[] bytes = cert.getEncoded();
        Base64 b64 = new Base64(-1);
        String certStr = b64.encodeToString(bytes);
        document.setCertificado(certStr);
        document.setNoCertificado(nc);
        String signature = getSignature(key);
        document.setSello(signature);
    }

    public Comprobante sellarComprobante(PrivateKey key, X509Certificate cert) throws Exception {
        sellar(key, cert);
        return doGetComprobante();
    }

    @Override
    public void validar() throws Exception {
        validar(null);
    }

    @Override
    public void validar(ErrorHandler handler) throws Exception {
        SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Source[] schemas = new Source[XSD.length];
        for (int i = 0; i < XSD.length; i++) {
            schemas[i] = new StreamSource(getClass().getResourceAsStream(XSD[i]));
        }
        Schema schema = sf.newSchema(schemas);
        Validator validator = schema.newValidator();
        if (handler != null) {
            validator.setErrorHandler(handler);
        }
        validator.validate(new JAXBSource(context, document));
    }

    @Override
    public void verificar() throws Exception {
        String certStr = document.getCertificado();
        Base64 b64 = new Base64();
        byte[] cbs = b64.decode(certStr);

        X509Certificate cert = KeyLoaderFactory
                .createInstance(KeyLoaderEnumeration.PUBLIC_KEY_LOADER, new ByteArrayInputStream(cbs)).getKey();

        String sigStr = document.getSello();
        byte[] signature = b64.decode(sigStr);
        byte[] bytes = getOriginalBytes();
        Signature sig = Signature.getInstance("SHA256withRSA");
        sig.initVerify(cert);
        sig.update(bytes);
        boolean bool = sig.verify(signature);
        if (!bool) {
            throw new Exception("Invalid signature");
        }
    }

    //Verifica textualmente el XML con el XSD (Funciona cuando queremos validar un XML que NO fue creado con esta librera
    public void verificar(InputStream in) throws Exception {
        String certStr = document.getCertificado();
        Base64 b64 = new Base64();
        byte[] cbs = b64.decode(certStr);

        X509Certificate cert = KeyLoaderFactory
                .createInstance(KeyLoaderEnumeration.PUBLIC_KEY_LOADER, new ByteArrayInputStream(cbs)).getKey();

        String sigStr = document.getSello();
        byte[] signature = b64.decode(sigStr);
        byte[] bytes = getOriginalBytes(in);
        Signature sig = Signature.getInstance("SHA256withRSA");
        sig.initVerify(cert);
        sig.update(bytes);
        boolean bool = sig.verify(signature);
        if (!bool) {
            throw new Exception("Invalid signature.");
        }
    }

    @Override
    public void guardar(OutputStream out) throws Exception {
        Marshaller m = context.createMarshaller();
        m.setProperty("com.sun.xml.bind.namespacePrefixMapper", new NamespacePrefixMapperImpl(localPrefixes));
        m.setProperty(Marshaller.JAXB_FRAGMENT, Boolean.TRUE);
        m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
        m.setProperty(Marshaller.JAXB_SCHEMA_LOCATION, getSchemaLocation());
        byte[] xmlHeaderBytes = XML_HEADER.getBytes("UTF8");
        out.write(xmlHeaderBytes);
        m.marshal(document, out);
    }

    //Se implement este mtodo para que agregue los esquemas de manera automtica (solo hay que enviar los contexts en el constructor)
    //Se deben agregar todos los complementos en todas sus versiones (tambien a todas las versiones de CFDi segn sus complementos)
    private String getSchemaLocation() throws Exception {
        List<String> contexts = new ArrayList<>();
        String schema = "http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd";
        if (document != null && document.getComplemento() != null && document.getComplemento().size() > 0) {
            for (Comprobante.Complemento o : document.getComplemento()) {
                for (Object c : o.getAny()) {
                    if (c instanceof mx.bigdata.sat.cfdi.v33.schema.TimbreFiscalDigital) {
                        //El schema location debe de ir en el nodo de TFD, no en el de comprobante.
                    } else if (c instanceof mx.bigdata.sat.common.nomina.v12.schema.Nomina) {
                        if (!schema.contains(
                                "http://www.sat.gob.mx/nomina12 http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina12.xsd")) {
                            schema += " http://www.sat.gob.mx/nomina12 http://www.sat.gob.mx/sitio_internet/cfd/nomina/nomina12.xsd";
                        }
                    } else if (c instanceof mx.bigdata.sat.common.implocal.schema.ImpuestosLocales) {
                        if (!schema.contains(
                                "http://www.sat.gob.mx/implocal http://www.sat.gob.mx/sitio_internet/cfd/implocal/implocal.xsd")) {
                            schema += " http://www.sat.gob.mx/implocal http://www.sat.gob.mx/sitio_internet/cfd/implocal/implocal.xsd";
                        }
                    } else if (c instanceof mx.bigdata.sat.common.pagos.schema.Pagos) {
                        if (!schema.contains(
                                "http://www.sat.gob.mx/Pagos http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos10.xsd")) {
                            schema += " http://www.sat.gob.mx/Pagos http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos10.xsd";
                        }
                    } else {
                        System.out.println("El complemento " + c + " an no ha sido declarado.");
                    }
                }
            }
            if (!contexts.isEmpty()) {
                getContext(contexts.toArray(new String[contexts.size()]));
            }
        }
        return schema;
    }

    @Override
    public String getCadenaOriginal() throws Exception {
        byte[] bytes = getOriginalBytes();
        return new String(bytes, "UTF8");
    }

    public static Comprobante newComprobante(InputStream in) throws Exception {
        return load(in);
    }

    byte[] getOriginalBytes() throws Exception {
        JAXBSource in = new JAXBSource(context, document);
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Result out = new StreamResult(baos);
        TransformerFactory factory = tf;
        if (factory == null) {
            factory = TransformerFactory.newInstance();
            factory.setURIResolver(new URIResolverImpl());
        }
        Transformer transformer = factory.newTransformer(new StreamSource(getClass().getResourceAsStream(XSLT)));
        transformer.transform(in, out);
        return baos.toByteArray();
    }

    //Funciona en conjunto con: verificar(InputStream in)
    byte[] getOriginalBytes(InputStream in) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        Source source = new StreamSource(in);
        Result out = new StreamResult(baos);
        TransformerFactory factory = tf;
        if (factory == null) {
            factory = TransformerFactory.newInstance();
            factory.setURIResolver(new URIResolverImpl());
        }
        Transformer transformer = factory.newTransformer(new StreamSource(getClass().getResourceAsStream(XSLT)));
        transformer.transform(source, out);
        in.close();
        return baos.toByteArray();
    }

    String getSignature(PrivateKey key) throws Exception {
        byte[] bytes = getOriginalBytes();
        Signature sig = Signature.getInstance("SHA256withRSA");
        sig.initSign(key);
        sig.update(bytes);
        byte[] signed = sig.sign();
        Base64 b64 = new Base64(-1);
        return b64.encodeToString(signed);
    }

    @Override
    public ComprobanteBase getComprobante() throws Exception {
        return new CFDv33ComprobanteBase(doGetComprobante());
    }

    Comprobante doGetComprobante() throws Exception {
        return copy(document);
    }

    // Defensive deep-copy
    private Comprobante copy(Comprobante comprobante) throws Exception {
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        DocumentBuilder db = dbf.newDocumentBuilder();
        Document doc = db.newDocument();
        Marshaller m = context.createMarshaller();
        m.marshal(comprobante, doc);
        Unmarshaller u = context.createUnmarshaller();
        return (Comprobante) u.unmarshal(doc);
    }

    public static final class CFDv33ComprobanteBase implements ComprobanteBase {

        private final Comprobante document;

        public CFDv33ComprobanteBase(Comprobante document) {
            this.document = document;
        }

        @Override
        public boolean hasComplemento() {
            return document.getComplemento() != null;
        }

        @Override
        public List<Object> getComplementoGetAny() {
            return document.getComplemento().stream().map(complemento -> (Object) complemento)
                    .collect(Collectors.toList());
        }

        @Override
        public String getSello() {
            return document.getSello();
        }

        @Override
        public void setComplemento(Element element) {
            ObjectFactory of = new ObjectFactory();
            Comprobante.Complemento comp = of.createComprobanteComplemento();
            List<Object> list = comp.getAny();
            list.add(element);
            document.getComplemento().add(comp);
        }

        @Override
        public Object getComprobante() {
            return document;
        }

    }

    private static JAXBContext getContext(String[] contexts) throws Exception {
        List<String> ctx = Lists.asList(BASE_CONTEXT, contexts);
        if (!contextMap.containsKey(ctx)) {
            JAXBContext context = JAXBContext.newInstance(JOINER.join(ctx));
            contextMap.put(ctx, context);
        }
        return contextMap.get(ctx);
    }

    private static Comprobante load(InputStream source, String... contexts) throws Exception {
        JAXBContext context = getContext(contexts);
        try {
            Unmarshaller u = context.createUnmarshaller();
            return (Comprobante) u.unmarshal(source);
        } finally {
            source.close();
        }
    }

    static void dump(String title, byte[] bytes, PrintStream out) {
        out.printf("%s: ", title);
        for (byte b : bytes) {
            out.printf("%02x ", b & 0xff);
        }
        out.println();
    }

}