/*
 * Copyright  1999-2004 The Apache Software Foundation.
 *
 *  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 org.apache.xml.security.keys.keyresolver.implementations;



import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.PublicKey;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Set;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.xml.security.c14n.CanonicalizationException;
import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.keys.content.RetrievalMethod;
import org.apache.xml.security.keys.content.x509.XMLX509Certificate;
import org.apache.xml.security.keys.keyresolver.KeyResolver;
import org.apache.xml.security.keys.keyresolver.KeyResolverException;
import org.apache.xml.security.keys.keyresolver.KeyResolverSpi;
import org.apache.xml.security.keys.storage.StorageResolver;
import org.apache.xml.security.signature.XMLSignatureInput;
import org.apache.xml.security.transforms.Transforms;
import org.apache.xml.security.utils.Constants;
import org.apache.xml.security.utils.XMLUtils;
import org.apache.xml.security.utils.resolver.ResourceResolver;
import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.SAXException;


/**
 * The RetrievalMethodResolver can retrieve public keys and certificates from
 * other locations. The location is specified using the ds:RetrievalMethod
 * element which points to the location. This includes the handling of raw
 * (binary) X.509 certificate which are not encapsulated in an XML structure.
 * If the retrieval process encounters an element which the
 * RetrievalMethodResolver cannot handle itself, resolving of the extracted
 * element is delegated back to the KeyResolver mechanism.
 *
 * @author $Author: raul $ modified by Dave Garcia
 */
public class RetrievalMethodResolver extends KeyResolverSpi {

   /** {@link org.apache.commons.logging} logging facility */
    static org.apache.commons.logging.Log log = 
        org.apache.commons.logging.LogFactory.getLog(
                        RetrievalMethodResolver.class.getName());

   /**
    * Method engineCanResolve
    * @inheritDoc
    * @param element
    * @param BaseURI
    * @param storage
    *
    */
   public boolean engineCanResolve(Element element, String BaseURI,
                                   StorageResolver storage) {

      if (!XMLUtils.elementIsInSignatureSpace(element,Constants._TAG_RETRIEVALMETHOD)) {      
         return false;
      }

      return true;
   }

   /**
    * Method engineResolvePublicKey
    * @inheritDoc
    * @param element
    * @param BaseURI
    * @param storage
    *
    */
   public PublicKey engineResolvePublicKey(
           Element element, String BaseURI, StorageResolver storage)
              {

      try {
		   //Create a retrieval method over the given element
		   RetrievalMethod rm = new RetrievalMethod(element, BaseURI);
	       // type can be null because it's optional
	       String type = rm.getType();
		   
		   XMLSignatureInput resource=this.resolveInput(rm,BaseURI);
			
            //a raw certificate, direct parsing is done!
            if ((type != null) && type.equals(RetrievalMethod.TYPE_RAWX509)) {
				X509Certificate cert=this.getRawCertificate(resource);
				if (cert != null) {
			         return cert.getPublicKey();
			    }
            } 
			
			else {
				//If the retrieved element is a single node
				Element e;
				if (resource.isElement()){
					e=(Element) resource.getSubNode();
					return this.resolveKey(e,BaseURI,storage);
				}

				else{
					//Retrieved resource is a nodeSet
					if (resource.isNodeSet()){
						  Set <Node>nodes=resource.getNodeSet();
						  PublicKey key;
						  for(Node currentNode:nodes){
							  //Only retrieved nodes that are elements can be used to resolve a key
							  if (!(currentNode instanceof Element)) continue;
							  
							  key=this.resolveKey((Element) currentNode,BaseURI,storage);
							  
							  if (key!=null) return key;
						  }
						  
						  return null;
					}
					//Retrieved resource is an inputStream
					else{
						byte inputBytes[] = resource.getBytes();
						e = this.getDocFromBytes(inputBytes);
						
						//otherwise, we parse the resource, create an Element and delegate
		                if (log.isDebugEnabled()) log.debug("we have to parse " + inputBytes.length + " bytes");
						return this.resolveKey(e,BaseURI,storage);
					}
				}

            }
      } catch (XMLSecurityException ex) {
         log.debug("XMLSecurityException", ex);
      } catch (CertificateException ex) {
         log.debug("CertificateException", ex);
      } catch (IOException ex) {
         log.debug("IOException", ex);
      } catch (ParserConfigurationException e) {
		  log.debug("ParserConfigurationException", e);
	  } catch (SAXException e) {
		 log.debug("SAXException", e);
	  } 

      return null;
   }

