/*
 * Created by Ray Plante for the National Virtual Observatory
 * c. 2006
 */
package net.ivoa.registry.search;

import net.ivoa.registry.RegistryServiceException;

import java.net.URL;

import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPElement;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPConnection;
import javax.xml.soap.SOAPConnectionFactory;
import javax.xml.soap.Name;
import javax.xml.soap.MimeHeaders;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.DOMException;

/**
 * A client for the IVOA Registry Search Interface 
 *
 */
public class SOAPSearchClient {

    protected URL endpoint = null;
    protected MessageFactory soapfactory = null;
    protected SOAPConnection conn = null;

    public static final String GETIDENTIFY_ACTION = 
        "http://www.ivoa.net/wsdl/RegistrySearch/v1.0#GetIdentity";

    public static final String GETRESOURCE_ACTION = 
        "http://www.ivoa.net/wsdl/RegistrySearch/v1.0#GetResource";

    public static final String SEARCH_ACTION = 
        "http://www.ivoa.net/wsdl/RegistrySearch/v1.0#Search";

    public static final String KEYWORDSEARCH_ACTION = 
        "http://www.ivoa.net/wsdl/RegistrySearch/v1.0#KeywordSearch";

    public static final String XQUERYSEARCH_ACTION = 
        "http://www.ivoa.net/wsdl/RegistrySearch/v1.0#XQuerySearch";

    public static final String GENERAL_FAULT = "ErrorResponse";

    public static final String NOT_FOUND_FAULT = "NotFound";

    public static final String UNSUPPORTED_OPERATION_FAULT = 
        "UnsupportedOperation";

    public static final String WSDL_NS = 
        "http://www.ivoa.net/wsdl/RegistrySearch/v1.0";

    public static final String WSDL_PREFIX = "rs";


    protected SOAPSearchClient() {
        try {
            soapfactory = MessageFactory.newInstance();
            conn = SOAPConnectionFactory.newInstance().createConnection();
        }
        catch (SOAPException ex) {
            throw new InternalError("installation/config error: " + 
                                    ex.getMessage());
        }
    }

    /**
     * Create a client configured to connect to a given registry
     */
    public SOAPSearchClient(URL endpointURL) { 
        this();
        endpoint = endpointURL; 
    }

    /**
     * submit a keyword search
     * @param keywords   space-delimited words to search on
     * @param orThem     if true, return results that contain any of the 
     *                      keywords
     * @param from       the position of the first match to return
     * @param max        the maximum number of matches to return.
     * @param identifiersOnly  if true, return only identifiers; otherwise,
     *                   return the entire VOResource record for each match.
     * @exception RegistryServiceException  if the service encounters an error 
     *                           (i.e. on the server side).
     * @exception SOAPException if an error is encountered while creating or
     *                           submitting the SOAP request or while processing
     *                           the SOAP response.  
     */
    public Element keywordSearch(String keywords, boolean orThem, 
                                 int from, int max, 
                                 boolean identifiersOnly) 
         throws RegistryServiceException, SOAPException
    {
        SOAPMessage msg = makeSOAPMessage();

        SOAPBody body = msg.getSOAPBody();
        SOAPElement sel = body.addBodyElement(makeRSName(msg, "KeywordSearch"));
        SOAPElement arg = sel.addChildElement(makeRSName(msg, "keywords"));
        arg.addTextNode(keywords.trim());
        arg = sel.addChildElement(makeRSName(msg, "from"));
        arg.addTextNode(Integer.toString(from));
        arg = sel.addChildElement(makeRSName(msg, "max"));
        arg.addTextNode(Integer.toString(from));
        arg = sel.addChildElement(makeRSName(msg, "identifiersOnly"));
        arg.addTextNode(Boolean.toString(identifiersOnly));

        Element res = call(msg, KEYWORDSEARCH_ACTION);
        return res;
    }

    /**
     * submit a constraint-based search
     * @param adqlWhere  the search constraints in the form of a ADQL Where 
     *                      clause.  The element's name should be "Where", and
     *                      its contents should comply with the ADQL schema's
     *                      "WhereType".  
     * @param from       the position of the first match to return
     * @param max        the maximum number of matches to return.
     * @param identifiersOnly  if true, return only identifiers; otherwise,
     *                   return the entire VOResource record for each match.
     * @exception DOMException if the adqlWhere Element object does not allow
     *                           itself to be imported or otherwise its 
     *                           implementation is defective.
     * @exception RegistryServiceException  if the service encounters an error 
     *                           (i.e. on the server side).
     * @exception SOAPException if an error is encountered while creating or
     *                           submitting the SOAP request or while processing
     *                           the SOAP response.  
     */
    public Element search(Element adqlWhere, int from, int max, 
                          boolean identifiersOnly) 
         throws RegistryServiceException, SOAPException, DOMException
    {
        SearchQuery query = new SearchQuery();
        Element wparent = query.getWhereParent();

        wparent.appendChild(wparent.getOwnerDocument().importNode(adqlWhere,
                                                                  true));
        return search(query);
    }

