/*
 * Copyright 2011 Federal Chancellery Austria and
 * Graz University of Technology
 * 
 * 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 at.gv.util.client.moaspss;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.net.ssl.SSLContext;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.namespace.QName;
import javax.xml.transform.Source;
import javax.xml.transform.TransformerException;
import javax.xml.transform.stream.StreamSource;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.soap.SOAPBinding;

import org.apache.cxf.configuration.jsse.TLSClientParameters;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.apache.xpath.XPathAPI;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import at.gv.util.DOMUtils;
import at.gv.util.LaxHostNameVerifier;
import at.gv.util.LoggingHandler;
import at.gv.util.MiscUtil;
import at.gv.util.config.EgovUtilConfiguration;
import at.gv.util.ex.EgovUtilException;
import at.gv.util.wsdl.SignatureCreationService;
import at.gv.util.wsdl.SignatureVerificationService;
import at.gv.util.xsd.moaspss.CreateXMLSignatureRequest;
import at.gv.util.xsd.moaspss.CreateXMLSignatureResponseType;
import at.gv.util.xsd.moaspss.ErrorResponseType;
import at.gv.util.xsd.moaspss.VerifyCMSSignatureResponseType;
import at.gv.util.xsd.moaspss.VerifyXMLSignatureRequestType;
import at.gv.util.xsd.moaspss.VerifyXMLSignatureResponseType;

/**
 * MOA-SS client.
 * 
 * @author <a href="mailto:Arne.Tauber@egiz.gv.at">Arne Tauber</a>
 * 
 */
public class MOASPSSClient {

	private static Logger log = LoggerFactory.getLogger(MOASPSSClient.class);

	private EgovUtilConfiguration config = null;

	public MOASPSSClient(EgovUtilConfiguration config) {
		if (config == null) {
			throw new NullPointerException("Argument 'config' must not be null.");
		}
		this.config = config;
	}

	public MOASPSSClient() {
	}

	@SuppressWarnings("unchecked")
	public Element sendSignatureCreationRequest(String serviceURL,
	    InputStream signatureCreationRequest) throws MOASPSSClientException {

		log.debug("Creating XML signature using raw CreateXMLSignatureRequest.");
		// check for arguments
		if (serviceURL == null) {
			throw new NullPointerException("Argument 'serviceURL' must not be null.");
		}
		if (signatureCreationRequest == null) {
			throw new NullPointerException(
			    "Argument 'signatureRequest' must not be null.");
		}

		try {

			log.trace("MOA-SS signature service URL: " + serviceURL);
			URL url = MOASPSSClient.class.getResource("/wsdl/MOA-SPSS-1.3.wsdl");
			SignatureCreationService service = new SignatureCreationService(
			    url,
			    new QName(
			        "http://reference.e-government.gv.at/namespace/moa/20020822#moa.wsdl",
			        "SignatureCreationService"));
			QName qname = new QName(
			    "http://localhost:8080/moa-spss/services/SignatureCreation",
			    "CreateXMLSignatureRequest");
			service.addPort(qname, SOAPBinding.SOAP11HTTP_BINDING, serviceURL);

			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			MiscUtil.copyStream(signatureCreationRequest, bos);

			Source source = new StreamSource(new ByteArrayInputStream(
			    bos.toByteArray()));
			Dispatch<Source> dispatch = service.createDispatch(qname, Source.class,
			    Service.Mode.PAYLOAD);

			BindingProvider bindingProvider = (BindingProvider) dispatch;
			log.trace("Adding JAX-WS request/response trace handler.");
			List<Handler> handlerList = bindingProvider.getBinding()
			    .getHandlerChain();
			if (handlerList == null) {
				handlerList = new ArrayList();
			}
			LoggingHandler loggingHandler = new LoggingHandler();
			handlerList.add(loggingHandler);
			bindingProvider.getBinding().setHandlerChain(handlerList);

			// initialize ssl
			Map<String, Object> requestContext = bindingProvider.getRequestContext();

			if (serviceURL.toLowerCase().startsWith("https")) {
				log.trace("Using ssl for MOA-SP request.");
				if (this.config == null) {
					throw new MOASPSSClientException(
					    "SSL requires client to be configured.");
				}
				SSLContext sslContext = this.config.getMOASPSSsslConfiguration()
				    .getSSLContext(false);
				if (sslContext == null) {
					throw new MOASPSSClientException(
					    "SSL context from configuration is empty. Please configure an SSL context in the configuration first.");
				}
				Client client = ClientProxy.getClient(dispatch);
				HTTPConduit http = (HTTPConduit) client.getConduit();
				HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();
				 
				httpClientPolicy.setConnectionTimeout(36000);
				httpClientPolicy.setAllowChunking(false);
				httpClientPolicy.setReceiveTimeout(32000);
				 
				http.setClient(httpClientPolicy);
				
				TLSClientParameters tlsParams = new TLSClientParameters();			
				tlsParams.setSSLSocketFactory(sslContext.getSocketFactory());
							
				// check for lax hostname
				if (this.config.getMOASPSSsslConfiguration().useLaxHostNameVerifier()) {
					log.trace("LaxHostnameVerifier enabled. This setting is not recommended to use.");
					tlsParams.setHostnameVerifier(new LaxHostNameVerifier());
				}
				
				http.setTlsClientParameters(tlsParams );
			}

			log.trace("Invoking MOA-SS signature creation service.");
			Source response = dispatch.invoke(source);

			log.trace("Parsing MOA-SS response.");
			byte[] moaResponse = MiscUtil.sourceToByteArray(response);
			JAXBContext ctx = JAXBContext.newInstance(CreateXMLSignatureRequest.class
			    .getPackage().getName());
			JAXBElement<CreateXMLSignatureResponseType> jaxbElement = (JAXBElement<CreateXMLSignatureResponseType>) ctx
			    .createUnmarshaller()
			    .unmarshal(new ByteArrayInputStream(moaResponse));
			CreateXMLSignatureResponseType createXMLResponse = jaxbElement.getValue();
			for (Object obj : createXMLResponse
			    .getSignatureEnvironmentOrErrorResponse()) {
				if (obj instanceof ErrorResponseType) {
					ErrorResponseType errorResponse = (ErrorResponseType) obj;
					log.trace("Could not create signature: "
					    + errorResponse.getErrorCode() + "/" + errorResponse.getInfo());
					throw new MOASPSSClientException("MOA-SS signature error: "
					    + errorResponse.getErrorCode() + "/" + errorResponse.getInfo());
				}
			}
			log.trace(new String(moaResponse));
			log.trace("Signature successfully created. Extracting from MOA-SS container.");
			// ok, we have success
			Document doc = MiscUtil.parseDocument(new ByteArrayInputStream(
			    moaResponse));
			String xpathExpression = "/moa:CreateXMLSignatureResponse/moa:SignatureEnvironment/child::*";
			Element nsNode = doc.createElement("NsNode");
			nsNode.setAttribute("xmlns:moa", doc.getDocumentElement()
			    .getNamespaceURI());
			log.trace("Selecting signed doc " + xpathExpression);
			Element documentNode = (Element) XPathAPI.selectSingleNode(doc,
			    xpathExpression, nsNode);
			log.trace("Signed document: " + DOMUtils.serializeNode(documentNode));

			return documentNode;
		} catch (TransformerException e) {
			throw new MOASPSSClientException(e);
		} catch (IOException e) {
			throw new MOASPSSClientException(e);
		} catch (JAXBException e) {
			throw new MOASPSSClientException(e);
		} catch (EgovUtilException e) {
			throw new MOASPSSClientException(e);
		}
	}

