// BaseClient.java
//
//    Created: August 2005
//
// This file is a component of the BioMoby project.
// Copyright Martin Senger (martin.senger@gmail.com).
//

package org.biomoby.client;

import org.biomoby.shared.MobyException;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.parser.JDOMUtils;
import org.biomoby.shared.Central;
import org.biomoby.client.CentralImpl;
import org.biomoby.shared.parser.MobyPackage;
import org.biomoby.shared.parser.MobyJob;
import org.biomoby.shared.parser.ServiceException;
import org.biomoby.shared.parser.MobyTags;

import org.tulsoft.tools.soap.axis.AxisCall;
import org.tulsoft.shared.GException;

import org.apache.axis.client.Call;
import org.apache.commons.lang.StringUtils;

import org.jdom.input.SAXBuilder;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.Namespace;

import java.net.URL;
import java.net.MalformedURLException;
import java.io.StringReader;

/**
 * This is a base class for Biomoby clients. It takes care about
 * converting user input into Biomoby XML, sending it in a SOAP
 * message to a Biomoby service, waiting for the response and parsing
 * it from Biomoby XML. It also divides requests and responses into
 * so-called <em>jobs</em> - each of them corresponds to a Biomoby
 * query (a Biomoby single network request may contain more
 * queries/jobs). <p>
 *
 * Any client can override various methods - but the ones she/he
 * <em>must</em> override in a subclass are those telling what service
 * to call ({@link #getServiceLocator}), what data to put in a request
 * ({@link #fillRequest(MobyJob,MobyPackage) fillRequest}), and what
 * to do with a response ({@link #useResponse(MobyJob,MobyPackage)
 * useResponse}. <p>
 *
 * @author <A HREF="mailto:martin.senger@gmail.com">Martin Senger</A>
 * @version $Id: BaseClient.java,v 1.14 2008/12/03 15:21:13 groscurt Exp $
 */

abstract public class BaseClient {

    private static org.apache.commons.logging.Log log =
       org.apache.commons.logging.LogFactory.getLog (BaseClient.class);

    /**************************************************************************
     *
     *************************************************************************/
    static protected boolean notEmpty (String value) {
	return StringUtils.isNotBlank (value);
    }
    static protected boolean isEmpty (String value) {
	return StringUtils.isBlank (value);
    }

    /**************************************************************************
     * The main method that packs input data, invokes a BioMoby
     * service and uses its response. Use this method if the input
     * data should have just one job (which is a usual case) -
     * otherwise use method {@link #process(int)}. <p>
     *
     * @throws MobyException if (a) a sub-class throws it during the
     * filling data or using response, or (b) a Biomoby service
     * invocation fails
     *************************************************************************/
    public void process()
	throws MobyException {
	process (1);
    }

    /**************************************************************************
     * The main method that packs input data, invokes a BioMoby
     * service and uses its response. The input data may consist from
     * more than one job (query) - the 'jobCount' is a suggestion how
     * many jobs will be included, but this can be changed by the
     * implementing sub-class. <p>
     *
     * Usually a client developer does not need to overwrite this
     * method. She or he makes the real input data filling in the
     * {@link #fillRequest} method, and uses the response in the
     * {@link #useResponse} method. <p>
     *
     * @throws MobyException if (a) a sub-class throws it during the
     * filling data or using response, or (b) a Biomoby service
     * invocation fails
     *************************************************************************/
    public void process (int jobCount)
	throws MobyException {

	String xmlResponse = null;

	// input: raw-level processing
	String xmlInput = fillRequest();
	if (xmlInput != null) {
	    if ( (xmlInput = interceptRequest (xmlInput)) == null )
		return;
	}

	// input: usual processing (i.e. collect XML in iterations)
	else {
	    MobyPackage mobyInput = new MobyPackage();
	    if (! fillRequest (mobyInput, jobCount)) return;
	    xmlInput = mobyInput.toXML();
	    if ( (xmlInput = interceptRequest (xmlInput)) == null )
		return;
	}

	// calling service
	xmlResponse = callRemoteService (xmlInput);

	// output: raw-level processing
	if (! useResponse (xmlResponse)) return;

	// output: usual processing (again by iterations)
	MobyPackage mobyResponse = MobyPackage.createFromXML (xmlResponse);
	useResponse (mobyResponse);
    }