    /**
     * return a SearchQuery object that can be used to attach an ADQL 
     * query to.
     * @exception SOAPException if an error is encountered while creating 
     *                           the SOAP request.  
     */
    public SearchQuery createSearchQuery() throws SOAPException {
        return new SearchQuery(); 
    }

    /**
     * submit a constraint-based search as a SearchQuery object.  Submitting
     * as a SearchQuery object is slightly more efficient(?) way to submit
     * as it provides a way to attach an ADQL query that doesn't ultimately 
     * require a cloning of the where element.
     * @param query   the search constraints as a SearchQuery object.
     * @exception RegistryServiceException  if the service encounters an error 
     *                           (i.e. on the server side).
     * @exception SOAPException if an error is encountered while creating or
     *                           submitting the SOAP request or while processing
     *                           the SOAP response.  
     */
    public Element search(SearchQuery query) 
         throws RegistryServiceException, SOAPException
    {
        return call(query.getSearchSOAPMessage(), SEARCH_ACTION);
    }

    /**
     * return the Registry description
     * @param query   the search constraints as a SearchQuery object.
     * @exception RegistryServiceException  if the service encounters an error 
     *                           (i.e. on the server side).
     * @exception SOAPException if an error is encountered while creating or
     *                           submitting the SOAP request or while processing
     *                           the SOAP response.  
     */
    public Element getIdentity() 
         throws RegistryServiceException, SOAPException
    {
        SOAPMessage msg = makeSOAPMessage();

        SOAPBody body = msg.getSOAPBody();
        SOAPElement sel = body.addBodyElement(makeRSName(msg, "GetIdentity"));
        return call(msg, GETIDENTIFY_ACTION);
    }

    /**
     * return the Resource description for a given identifier
     * @param ivoid   the IVOA Identifier to resolve
     * @param query   the search constraints as a SearchQuery object.
     * @exception IDNotFoundException  if the service cannot match the given
     *                           ID to a description
     * @exception RegistryServiceException  if the service encounters an error 
     *                           (i.e. on the server side).
     * @exception SOAPException if an error is encountered while creating or
     *                           submitting the SOAP request or while processing
     *                           the SOAP response.  
     */
    public Element getResource(String ivoid) 
         throws RegistryServiceException, IDNotFoundException, SOAPException
    {
        SOAPMessage msg = makeSOAPMessage();

        SOAPBody body = msg.getSOAPBody();
        SOAPElement sel = body.addBodyElement(makeRSName(msg, "GetResource"));
        SOAPElement id = sel.addChildElement(makeRSName(msg, "identifier"));
        id.addTextNode(ivoid.trim());

        Element res = call(msg, GETRESOURCE_ACTION);
        if (res.getLocalName().equals(NOT_FOUND_FAULT)) 
            throw new IDNotFoundException(getMessage(res));
        return res;
    }

    protected Name makeRSName(SOAPMessage msg, String elname) 
         throws SOAPException
    {
        return msg.getSOAPPart().getEnvelope().createName(elname, WSDL_PREFIX,
                                                          WSDL_NS);
    }

    /**
     * return the result of an XQuery search
     * @exception UnsupportedOperationException  if the service does not support
     *                          an XQuery-based search
     * @exception RegistryServiceException  if the service encounters an error 
     *                           (i.e. on the server side).
     * @exception SOAPException if an error is encountered while creating or
     *                           submitting the SOAP request or while processing
     *                           the SOAP response.  
     */
    public Element xquerySearch(String xquery) 
         throws RegistryServiceException, UnsupportedOperationException, 
                SOAPException
    {
        SOAPMessage msg = makeSOAPMessage();
        SOAPEnvelope env = msg.getSOAPPart().getEnvelope();

        SOAPBody body = msg.getSOAPBody();
        SOAPElement sel = body.addBodyElement(makeRSName(msg, "XQuerySearch"));
        SOAPElement id = sel.addChildElement(makeRSName(msg, "xquery"));
        id.addTextNode(xquery.trim());

        Element res = call(msg, XQUERYSEARCH_ACTION);
        if (res.getLocalName().equals(UNSUPPORTED_OPERATION_FAULT)) 
            throw new UnsupportedOperationException(getMessage(res));
        return res;
    }
     
    /**
     * create an empty SOAP message.  This can provide a DOM Document with which
     * an element containing an ADQL query can be created and inserted directly.
     * @exception SOAPException if an error is encountered while creating 
     *                           the SOAP request.  
     */
    public SOAPMessage makeSOAPMessage() throws SOAPException {
        return soapfactory.createMessage();
    }