	public VerifyCMSSignatureResponseType sendCMSSignatureVerificationRequest(
			String serviceURL, InputStream signatureVerificationRequest)
	    throws MOASPSSClientException {
		log.debug("Verifying CMS signature using raw VerifyXMLSignatureRequest.");

		try {			
			byte[] moaResponse = sendBasicSignatureVerificationRequest(serviceURL, signatureVerificationRequest);
			
			JAXBContext ctx = JAXBContext
			    .newInstance(VerifyCMSSignatureResponseType.class.getPackage()
			        .getName());
			JAXBElement<VerifyCMSSignatureResponseType> jaxbElement = (JAXBElement<VerifyCMSSignatureResponseType>) ctx
			    .createUnmarshaller()
			    .unmarshal(new ByteArrayInputStream(moaResponse));
			VerifyCMSSignatureResponseType verifyXMLResponse = jaxbElement.getValue();
			log.trace(new String(moaResponse));
			return verifyXMLResponse;
			
		} catch (JAXBException e) {
			throw new MOASPSSClientException(e);
			
		}
		
		
	}
	
	
	@SuppressWarnings("unchecked")
	public VerifyXMLSignatureResponseType sendSignatureVerificationRequest(
	    String serviceURL, InputStream signatureVerificationRequest)
	    throws MOASPSSClientException {
		log.debug("Verifying XML signature using raw VerifyXMLSignatureRequest.");

		try {			
			byte[] moaResponse = sendBasicSignatureVerificationRequest(serviceURL, signatureVerificationRequest);
			
			JAXBContext ctx = JAXBContext
			    .newInstance(VerifyXMLSignatureRequestType.class.getPackage()
			        .getName());
			JAXBElement<VerifyXMLSignatureResponseType> jaxbElement = (JAXBElement<VerifyXMLSignatureResponseType>) ctx
			    .createUnmarshaller()
			    .unmarshal(new ByteArrayInputStream(moaResponse));
			VerifyXMLSignatureResponseType verifyXMLResponse = jaxbElement.getValue();
			log.trace(new String(moaResponse));
			return verifyXMLResponse;
			
		} catch (JAXBException e) {
			throw new MOASPSSClientException(e);
			
		}

	}