    public String interceptRequest (String xmlInput)
	throws MobyException {
	return xmlInput;
    }

    /**************************************************************************
     * Create raw XML input. Override this method if you have already
     * an input XML, or you want to create it yourself. <p>
     *
     * @return a full XML input for a Biomoby service (in this case no
     * other <tt>fillRequest</tt> methods will called); return null if
     * no XML was created and a usual process to gather it will be used
     *
     * @throws MobyException if an XML cannot be created
     *************************************************************************/
    public String fillRequest()
	throws MobyException {
	return null;
    }

    /**************************************************************************
     *
     *************************************************************************/
    protected String filterMobyResponseType (Object result)
	throws MobyException {
	if (result instanceof String)
	    return (String)result;
	else if (result instanceof byte[])
	    return new String ((byte[])result);
	else
	    throw new MobyException
		("The Biomoby data should be sent/received either as type String or base64/byte[]. " +
		 "But they are of type '" + result.getClass().getName() + "'.");
    }

    /**************************************************************************
     *
     *************************************************************************/
    protected String callBiomobyService (MobyServiceLocator locator,
					 String xmlInput)
	throws MobyException {


	MobyService service = locator.getService();
	String serviceName = service.getName();
	boolean asBytes = locator.isAsBytes();
	String serviceEndpoint = service.getURL();
	int timeout = locator.getSuggestedTimeout();

	try {
	    URL target = new URL (serviceEndpoint);
 	    AxisCall call = new AxisCall (target, timeout);
 	    // if the locator has authentication information.
 	    if(locator.hasAuthentication()) {
 	        call.getCall().setProperty( Call.USERNAME_PROPERTY, locator.getUser() );
 	        call.getCall().setProperty( Call.PASSWORD_PROPERTY, locator.getPassword() );
 	    }
	    call.getCall().setSOAPActionURI (MobyService.BIOMOBY_SERVICE_URI + "#" + serviceName);
	    return filterMobyResponseType
		(call.doCall (MobyService.BIOMOBY_SERVICE_URI,
			      serviceName,
			      new Object[] { sendingFilter (xmlInput, asBytes) }));
	} catch (MalformedURLException e) {
	    throw new MobyException ("Service endpoint '" + serviceEndpoint +
				     "' is not a valid URL.");
  	} catch (GException e) {
 	    throw new MobyException (e.getMessage(), e);
  	}
    }

    //
    protected Object sendingFilter (String input, boolean asBytes) {
	if (asBytes) {
	    log.debug ("Data sent as a byte array");
	    return input.getBytes();
	} else {
	    return input;
	}
    }

    /**************************************************************************
     * Call a SOAP-based BioMoby service. In order to find what
     * service to call and what are its characteristics (such as its
     * endpoint) it will call method {@link #getServiceLocator} that
     * should be implemented by a sub-class. <p>
     *
     * Once it has the service locator, this class does one of the
     * following, in this order: <ul>
     *
     *   <li> The locator must contain at least a service name. If it
     *   does not, an exception is raised.
     *
     *   <li> If the locator contains a service endpoint, a call is
     *   made to this endpoint, using also the service name as a
     *   method name.
     *
     *   <li> If the locator has a registry endpoint, an enquiry to
     *   the registry is made to find an endpoint of a service
     *   corresponding with the given service name. Once found, the
     *   service is called.
     *
     *   <li> The same as the previous one but using a default
     *   registry.
     *
     * @param xmlInput data will be sent to the called Biomoby service
     *
     * @return service response (still in XML)
     *
     * @throws MobyException (a) if service call (or a call to
     * registry; for example because the registry does not know given
     * service) fails, or (b) if the used {@link MobyServiceLocator}
     * does not contain a service name.
     *
     *************************************************************************/
    public String callRemoteService (String xmlInput)
	throws MobyException {

	// 1) service name is a must
 	MobyServiceLocator locator = getServiceLocator();
	MobyService service = locator.getService();
	if (service == null)
	    throw new MobyException ("MobyService locator returned an empty service object.\n" +
				     "I do not know what service to call...");
	String serviceName = service.getName();
	if (isEmpty (serviceName) ||
	    MobyService.DUMMY_NAME.equals (serviceName))
	    throw new MobyException ("MobyService locator returned an empty service name.\n" +
				     "I do not know what service to call...");

	// 2) try service endpoint
	String serviceEndpoint = service.getURL();
	if (notEmpty (serviceEndpoint))
	    return callBiomobyService (locator, xmlInput);

	// 3) find service endpoint in a Biomoby registry
	Central worker = new CentralImpl (locator.getRegistryEndpoint(),
					  locator.getRegistryNamespace());
	MobyService[] services = worker.findService (service);
	if (services == null || services.length == 0)
	    throw new MobyException ("Service " + service.toShortString() +
				     " is not known in Biomoby registry: \n" +
				     "\t" + worker.getRegistryEndpoint() + "\n" +
				     "\t" + worker.getRegistryNamespace());
	// ...and call the service
	serviceEndpoint = services[0].getURL();
	if (notEmpty (serviceEndpoint)) {
	    service.setURL (serviceEndpoint);
	    return callBiomobyService (locator, xmlInput);
	}

	// what else can I do?
	throw new MobyException ("Registry has not returned any URL for service " +
				 service.toShortString());
    }