   /**
    * Method engineResolveX509Certificate
    * @inheritDoc
    * @param element
    * @param BaseURI
    * @param storage
    *
    */
   public X509Certificate engineResolveX509Certificate(
           Element element, String BaseURI, StorageResolver storage)
              {

      try {
         RetrievalMethod rm = new RetrievalMethod(element, BaseURI);
		 // type can be null because it's optional
	     String type = rm.getType();
		   
		 XMLSignatureInput resource=this.resolveInput(rm,BaseURI);
			
         
            if ((rm.getType() != null)&& rm.getType().equals(RetrievalMethod.TYPE_RAWX509)) {

			   X509Certificate cert=this.getRawCertificate(resource);

               if (cert != null) {
                  return cert;
               }
            } else {

               
				Element e;
				if (resource.isElement()){
					e=(Element) resource.getSubNode();
					return this.resolveCertificate(e,BaseURI,storage);
				}

				else{
					//Retrieved resource is a nodeSet
					if (resource.isNodeSet()){
						  Set <Node>nodes=resource.getNodeSet();
						  X509Certificate cert;
						  for(Node currentNode:nodes){
							  if (!(currentNode instanceof Element)) continue;
							  
							  cert=this.resolveCertificate((Element) currentNode,BaseURI,storage);
							  
							  if (cert!=null) return cert;
						  }
						  
						  return null;
					}
					//Retrieved resource is an inputStream
					else{
						byte inputBytes[] = resource.getBytes();
						e = this.getDocFromBytes(inputBytes);
//						 otherwise, we parse the resource, create an Element and delegate
		                if (log.isDebugEnabled()) log.debug("we have to parse " + inputBytes.length + " bytes");
						return this.resolveCertificate(e,BaseURI,storage);
					}
				}


            }
      } catch (XMLSecurityException ex) {
         log.debug("XMLSecurityException", ex);
      } catch (CertificateException ex) {
         log.debug("CertificateException", ex);
      } catch (IOException ex) {
         log.debug("IOException", ex);
	  } catch (ParserConfigurationException e) {
		  log.debug("ParserConfigurationException", e);
	  } catch (SAXException e) {
		 log.debug("SAXException", e);
	  } 

      return null;
   }
   
   /**
    * Retrieves a x509Certificate from the given information
    * @param e
    * @param BaseURI
    * @param storage
    * @return
    * @throws KeyResolverException 
    */
   private X509Certificate resolveCertificate(Element e,String BaseURI,StorageResolver storage) throws KeyResolverException{
		  if (log.isDebugEnabled()) log.debug("Now we have a {" + e.getNamespaceURI() + "}"+ e.getLocalName() + " Element");
		  //An element has been provided
       	  if (e != null) { 
			  KeyResolver newKeyResolver = KeyResolver.getInstance(e,BaseURI, storage);
		  			if (newKeyResolver != null) { 
						return newKeyResolver.resolveX509Certificate(e, BaseURI,storage);
					}
   		  }
		  return null;
   } 
   
   /**
    * Retrieves a x509Certificate from the given information
    * @param e
    * @param BaseURI
    * @param storage
    * @return
    * @throws KeyResolverException 
    */
   private PublicKey resolveKey(Element e,String BaseURI,StorageResolver storage) throws KeyResolverException{
		  if (log.isDebugEnabled()) log.debug("Now we have a {" + e.getNamespaceURI() + "}"+ e.getLocalName() + " Element");
		  //An element has been provided
       	  if (e != null) { 
			  KeyResolver newKeyResolver = KeyResolver.getInstance(e,BaseURI, storage);
		  			if (newKeyResolver != null) { 
						return newKeyResolver.resolvePublicKey(e, BaseURI,storage);
					}
   		  }
		  return null;
   }

   private X509Certificate getRawCertificate(XMLSignatureInput resource) throws CanonicalizationException, IOException, CertificateException{
	   byte inputBytes[] = resource.getBytes();
	   
       // if the resource stores a raw certificate, we have to handle it
       CertificateFactory certFact =CertificateFactory.getInstance(XMLX509Certificate.JCA_CERT_ID);
       X509Certificate cert =(X509Certificate) certFact.generateCertificate(new ByteArrayInputStream(inputBytes));

       return cert;
   }
   /**
    * Resolves the input from the given retrieval method 
    * @return
    * @throws XMLSecurityException 
    */
   private XMLSignatureInput resolveInput(RetrievalMethod rm,String BaseURI) throws XMLSecurityException{
	   
	   
       Attr uri = rm.getURIAttr();

	   //Apply the trnasforms
       Transforms transforms = rm.getTransforms();
       ResourceResolver resRes = ResourceResolver.getInstance(uri, BaseURI);

       if (resRes != null) {
          XMLSignatureInput resource = resRes.resolve(uri, BaseURI);

          if (transforms != null) {log.debug("We have Transforms");
				resource = transforms.performTransforms(resource);
          }
		  
		  return resource;
       }
	   return null;
   }
   
   /**
    * Parses a byte array and returns the parsed Element.
    *
    * @param bytes
    * @return the Document Element after parsing bytes 
    * @throws KeyResolverException if something goes wrong
    */
   Element getDocFromBytes(byte[] bytes) throws KeyResolverException {

      try {
         javax.xml.parsers.DocumentBuilderFactory dbf =javax.xml.parsers.DocumentBuilderFactory.newInstance();

         dbf.setNamespaceAware(true);

         javax.xml.parsers.DocumentBuilder db = dbf.newDocumentBuilder();
         org.w3c.dom.Document doc =
            db.parse(new java.io.ByteArrayInputStream(bytes));

         return doc.getDocumentElement();
      } catch (org.xml.sax.SAXException ex) {
         throw new KeyResolverException("empty", ex);
      } catch (java.io.IOException ex) {
         throw new KeyResolverException("empty", ex);
      } catch (javax.xml.parsers.ParserConfigurationException ex) {
         throw new KeyResolverException("empty", ex);
      }
   }

   /**
    * Method engineResolveSecretKey
    * @inheritDoc
    * @param element
    * @param BaseURI
    * @param storage
    *
    */
   public javax.crypto.SecretKey engineResolveSecretKey(
           Element element, String BaseURI, StorageResolver storage)
   {
      return null;
   }
   static Element getFirstElementChild(Element e){
   	    Node n=e.getFirstChild();
   	    while (n!=null && n.getNodeType()!=Node.ELEMENT_NODE) {
   	    	n=n.getNextSibling();
   	    }
   		return (Element)n;
   }
}