    /**
     * submit the soap message
     * @exception RegistryServiceException  if the service encounters an error 
     *                           (i.e. on the server side).
     * @exception SOAPException if an error is encountered while submitting 
     *                           the SOAP request or while processing
     *                           the SOAP response.  
     */
    protected Element call(SOAPMessage msg, String actionURI) 
         throws RegistryServiceException, SOAPException
    {
        MimeHeaders mh = msg.getMimeHeaders();
        mh.setHeader("SOAPAction", actionURI);
        SOAPMessage resp = conn.call(msg, endpoint);

        if (resp == null) 
            throw new RegistryServiceException("No response from registry");

        Node out = resp.getSOAPBody().getFirstChild();
        while (out != null && out.getNodeType() != Node.ELEMENT_NODE) {
            out = out.getNextSibling();
        }

        if (out == null) 
            throw new RegistryServiceException("Empty SOAP Envelope!");

        if (out.getLocalName().equals(GENERAL_FAULT)) {
            throw new RegistryServiceException(getMessage((Element) out));
        }

        return ((Element) out);
    }

    protected String getMessage(Element rifault) throws DOMException {
        StringBuffer sb = new StringBuffer();
        NodeList nl = rifault.getElementsByTagName("errorMessage");
        Node msgel = null;
        for(int i=0; i < nl.getLength(); i++) {
            msgel = nl.item(i);
        }
        if (msgel != null) {
            Node msg = msgel.getFirstChild();
            while (msg != null) {
                if (msg.getNodeType() == Node.TEXT_NODE) 
                    sb.append(msg.getNodeValue());
                msg = msg.getNextSibling();
            }
        }
        if (sb.length() == 0) sb.append("Unknown error");
        return sb.toString().trim();
    }

    /**
     * an updatable search query
     */
    public class SearchQuery {

        SOAPMessage msg = null;
        int from=0, max=-1;
        boolean identifiersOnly = false;
        SOAPElement whereParent = null;

        SearchQuery() throws SOAPException {
            msg = makeSOAPMessage();
            SOAPBody body = msg.getSOAPBody();
            SOAPEnvelope env = msg.getSOAPPart().getEnvelope();
            whereParent = body.addBodyElement(makeRSName(msg, "Where"));
            whereParent.setAttribute("xmlns", WSDL_NS);
        }

        /**
         * return a SOAP Message that is ready for submission
         */
        public SOAPMessage getSearchSOAPMessage() throws SOAPException {
            SOAPElement child = null;
            SOAPEnvelope env = msg.getSOAPPart().getEnvelope();

            // check that we have an ADQL
//             if (! whereParent.getChildElements("Where").hasNext())
//                 throw new IllegalStateException("Missing ADQL Where clause");

            child = whereParent.addChildElement(makeRSName(msg, "from"));
            child.addTextNode(Integer.toString(from));
            child = whereParent.addChildElement(makeRSName(msg, "max"));
            child.addTextNode(Integer.toString(max));
            child = whereParent.addChildElement(
                                    makeRSName(msg, "identifiersOnly"));
            child.addTextNode(Boolean.toString(identifiersOnly));

            return msg;
        }

        /**
         * return a parent element that the Where clause can be appended to.
         */
        public Element getWhereParent() {
            whereParent.removeContents();
            return whereParent;
        }

        /**
         * return the position of the first record to return
         */
        public int getFrom() { return from; } 

        /**
         * set the position of the first record to return
         */
        public void setFrom(int pos) { from = pos; }

        /**
         * return the maximum number of records to return
         */
        public int getMax() { return max; }

        /**
         * set the maximum number of records to return
         */
        public void setMax(int count) { max = count; }

        /**
         * return whether idenitifiers only should be returned
         */
        public boolean isIdentifiersOnly() { return identifiersOnly; }

        /**
         * set whether idenitifiers only should be returned.  The 
         * default value is false. 
         */
        public void setIdentifiersOnly(boolean yes) { identifiersOnly = yes; }

    }

    class Caller implements ServiceCaller {
        URL ep = endpoint;

        Caller() {}

        public void setEndpoint(URL endpoint) { ep = endpoint; }
        public URL getEndpoint() { return ep; }

        public Element call(SOAPMessage msg, String action) {
            MimeHeaders mh = msg.getMimeHeaders();
            mh.setHeader("SOAPAction", actionURI);
            SOAPMessage resp = conn.call(msg, ep);

            if (resp == null) 
                throw new RegistryServiceException("No response from registry");

            Node out = resp.getSOAPBody().getFirstChild();
            while (out != null && out.getNodeType() != Node.ELEMENT_NODE) {
                out = out.getNextSibling();
            }

            if (out == null) 
                throw new RegistryServiceException("Empty SOAP Envelope!");

            if (out.getLocalName().equals(GENERAL_FAULT)) {
                throw new RegistryServiceException(getMessage((Element) out));
            }

            return ((Element) out);
            
        }
    }
}