    /**************************************************************************
     * Fill the whole 'mobyInput' - put there any number of jobs
     * (queries) as you wish (you do not need to follow the 'jobCount'
     * hint suggesting how many jobs should be put there). <p>
     *
     * Usually there is not need to overwrite this method. It serves
     * as an inter-mediator between the main {@link #process} method
     * and the individual request fillings (done by a sub-class in
     * method {@link #fillRequest(MobyJob,MobyPackage)}). <p>
     *
     * @return false if you wish to cancel whole request (nothing will
     * be sent to a Biomoby service); otherwise return true.
     *
     * @param mobyInput is an empty shell that you are supposed to
     * fill with the input data for a Biomoby service execution
     *
     * @param jobCount is only a suggestion how many requests/job
     * should be created (it comes from the {@link #process} method) -
     * but it does not need to be obeyed
     *
     *************************************************************************/
    public boolean fillRequest (MobyPackage mobyInput, int jobCount)
	throws MobyException {

	if (jobCount < 0) jobCount = 0;
	for (int i = 0; i < jobCount; i++) {
	    MobyJob request = new MobyJob ("job_" + i);
	    if (! fillRequest (request, mobyInput))
		break;
	    mobyInput.addJob (request);
	}
	return (mobyInput.size() > 0);
    }

    /**************************************************************************
     * A raw-level processing. Use it if you need access to raw XML
     * coming from a service. <p>
     *
     * @param xmlResponse is a raw XML response returned from a
     * BioMoby service
     *
     * @return false if the response should be considered fully
     * processed (in this case no other 'useResponse' will be called);
     * true indicates that normal processing of the response will
     * follow; by default, this class (<tt>BaseClient</tt>) returns true
     *
     * @throws MobyException if you are not satisfied with a response
     * data, or from whatever reasons; it also throws this exception
     * if the 'mobyResponse' is broken
     *************************************************************************/
    public boolean useResponse (String xmlResponse)
	throws MobyException {
	return true;
    }

    /**************************************************************************
     * A high-level processing. Use it if you need access to all jobs
     * (queries) - that returned from a service - in the same time.
     * Otherwise use the processing on the job level (method {@link
     * #useResponse(MobyJob,MobyPackage)}. <p>
     *
     * @param mobyResponse is a full response returned from a BioMoby
     * service
     *
     * @throws MobyException if you are not satisfied with a response
     * data, or from whatever reasons; it also throws this exception
     * if the 'mobyResponse' is broken
     *************************************************************************/
    public void useResponse (MobyPackage mobyResponse)
	throws MobyException {

	// iterate over all input jobs
	for (int i = 0; i < mobyResponse.size(); i++) {
	    if (! useResponse (mobyResponse.getJob (i),
			       mobyResponse))
		return;
	}
    }