	private byte[] sendBasicSignatureVerificationRequest(String serviceURL, InputStream signatureVerificationRequest)
		    throws MOASPSSClientException {
		// check for arguments
		if (serviceURL == null) {
			throw new NullPointerException("Argument 'serviceURL' must not be null.");
		}
		if (signatureVerificationRequest == null) {
			throw new NullPointerException(
			    "Argument 'signatureRequest' must not be null.");
		}

		try {
			log.trace("MOA-SP verification service URL: " + serviceURL);
			URL url = MOASPSSClient.class.getResource("/wsdl/MOA-SPSS-1.3.wsdl");
			SignatureVerificationService service = new SignatureVerificationService(
			    url,
			    new QName(
			        "http://reference.e-government.gv.at/namespace/moa/20020822#moa.wsdl",
			        "SignatureCreationService"));
			QName qname = new QName(
			    "http://localhost:8080/moa-spss/services/SignatureVerification",
			    "VerifyXMLSignatureRequest");
			
			service.addPort(qname, SOAPBinding.SOAP11HTTP_BINDING, serviceURL);

			ByteArrayOutputStream bos = new ByteArrayOutputStream();
			MiscUtil.copyStream(signatureVerificationRequest, bos);

			Source source = new StreamSource(new ByteArrayInputStream(
			    bos.toByteArray()));
			Dispatch<Source> dispatch = service.createDispatch(qname, Source.class,
			    Service.Mode.PAYLOAD);

			BindingProvider bindingProvider = (BindingProvider) dispatch;
			log.trace("Adding JAX-WS request/response trace handler.");
			List<Handler> handlerList = bindingProvider.getBinding()
			    .getHandlerChain();
			if (handlerList == null) {
				handlerList = new ArrayList();
			}
			LoggingHandler loggingHandler = new LoggingHandler();
			handlerList.add(loggingHandler);
			bindingProvider.getBinding().setHandlerChain(handlerList);

			// initialize ssl
			Map<String, Object> requestContext = bindingProvider.getRequestContext();

			if (serviceURL.toLowerCase().startsWith("https")) {
				log.trace("Using ssl for MOA-SP request.");
				if (this.config == null) {
					throw new MOASPSSClientException(
					    "SSL requires client to be configured.");
				}
				SSLContext sslContext = this.config.getMOASPSSsslConfiguration()
				    .getSSLContext(false);
				if (sslContext == null) {
					throw new MOASPSSClientException(
					    "SSL context from configuration is empty. Please configure an SSL context in the configuration first.");
				}
				Client client = ClientProxy.getClient(dispatch);
				HTTPConduit http = (HTTPConduit) client.getConduit();
				HTTPClientPolicy httpClientPolicy = new HTTPClientPolicy();
				 
				httpClientPolicy.setConnectionTimeout(36000);
				httpClientPolicy.setAllowChunking(false);
				httpClientPolicy.setReceiveTimeout(32000);
				 
				http.setClient(httpClientPolicy);
				
				TLSClientParameters tlsParams = new TLSClientParameters();			
				tlsParams.setSSLSocketFactory(sslContext.getSocketFactory());
							
				// check for lax hostname
				if (this.config.getMOASPSSsslConfiguration().useLaxHostNameVerifier()) {
					log.trace("LaxHostnameVerifier enabled. This setting is not recommended to use.");
					tlsParams.setHostnameVerifier(new LaxHostNameVerifier());
				}
				
				http.setTlsClientParameters(tlsParams );
			}

			log.trace("Invoking MOA-SP signature verification service.");
			Source response = dispatch.invoke(source);

			log.trace("Parsing MOA-SP response.");
			byte[] moaResponse = MiscUtil.sourceToByteArray(response);
			
			return moaResponse;
			
		} catch (TransformerException e) {
			throw new MOASPSSClientException(e);
		} catch (IOException e) {
			throw new MOASPSSClientException(e);
		} catch (EgovUtilException e) {
			throw new MOASPSSClientException(e);
		}
	}
	
	public static boolean isSuccess(VerifyXMLSignatureResponseType verifyResult) {
		if (verifyResult == null) {
			throw new NullPointerException(
			    "Argument 'verifyResult' must not be null.");
		}
		log.trace("Checking for signature verification result.");

		int signatureCheckCode = verifyResult.getSignatureCheck().getCode()
		    .intValue();
		int signtaureManifestCheckCode = verifyResult.getSignatureManifestCheck()
		    .getCode().intValue();
		int certificateCheckCode = verifyResult.getCertificateCheck().getCode()
		    .intValue();

		log.trace("Signature check code: " + signatureCheckCode);
		log.trace("Signature manifest check code: " + signtaureManifestCheckCode);
		log.trace("Certificate check code: " + certificateCheckCode);

		return signatureCheckCode == 0 && signtaureManifestCheckCode == 0
		    && certificateCheckCode == 0;
	}

}