// MobyService.java
//    A container for a service definition.
//
//    senger@ebi.ac.uk
//    February 2003
//

package org.biomoby.shared;

import java.util.*;
import java.net.URL;
import org.biomoby.shared.extended.ServiceInstanceParser;

/**
 * A container representing a service. But do not be too excited -
 * this is not a real service but only its definition as it appears in
 * the BioMoby registry. <p>
 *
 * This container is used mainly to register a new service in a
 * Moby registry, and to find registered services later. <p>
 *
 * @author <A HREF="mailto:senger@ebi.ac.uk">Martin Senger</A>
 * @version $Id: MobyService.java,v 1.28 2009/05/19 21:03:56 kawas Exp $
 */

public class MobyService
    implements Comparable<MobyService>, Cloneable, LSIDAccessible {

    /**
     * A dummy name used for MobyService instances that do not have
     * (yet) a real name.
     */
    static public final String DUMMY_NAME = "_dummy_";

    /**
     * An immutable string representing URI (namespace) of all Biomoby
     * services.
     */
    static public final String BIOMOBY_SERVICE_URI = "http://biomoby.org/";

    protected String name;
    protected String category = CATEGORY_MOBY;
    protected String authority = "";
    protected String emailContact = "";
    protected String type = "";
    protected String description = "";
    protected String url = "";
    protected String signatureURL = "";
    protected String rdf = "";
    protected String pathToRDF = "";
    protected boolean authoritativeService = true;
    protected String id = null;
    protected String lsid = null;
    protected MobyServiceType serviceType;
    // moby unit test object
    protected Vector<MobyUnitTest> unitTests = new Vector<MobyUnitTest>();

    public static final int UNCHECKED = 0;
    public static final int DEAD= 1;
    public static final int ALIVE = 2; // Can connect to the service endpoint
    public static final int PINGED = 4; // Responds to a blank MOBY request
    public static final int TESTED = 8; // Gives a valid answer to a valid test input provided by the service
    protected int serviceStatus = UNCHECKED;
    protected int statusChecks = UNCHECKED;

    /** Name of a default category for BioMoby services. */
    public static final String CATEGORY_MOBY = "moby";
    
    /** Name of a category for asynchornous BioMoby services. */
    public static final String CATEGORY_MOBY_ASYNC = "moby-async";
    
    /** Name of a category for CGI BioMoby services. */
    public static final String CATEGORY_CGI = "cgi";
    
    /** Name of an asynchronous CGI category for BioMoby services. */
    public static final String CATEGORY_CGI_ASYNC = "cgi-async";
    
    /** Name of a category for document/literal BioMoby services. */
    public static final String CATEGORY_MOBY_DOCLIT = "doc-literal";

    /** Name of a category for asynchronous document/literal BioMoby
     * services. */
    public static final String CATEGORY_MOBY_DOCLIT_ASYNC = "doc-literal-async";

    /** A suffix added to a service name in order to make a method
     * name that submits input to an asynchronous service.
     */
    public static final String SUBMIT_ACTION_SUFFIX = "_submit";
    
    // We need both, because you can't synchrinize on a null array
    protected static MobyService[] uninitializedServices = new MobyService[0];
    protected static MobyService[] services = uninitializedServices;
    protected static Map<String,MobyService> serviceMap = new HashMap<String,MobyService>();

    // the elements of these Vectors are of type MobyData
    protected Vector<MobyData> primaryInputs = new Vector<MobyData>();
    protected Vector<MobyData> secondaryInputs = new Vector<MobyData>();
    protected Vector<MobyData> primaryOutputs = new Vector<MobyData>();

    /**************************************************************************
     * Implementing Comparable interface.
     *************************************************************************/
    public int compareTo (MobyService obj) {
	return getUniqueName().compareToIgnoreCase ( obj.getUniqueName() );
    }

    public boolean equals (Object obj) {
	if (obj == null) return false;
	return getUniqueName().equals ( ((MobyService)obj).getUniqueName() );
    }
    
    public int hashCode() {
    	return getUniqueName().hashCode();
    }

    /**************************************************************************
     * Default constructor.
     *************************************************************************/
    public MobyService() {
	this (DUMMY_NAME);
    }

    /**************************************************************************
     * Normal constructor. Other characteristics are empty - which is
     * usually wrong - therefore use 'set' methods to fill them before
     * using this instance.
     *************************************************************************/
    public MobyService (String name) {
	setName (name);
    }

    /**************************************************************************
     * Even more normal constructor - because a service is fully
     * qualified only by its name <b>and</b> its authority. <p>
     *************************************************************************/
    public MobyService (String name, String authority) {
	setName (name);
	setAuthority (authority);
    }

    /**
     * Generally, you don't need to clone a service, unless you plan on modifying
     * fields of the object, but do not want it to affect the service definition 
     * used by other threads within the JVM (i.e. getService(serviceName) always 
     * returns the same object within a single JVM instance).
     */
    public MobyService clone(){
	MobyService clone = new MobyService(getName(), getAuthority());
	clone.setId(getId());
	clone.setAuthoritative(isAuthoritative());
	clone.setEmailContact(getEmailContact());
	clone.setCategory(getCategory());
	clone.setDescription(getDescription());
	clone.setType(getType());
	clone.setURL(getURL());
	clone.setSignatureURL(getSignatureURL());
	clone.setPathToRDF(getPathToRDF());
	clone.setRDF(getRDF());
	clone.setUnitTests(getUnitTests());
	
	clone.serviceStatus = serviceStatus;
	clone.statusChecks = statusChecks;
	for(MobyPrimaryData primary: getPrimaryInputs()){
	    clone.primaryInputs.add(primary.clone());
	}
	for(MobySecondaryData secondary: getSecondaryInputs()){
	    clone.secondaryInputs.add(secondary.clone());
	}
	for(MobyPrimaryData primary: getPrimaryOutputs()){
	    clone.primaryOutputs.add(primary.clone());
	}
	return clone;
    }

    public String getUniqueName() {
	return name + "/" + authority;
    }

    /**
     * @param statusCode should be one of ALIVE, PINGED, TESTED
     * @param mode whether the service passed the requirement or not
     */
    public void setStatus(int statusCode, boolean mode){
	if(mode){
           serviceStatus |= statusCode;
        }
	statusChecks |= statusCode;
    }

    /**
     * @return bit-wise combination of ALIVE, PINGED, TESTED states for the service, or DEAD if no tests passed, or UNCHECKED if no tests have been run
     */
    public int getStatus(){
        if((statusChecks & serviceStatus) != 0){ //some status test passed
	  return serviceStatus;
        }
        else if(statusChecks == UNCHECKED){
          return UNCHECKED;
        }
        else{
          return DEAD; // failed every test thrown at it so far
        }
    }

    public String getName() {
	return name;
    }
    public void setName (String value) {
	name = value;
    }

    /**
     * Return an ID that is given to this service instance during its
     * registration. The ID is used only for de-registration of this
     * service instance (you are not supposed to fill it when you
     * create an instance of this object). But it soon becomes
     * obsolete anyway because of the new way of registering services
     * (see {@link #getSignatureURL}). <p>
     */
    public String getId() {
	return id;
    }
    /**
     * Don't use it.
     * @see #getId
     */
    public void setId (String value) {
	id = value;
    }

    public String getLSID() {
	return lsid;
    }
    public void setLSID (String value) {
	lsid = value;
    }

    public boolean isAuthoritative() {
	return authoritativeService;
    }
    public void setAuthoritative (boolean value) {
	authoritativeService = value;
    }

    public String getAuthority() {
	return authority;
    }
    public void setAuthority (String value) {
	authority = (value == null ? "" : value);
    }

    public String getEmailContact() {
	return emailContact;
    }
    public void setEmailContact (String value) {
	emailContact = (value == null ? "" : value);
    }

    public String getCategory() {
	return category;
    }
    public void setCategory (String value) {
	category = (value == null ? CATEGORY_MOBY : value);
    }

    public String getDescription() {
	return description;
    }
    public void setDescription (String value) {
	description = (value == null ? "" : value);
    }

    public String getType() {
	return type;
    }
    public void setType (String value) {
        serviceType = (value == null ? new MobyServiceType() : new MobyServiceType(value));
	type = (value == null ? "" : value);
    }

    /**
     * 
     * @return an array of MobyUnitTest objects set for this service
     */
    public MobyUnitTest[] getUnitTests() {
	synchronized (unitTests) {
	    MobyUnitTest[] tests = new MobyUnitTest[unitTests.size()];
	    unitTests.copyInto(tests);
	    return tests;
	}
    }

    /**
     * 
     * @param unitTests
     *                the tests to set for the service
     */
    public void setUnitTests(MobyUnitTest[] unitTests) {
	if (unitTests == null) {
	    this.unitTests.clear();
	} else {
	    for (MobyUnitTest mut : unitTests) {
		addUnitTest(mut);
	    }
	}
    }
    
    /**
     * 
     * @param unitTest the MobyUnitTest to add to our service
     */
    public void addUnitTest(MobyUnitTest unitTest) {
	if (unitTest != null) {
	    for (MobyUnitTest t : unitTests) {
		// dont add duplicate tests
		if (t.toString().equals(unitTest.toString()))
		    return;
	    }
	    unitTests.add(unitTest);
	}
    }
    
    /**
     * 
     * @param unitTest the test to remove from the set of tests for the service
     */
    public void removeUnitTest(MobyUnitTest unitTest) {
	if (unitTest != null) {
	    for (int x = 0; x < unitTests.size(); x++) {
		if (unitTest.toString().equals(unitTests.elementAt(x).toString())) {
		    unitTests.removeElementAt(x);
		    return;
		}
	    }
	}
    }

	/**
     * Return a URL where this service is being served from. It is a
     * URL where the clients of this service are going when they want
     * to use it. <p>
     *
     * Note that there is nothing in the BioMoby registry remembering
     * (registering) a namespace of your service. Therefore, your
     * service is supposed to use <em>always</em> the namespace
     * <tt>http://biomoby.org</tt>. <p>
     */
    public String getURL() {
	return url;
    }
    /**
     * @see #getURL
     */
    public void setURL (String value) {
	url = (value == null ? "" : value);
    }

    /**
     * Return a URL pointing to an RDF document that contains this
     * service description (signature). Note that this RDF document
     * can include also other things, such as other service
     * signatures. <p>
     *
     * This is how it works:
     * <ol>
     *
     *   <li> When you register a service (using an instance of this
     *   class) you put here (by calling {@link #setSignatureURL}) a
     *   URL pointing to <em>your</em> HTTP space, to an RDF
     *   document. But this document does not need to exist yet.
     *
     *   <li> After a successful registration this instance will have
     *   an RDF document with this service signature. You can obtain
     *   it by calling {@link #getRDF}.
     *
     *   <li> You are expected to copy this RDF document to the place
     *   that you had suggested in step 1. Failing to do it will
     *   result in de-registrating your service automatically
     *   sometimes later. Therefore, if your service is only a testing
     *   one, do not copy RDF anywhere and the service will disappear.
     *
     *   <li> Some implementations of the {@link Central} interface
     *   (such as its default implementation {@link
     *   org.biomoby.client.CentralImpl CentralImpl} can copy the RDF
     *   document for you if you put here a fully qualified path to
     *   the file where the RDF document should be put (by calling
     *   {@link #setPathToRDF}).
     *
     * </ol>
     */
    public String getSignatureURL() {
	return signatureURL;
    }
    /**
     * @see #getSignatureURL
     */
    public void setSignatureURL (String value) {
	signatureURL = (value == null ? "" : value);
    }

    /**
     * @see #getSignatureURL
     */
    public String getPathToRDF() {
	return pathToRDF;
    }
    public void setPathToRDF (String value) {
	pathToRDF = (value == null ? "" : value);
    }

    /**
     * @see #getSignatureURL
     */
    public String getRDF() {
	return rdf;
    }
    public void setRDF (String value) {
	rdf = (value == null ? "" : value);
    }

    /**
     * Adds an array of input parameter to the service.  
     * NOTE: the "set" is a bit of a misnomer: parameters will be added to the existing list.
     * To clear the existing list of parameters, call this finction with 'null' first.
     */
    public void setInputs (MobyData[] value) {
	if (value == null) {
	    primaryInputs.clear();
	    secondaryInputs.clear();
	} else {
	    for (int i = 0 ; i < value.length; i++) {
		if (value[i].isPrimary())
		    addOrReplaceData(primaryInputs, value[i]);
		else
		    addOrReplaceData(secondaryInputs, value[i]);
	    }
	}
    }
    public void addInput (MobyData value) {
	if (value != null) {
	    if (value.isPrimary())
		addOrReplaceData(primaryInputs, value);
	    else
		addOrReplaceData(secondaryInputs, value);
	}
    }

    public void removeInput(MobyData value) {
	if (value != null) {
	    if (value.isPrimary())
		removeData(primaryInputs, value);
	    else
		removeData(secondaryInputs, value);
	}
    }

    private void addOrReplaceData(Vector<MobyData> vector, MobyData value){
	for(int i = 0; i < vector.size(); i++){
	    // Replace an existing parameter with the same name
	    if(vector.elementAt(i).getName().equals(value.getName())){
		vector.removeElementAt(i);
		vector.insertElementAt(value, i);
		return;
	    }
	}
	// Isn't a replacement, add to the end
	vector.addElement(value);
    }

    private void removeData(Vector<MobyData> vector, MobyData value){
	for(int i = 0; i < vector.size(); i++){
	    // Remove an existing parameter with the same name
	    if(vector.elementAt(i).getName().equals(value.getName())){
		vector.removeElementAt(i--);
	    }
	}
    }

    /**
     * Adds an array of output parameter to the service.  
     * NOTE: Unless the parameter is Primary, it will be ignored.
     * ALSO NOTE: the "set" is a bit of a misnomer: parameters will be added to the existing list.
     * To clear the existing list of parameters, call this finction with 'null' first.
     */
    public void setOutputs (MobyData[] value) {
	if (value == null) {
	    primaryOutputs.clear();
	} else {
	    for (int i = 0 ; i < value.length; i++) {
		if (value[i].isPrimary())
		    addOrReplaceData(primaryOutputs, value[i]);
	    }
	}
    }

    /**
     * Adds an output parameter to the service.  NOTE: Unless the parameter is Primary, it will be ignored.
     */
    public void addOutput (MobyData value) {
	if (value != null) {
	    if (value.isPrimary())
		addOrReplaceData(primaryOutputs, value);
	}
    }

    public void removeOutput (MobyData value) {
	if (value != null) {
	    if (value.isPrimary())
		removeData(primaryOutputs, value);
	}
    }

    public MobyPrimaryData[] getPrimaryInputs() {
	synchronized (primaryInputs) {
	    MobyPrimaryData[] results = new MobyPrimaryData [primaryInputs.size()];
	    primaryInputs.copyInto (results);
	    return results;
	}
    }

    public MobySecondaryData[] getSecondaryInputs() {
	synchronized (secondaryInputs) {
	    MobySecondaryData[] results = new MobySecondaryData [secondaryInputs.size()];
	    secondaryInputs.copyInto (results);
	    return results;
	}
    }

    public MobyPrimaryData[] getPrimaryOutputs() {
	synchronized (primaryOutputs) {
	    MobyPrimaryData[] results = new MobyPrimaryData [primaryOutputs.size()];
	    primaryOutputs.copyInto (results);
	    return results;
	}
    }

    // some historical reasons for this method...
    public boolean equals (MobyService anotherOne) {
 	if (anotherOne == null) return false;
	return equals ((Object)anotherOne);
    }

    public String toString() {
	StringBuffer buf = new StringBuffer();
	buf.append ("Name:          " + name + "\n");
	buf.append ("Type:          " + type + "\n");
	buf.append ("Category:      " + category + "\n");
	buf.append ("Auth:          " + authority + "\n");
	buf.append ("Desc:          " + description + "\n");
	buf.append ("URL:           " + url + "\n");
	buf.append ("Contact:       " + emailContact + "\n");
	buf.append ("LSID:          " + lsid + "\n");
	buf.append ("Signature URL: " + signatureURL + "\n");
	buf.append ("Path to RDF:   " + pathToRDF + "\n");
	if (id != null) buf.append ("ID:            " + id + "\n");
	
	// print unitTest information for this service
	if (unitTests != null && unitTests.size() > 0) {
	    buf.append("Unit Tests:\n");
	    for (MobyUnitTest unitTest : unitTests)
		buf.append(unitTest.toString()+"\n");
	} else {
	    buf.append("Unit Tests:     None available\n");
	}

	buf.append ("Primary inputs:\n");
	for (Enumeration<MobyData> en = primaryInputs.elements(); en.hasMoreElements(); )
	    buf.append (((MobyData)en.nextElement()).format (1));

	if (secondaryInputs.size() > 0) {
	    buf.append ("Secondary inputs:\n");
	    for (Enumeration<MobyData> en = secondaryInputs.elements(); en.hasMoreElements(); )
		buf.append (((MobyData)en.nextElement()).format (1));
	}

	buf.append ("Outputs:\n");
	for (Enumeration<MobyData> en = primaryOutputs.elements(); en.hasMoreElements(); )
	    buf.append (((MobyData)en.nextElement()).format (1));

	return new String (buf);
    }

    /**************************************************************************
     * It combines this service name and its authority name. It is
     * used also in {@link #equals} and {@link #compareTo} methods. <p>
     *
     * TBD: The authority should be checked that it does not contain
     * character sequence 'space followed by a left parenthesis'.
     *************************************************************************/
    public String toShortString() {
	return name + " (" + authority + ")";
    }

    /**************************************************************************
     * Create a comparator for case-insensitive sorting of services by
     * their authorities.
     *************************************************************************/
    public static Comparator getAuthorityComparator() {
	return new Comparator() {
		public int compare (Object o1, Object o2) {
		    String a1 = ((MobyService)o1).getAuthority();
		    String a2 = ((MobyService)o2).getAuthority();
		    int compared = (a1).compareToIgnoreCase ((String)a2);
		    if (compared == 0)
			return ( ((MobyService)o1).getName().compareToIgnoreCase ( ((MobyService)o2).getName() ) );
		    else
			return compared;
		}
	    };
    }

     /**
      * 
      * @return the MobyServiceType of this service
      */
 	public MobyServiceType getServiceType() {
 		return serviceType != null ? serviceType : new MobyServiceType();
 	}
 
 	/**
 	 * 
 	 * @param serviceType the MobyServiceType of this service
 	 */
 	public void setServiceType(MobyServiceType serviceType) {
 		this.type = (serviceType == null ? "" : serviceType.getName());
 		this.serviceType = (serviceType == null ? new MobyServiceType() : serviceType);
 	}

    public static MobyService getService(String lsid){
	String[] lsidParts = lsid.split(":");
	String[] sNameParts = lsidParts[4].split(",");
	return getService(sNameParts[1], sNameParts[0]);
    }

    public static MobyService getService(String name, String authority){
	if(name == null || authority == null){
	    return null;
	}
	
	// Perform a linear search for the corresponding namespace and authority
	String lsid = "urn:lsid:biomoby.org:serviceinstance:"+authority+","+name;
	if(!serviceMap.containsKey(lsid)){
	    try{
		// Note: when we properly implement LSID resolution in Java (hopefully soon), 
		// the metadata resolution URL below will come from DNS SRV records...and we'll parse the XML
		// rather than using a regex :-)
		URL lsidResolver = new URL("http://moby.ucalgary.ca/authority/metadata/?lsid="+lsid);
		Object o = getContent(lsidResolver);
		if(!(o instanceof String)){
		    System.err.println("Response for "+lsid + " was not a String, but a " + o.getClass());
		}
		String metadata = (String) o;

		String redirectPattern = "latest>";
		int redirectStringIndex = metadata.indexOf(redirectPattern+lsid);
		String lsidFull = null;
		if(redirectStringIndex != -1){
		    int lsidFullIndex = redirectStringIndex+redirectPattern.length();
		    lsidFull = metadata.substring(lsidFullIndex, lsidFullIndex+lsid.length()+21);
		    //System.err.println("Retrieving full LSID " + lsidFull);
		    lsidResolver = new URL("http://moby.ucalgary.ca/authority/metadata/?lsid="+lsidFull);
		    metadata = getContent(lsidResolver);
		}

		ServiceInstanceParser p = new ServiceInstanceParser();
		services = p.getMobyServicesFromRDF(metadata);
		if(services.length > 0){
		    serviceMap.put(lsid, services[0]);
		    if(lsidFull != null){
			serviceMap.put(lsidFull, services[0]);
		    }
		}
	    } catch(Exception e){
		System.err.println("Error while fetching service metadata: " + e);
	    }
	}

	return serviceMap.get(lsid);
    }

    private static String getContent(URL u) throws Exception{
	StringBuffer contents = new StringBuffer();
	java.io.LineNumberReader reader = new java.io.LineNumberReader(new java.io.InputStreamReader(u.openStream()));
	for(String line = reader.readLine(); line != null; line = reader.readLine()){
	    contents.append(line+"\n");
	}
	return contents.toString();
    }

    public boolean isAsynchronous(){
	return
	    category.equals (CATEGORY_MOBY_ASYNC) ||
	    category.equals (CATEGORY_MOBY_DOCLIT_ASYNC);
    }
}