    /**************************************************************************
     * Extracts errors from a raw service response. It is iseful when
     * one does not want to create a whole <tt>MobyPackage</tt> from a
     * response, but just find whether a response is good or bad. <p>
     *
     * @param xmlResponse is a full response returned from a BioMoby
     * service
     *
     * @return a slightly formatted list of exceptions (of severity
     * <em>error</em>) extracted from the response; or null if there
     * are no errors there
     *************************************************************************/
    public String errorsInResponse (String xmlResponse) {
	try {
	    StringBuffer buf = new StringBuffer();
	    SAXBuilder builder = new SAXBuilder();
	    Document doc =
		builder.build (new StringReader (xmlResponse));
	    Element root = doc.getRootElement();
	    Element mobyContent = JDOMUtils.getChild (root, MobyTags.MOBYCONTENT);
	    Element serviceNotes = JDOMUtils.getChild (mobyContent, MobyTags.SERVICENOTES);
	    ServiceException[] exceptions =
		ServiceException.extractExceptions (serviceNotes);
	    for (int i = 0; i < exceptions.length; i++) {
		if (exceptions[i].getSeverity() != ServiceException.ERROR)
		    continue;
		if (buf.length() > 0)
		    buf.append ("\n");
		buf.append (exceptions[i].getErrorCodeAsString());
		buf.append (": ");
		buf.append (exceptions[i].getMessage());
	    }
	    return (buf.length() == 0 ? null : new String (buf));
	    
	} catch (Exception e) {
	    return e.toString();
	}
    }


    //
    // Abstract methods
    //

    /**************************************************************************
     * Return characteristics of a BioMoby service that will be
     * called, and that reveal where to find such service. <p>
     *
     * @see #callRemoteService
     *
     * @return an object defining a location of a BioMoby service
     * @throws MobyException if service locator cannot be
     * returned/created (e.g. because there is not enough information
     * about what service to call)
     *************************************************************************/
    abstract public MobyServiceLocator getServiceLocator()
	throws MobyException;

    /**************************************************************************
     * Crate data (fill them into 'request') for one Moby job
     * (query). The request will be sent within given 'inputContext' -
     * but it is not yet there (because you may wish not to put it
     * there - see the return value), and it is not the task of this
     * method to put it there (just fill the 'request'). <p>
     *
     * This is a method that should be implemented by a client
     * developer, and it is the place where the client's business
     * logic sits. <p>
     *
     * @return true if this request should be included into the input
     * data (package) that will be sent to a biomoby service; or
     * return false if you do not wish to create any more requests for
     * this particular package (also this 'request' will not be used)
     *
     * @param request is an object that you are supposed to fill with
     * input data for one service invocation; it already has a name
     * (so called 'query id' in the BioMoby speak) but you are free to change it
     *
     * @param inputContext is an envelope where all requests will be
     * stored and sent to a Biomoby service; you do not need to do
     * anything with it here unless you wish; note that all already
     * created requests are there, but not the one you are just
     * creating in this method
     *
     * @throws MobyException if you need so (from whatever reason in
     * your business logic); if thrown then nothing will be sent to a
     * Biomoby service
     *
     *************************************************************************/
    abstract public boolean fillRequest (MobyJob request, MobyPackage inputContext)
	throws MobyException;

    /**************************************************************************
     * Process a single job returned from a BioMoby service. <p>
     *
     * This is a method that should be implemented by a client
     * developer, and it is the place where the client's business
     * logic using the response sits. <p>
     *
     * @param response is an object that you are supposed to use
     *
     * @param responseContext is an envelope where the full response
     * (all its jobs) is located; you do not need to do anything with
     * it here unless you wish (e.g. it gives you knowledge about how
     * many jobs are in the full response, or it gives you access to
     * the so-called 'service notes')
     *
     * @return false if you do not wish to get any more
     * responses/jobs; otherwise return true
     *
     * @throws MobyException if you are not satisfied with a response
     * data, or from whatever reasons; it also throws this exception
     * if the 'response' is broken
     *
     *************************************************************************/
    abstract public boolean useResponse (MobyJob response,
					 MobyPackage responseContext)
	throws MobyException;

}
