package sign;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.*;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.*;

import javax.xml.crypto.*;
import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dom.*;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.keyinfo.*;
import javax.xml.crypto.dsig.spec.*;
import javax.xml.namespace.NamespaceContext;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.*;

import org.w3c.dom.*;

import org.xml.sax.SAXException;

import xutil.UniversalNamespaceCache;
import xutil.UniversalNamespaceResolver;

public class FirmarArchivo02
{
  private static final String KEY_STORE_TYPE = "JKS";
      private static final String KEY_STORE_NAME = "C:\\Proyectos\\Java\\Repositorio\\devMatce";
      private static final String KEY_STORE_PASS = "nkQ008Tz";
      private static final String PRIVATE_KEY_PASS = "hrz008";
      private static final String KEY_ALIAS = "certPatito";

      //private static final String PATH = "/PatientRecord/Account";
      private static final String PATH = "/dodas";
      //private static final String ID = "acct";
      private static final String ID = "body";

      private static enum SignatureType {
          SIGN_BY_ID,
          SIGN_BY_PATH,
          SIGN_WHOLE_DOCUMENT
      };
  

  
  public static void main(String[] args)
  {
      if(args.length<2)
      {
        usage();
        return;
      }
      
      
      String inputFile=args[0];
      String outputFile=args[1];
      SignatureType sigType=null;
      
      if(args.length>=3)
      {
        if("id".equals(args[2]))
        {
          sigType=SignatureType.SIGN_BY_ID;
        }
        else if("path".equals(args[2]))
        {
          sigType=SignatureType.SIGN_BY_PATH;
        }
        else
        {
          sigType=SignatureType.SIGN_WHOLE_DOCUMENT;
        }
      }
      
      
      DocumentBuilderFactory dbFactory=DocumentBuilderFactory.newInstance();
      
      dbFactory.setNamespaceAware(true);
      
      try
      {
        // Lee todo el contenido del archivo y lo interpreta como XML.
        Document doc=dbFactory.newDocumentBuilder().parse(new FileInputStream(inputFile));
        
        String providerName=System.getProperty("jsr105Provider", "org.jcp.xml.dsig.internal.dom.XMLDSigRI");
        
        final XMLSignatureFactory sigFactory=XMLSignatureFactory.getInstance("DOM", (Provider)Class.forName(providerName).newInstance());
        
        Node nodeToSign=null;
        Node sigParent=null;
        String referenceURI=null;
        XPathExpression expr=null;
        NodeList nodes=null;
        List transforms=null;
        
        XPathFactory factory=XPathFactory.newInstance();
        XPath xpath=factory.newXPath();
        
        xpath.setNamespaceContext(new UniversalNamespaceCache(doc,true));
        
        switch(sigType)
        {
          case SIGN_BY_ID:
            
            try
            {
              expr=xpath.compile(String.format("//*[@id='%s']", ID));
              nodes=(NodeList)expr.evaluate(doc, XPathConstants.NODESET);
                
                if(nodes.getLength()==0)
                {
                  System.out.println("No se pudo encontrar al nodo con id: "+ID);
                  return;
                }
              
              nodeToSign=nodes.item(0);
              sigParent=nodeToSign.getParentNode();
              referenceURI="#"+ID;
            }
            catch(XPathExpressionException xpeEx)
            {
              System.err.println("No pudo compilar la expresión xPath dada: "+xpeEx.getMessage());
              return;
            }
            
            break;
          
          case SIGN_BY_PATH:
            
            try
            {
              //xpath.setNamespaceContext();
              expr=xpath.compile(PATH);
              nodes=(NodeList)expr.evaluate(doc, XPathConstants.NODESET);
              
              if(nodes.getLength()<1)
              {
                System.out.println("Documento XML inválido; no se encontró el nodo por la ruta XML dada: "+PATH);
                return;
              }
            }
            catch(XPathExpressionException xpexEx)
            {
              System.out.println("No pudo compilar la ruta XML dada: "+xpexEx.getMessage());
              return;
            }
            
            nodeToSign=nodes.item(0);
            //sigParent=nodeToSign.getParentNode();
            sigParent=nodes.item(0);
            referenceURI="";
            
            
            
            try
            {
              transforms=new ArrayList<Transform>(){
              {
                add(sigFactory.newTransform(Transform.XPATH, new XPathFilterParameterSpec(PATH)));
                add(sigFactory.newTransform(Transform.ENVELOPED, (TransformParameterSpec)null));
              }};
            }
            catch(NoSuchAlgorithmException nsalEx)
            {
              System.err.println("Excepción al transformar el elemento XML: "+nsalEx.getMessage());
              return;
            }
            catch(InvalidAlgorithmParameterException iapex)
            {
              System.err.println("Excepción al trasformar los elementos XML: "+iapex.getMessage());
              return;
            }
            
            break;
          
          
          default:
            
              sigParent=doc.getDocumentElement();
              referenceURI="";
            
              try
              {
                transforms=Collections.singletonList(sigFactory.newTransform(Transform.ENVELOPED, (TransformParameterSpec)null));
              }
              catch(NoSuchAlgorithmException nsalEx)
              {
                System.err.println("Excepción al transformar el elemento XML: "+nsalEx.getMessage());
                return;
              }
              catch(InvalidAlgorithmParameterException iapex)
              {
                System.err.println("Excepción al trasformar los elementos XML: "+iapex.getMessage());
                return;
              }
              
            break;
        } // Fin switch
        
        
        
        
        //Buscando llave para firmar.
        try
        {
          KeyStore keyStore=KeyStore.getInstance(KEY_STORE_TYPE);
          
          keyStore.load(new FileInputStream(KEY_STORE_NAME), KEY_STORE_PASS.toCharArray());
          
          PrivateKey privateKey=(PrivateKey)keyStore.getKey(KEY_ALIAS, PRIVATE_KEY_PASS.toCharArray());
          
          X509Certificate cert=(X509Certificate)keyStore.getCertificate(KEY_ALIAS);
          PublicKey publicKey=cert.getPublicKey();
          
          // Referencia al documento.
          //Reference ref=sigFactory.newReference(referenceURI, sigFactory.newDigestMethod(DigestMethod.SHA1, null), transforms, null, null);
          Reference ref=sigFactory.newReference(referenceURI, sigFactory.newDigestMethod(DigestMethod.SHA256, null), transforms, null, null);
          
          //Firmando la información.
          
          SignedInfo signedInfo=sigFactory.newSignedInfo(
                                  sigFactory.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS, (C14NMethodParameterSpec)null),
                                  sigFactory.newSignatureMethod(SignatureMethod.RSA_SHA1, null),
                                  Collections.singletonList(ref));
          
          /*
          SignedInfo signedInfo=sigFactory.newSignedInfo(
                                  sigFactory.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE_WITH_COMMENTS, (C14NMethodParameterSpec)null),
                                  sigFactory.newSignatureMethod(SignatureMethod.DSA_SHA1, null),
                                  Collections.singletonList(ref));
          */
          
          
          KeyInfoFactory keyInfoFactory=sigFactory.getKeyInfoFactory();
          KeyValue keyValue=keyInfoFactory.newKeyValue(publicKey);
          
          KeyInfo keyInfo=keyInfoFactory.newKeyInfo(Collections.singletonList(keyValue));
          
          DOMSignContext dsc=new DOMSignContext(privateKey, sigParent);
          
          // Creando el elemento XMLSignature
          XMLSignature signature=sigFactory.newXMLSignature(signedInfo, keyInfo);
          
          // Firmando...
          signature.sign(dsc);
          
          //Generando el nuevo documento XML
          
          OutputStream os=new FileOutputStream(args[1]);
          Transformer trans=TransformerFactory.newInstance().newTransformer();
          trans.transform(new DOMSource(doc), new StreamResult(os));
        }
        catch(KeyStoreException ksEx)
        {
          System.err.println("Excepción con el repositorio: "+ksEx.getMessage());
        }
        catch(NoSuchAlgorithmException nsaEx)
        {
          System.err.println("Excepción al tratar de acceder al repositorio: "+nsaEx.getMessage());
        }
        catch(CertificateException cerEx)
        {
          System.err.println("Excepción con el certificado: "+cerEx.getMessage());
        }
        catch(UnrecoverableKeyException ukEx)
        {
          System.err.println("Excepción con la llave: "+ukEx.getMessage());
        }
        catch(InvalidAlgorithmParameterException iapEx)
        {
          System.out.println("Excepción al tratar de crear una referencia al documento: "+iapEx.getMessage());
        }
        catch(KeyException kex)
        {
          System.out.println("Excepción con la llave pública: "+kex.getMessage());
        }
        catch(MarshalException mEx)
        {
          System.err.println("Excepción al tratar de arreglar el nuevo elemento XML: "+mEx.getMessage());
        }
        catch(XMLSignatureException xsEx)
        {
          System.err.println("Excepción al firmar el elemento XML: "+xsEx.getMessage());
        }
        catch(TransformerConfigurationException tcEx)
        {
          System.err.println("Excepción al tratar de crear la fábrica para generar la salida XML: "+tcEx.getMessage());
        }
        catch(TransformerException tEx)
        {
          System.err.println("Excepción al tratar de generar el XML de salida: "+tEx.getMessage());
        }
            
            
            System.out.println("Terminó el proceso.");
      
        
      }
      catch(FileNotFoundException fnfex)
      {
        System.err.println("No se encontró el archivo de entrada: "+fnfex.getMessage());
      }
      catch(IOException ioex)
      {
        System.err.println("Excepción al tratar de leer el archivo de entrada: "+ioex.getMessage());
      }
      catch(ParserConfigurationException pcex)
      {
        System.out.println("Excepción al tratar de configurar el parser XML: "+pcex.getMessage());
      }
      catch(SAXException saxEx)
      {
        System.err.println("Excepción al tratar de leer el XML: "+saxEx.getMessage());
      }
      catch(IllegalAccessException iaEx)
      {
        System.err.println("Excepción al tratar de tener acceso al proveedor de seguidad: "+iaEx.getMessage());
      }
      catch(InstantiationException insEx)
      {
        System.err.println("Excepción al tratar de instanciar el proveedor de seguridad: "+insEx.getMessage());
      }
      catch(ClassNotFoundException cnfex)
      {
        System.out.println("No se encontró el proveedor de seguridad: "+cnfex.getMessage());
      }
  } // Fin main


  private static void usage()
  {
    System.out.println("Uso: java SignFile <archivoEntrada> <archivoSalida> [id|path|whole]");
  }
  
}
