// CentralImpl.java
//    A default client to the Moby Central service.
//
//    senger@ebi.ac.uk
//    February 2003
//

package org.biomoby.client;

import org.biomoby.registry.meta.Registry;
import org.biomoby.shared.Central;
import org.biomoby.shared.MobyData;
import org.biomoby.shared.MobyDataType;
import org.biomoby.shared.MobyException;
import org.biomoby.shared.MobyNamespace;
import org.biomoby.shared.MobyPrimaryDataSet;
import org.biomoby.shared.MobyPrimaryDataSimple;
import org.biomoby.shared.MobyRelationship;
import org.biomoby.shared.MobySecondaryData;
import org.biomoby.shared.MobyService;
import org.biomoby.shared.MobyServiceType;
import org.biomoby.shared.NoSuccessException;
import org.biomoby.shared.PendingCurationException;
import org.biomoby.shared.MobyResourceRef;
import org.biomoby.shared.Utils;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.namespace.QName;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import org.apache.axis.AxisFault;
import org.apache.axis.client.Call;
import org.apache.axis.client.Service;
import org.apache.commons.httpclient.DefaultHttpMethodRetryHandler;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.methods.HeadMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.PrintStream;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;
import java.util.TreeMap;
import java.util.Comparator;
import java.util.zip.GZIPInputStream;
import java.util.logging.*;

/**
 * A default implementation of the
 * interface {@link org.biomoby.shared.Central Central}
 * allowing access to a Moby registry.
 *<p>
 * This class is supposed to be used by all other clients that wish
 * to communicate with the Moby Registry, but do not want to know
 * about all XML details that are necessary for talking with the Moby Central
 * directly. This is an example of a client program:
 *<pre>
 * import org.biomoby.shared.Central;
 * import org.biomoby.shared.MobyException;
 * import org.biomoby.client.CentralImpl;
 * import java.util.Map;
 * import java.util.Iterator;
 *
 * public class Test {
 *
 *    public static void main (String[] args)
 *       throws MobyException {
 *
 *       Central worker = new CentralImpl();
 *       Map authorities = worker.getServiceNamesByAuthority();
 *
 *       for (Iterator it = authorities.entrySet().iterator(); it.hasNext(); ) {
 *          Map.Entry entry = (Map.Entry)it.next();
 *          System.out.println (entry.getKey());
 *          String[] names = (String[])entry.getValue();
 *          for (int i = 0; i < names.length; i++)
 *             System.out.println ("\t" + names[i]);
 *       }
 *    }
 * }
 *</pre>
 *
 * @author <A HREF="mailto:senger@ebi.ac.uk">Martin Senger</A>
 * @version $Id: CentralImpl.java,v 1.65 2010/04/16 19:59:01 gordonp Exp $
 */

public class CentralImpl
    implements Central, SimpleCache {

    private URL endpoint;
    private String uri;
    protected boolean debug = false;

    /** Common central used to if getDefaultCentral() is called */
    protected static Map<String,CentralImpl> defaultCentrals = new HashMap<String,CentralImpl>();

    /** Default location (endpoint) of a Moby registry. */
    public static final String DEFAULT_ENDPOINT = "http://moby.ucalgary.ca/moby/MOBY-Central.pl";

    /** Default namespace used by the contacted Moby registry. */
    public static final String DEFAULT_NAMESPACE = "http://moby.ucalgary.ca/MOBY/Central";

    /**
     * The META-INF resource file that will be checked to determine what
     * default class should be instantiated in order to create a Central Implementation     
     * when getDefaultCentral() is called.
     */
    public static final String CENTRAL_IMPL_RESOURCE_NAME = "org.biomoby.client.CentralImpl";
    /** The class to use for getDefaultCentral if all else fails */
    public static final String DEFAULT_CENTRAL_IMPL_CLASSNAME = "org.biomoby.client.CentralDigestCachedImpl";
    private static Logger logger = Logger.getLogger(CentralImpl.class.getName());

   /**
    * Thread local that gives each thread its own
    * DocumentBuilderFactory (since it is not thread-safe). Code taken
    * from Apache's JaxpUtils.
    */
   public static ThreadLocal DOCUMENT_BUILDER_FACTORIES = new ThreadLocal() {
	   protected synchronized Object initialValue() {
	       DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	       dbf.setNamespaceAware (true);
	       return dbf;
	   }
       };


    /*************************************************************************
     * Default constructor. It connects to a default Moby registry
     * (as defined in {@link #DEFAULT_ENDPOINT}) using a default namespace
     * (as defined int {@link #DEFAULT_NAMESPACE}).
     *************************************************************************/
    public CentralImpl()
	throws MobyException {
	this (DEFAULT_ENDPOINT, DEFAULT_NAMESPACE);
    }

    /*************************************************************************
     * Constructor allowing to specify which Moby Registry to use.
     *
     * @throws MobyException if 'endpoint' is not a valid URL, or if no
     *                          DOM parser is available
     *************************************************************************/
    public CentralImpl (String endpoint)
	throws MobyException {
	this (endpoint, DEFAULT_NAMESPACE);
    }

    /*************************************************************************
     * Constructor allowing to specify which Moby Registry and what
     * namespace to use. If any of the parameters is null, its default
     * value is used instead.
     *<p>
     * @throws MobyException if 'endpoint' is not a valid URL, or if no
     *                          DOM parser was found
     *************************************************************************/
    public CentralImpl (String endpoint, String namespace)
	throws MobyException {

	if (endpoint == null || "".equals (endpoint.trim()))
	    endpoint = DEFAULT_ENDPOINT;
	if (namespace == null || "".equals (namespace.trim()))
	    namespace = DEFAULT_NAMESPACE;

	try {
	    this.endpoint = new URL (endpoint);
	} catch (MalformedURLException e) {
	    throw new MobyException ("Bad URL: " + endpoint);
	}
	this.uri = namespace;

	cache = new Hashtable<String,Object>();
	useCache = true;
    }

    /*************************************************************************
     * Loads a DOM Document from an InputStream. Uses thread-safe
     * mechanism.
     *************************************************************************/
    public static Document loadDocument (InputStream input)
	throws MobyException {
	try {
	    DocumentBuilderFactory dbf
		= (DocumentBuilderFactory)DOCUMENT_BUILDER_FACTORIES.get();
	    DocumentBuilder db = dbf.newDocumentBuilder();
	    return (db.parse (input));
	} catch (Exception e) {
	    throw new MobyException ("Problem with reading XML input: " + e.toString(), e);
	}
    }

    /*************************************************************************
     * Call 'method' with 'parameters' and return its result.
     *************************************************************************/
    protected Object doCall (String method, Object[] parameters)
	throws MobyException {

	Call call = null;
	try {
	    Service service = new Service();
	    call = (Call) service.createCall();
	    call.setTargetEndpointAddress (endpoint);
	    call.setTimeout (new Integer (0));

	    call.setSOAPActionURI (uri + "#" + method);

	    if (debug) {
		System.err.println ("METHOD CALL: " + method);
		System.err.println ("------------");
		if (parameters.length > 0)
		    System.err.println (parameters[0] + "\n");
		System.err.println ("------------\n");

		Object result = call.invoke (uri, method, parameters);

		System.err.println ("METHOD RETURN:");
		System.err.println ("------------");
		if (result != null)
		    System.err.println (result + "\n");
		System.err.println ("------------\n");

		return resultToString (result);

	    } else {
		return resultToString (call.invoke (uri, method, parameters));
	    }

	} catch (AxisFault e) {
	    throw new MobyException
		(formatFault (e,
			      endpoint.toString(),
			      (call == null ? null : call.getOperationName())),
		 e);
// 		(endpoint.toString()+(call == null ? "" : call.getOperationName()), e);

	} catch (Exception e) {
	    throw new MobyException (e.toString(), e);
// 	    e.printStackTrace();
 	}
    }


    /**************************************************************************
     * Parse the given XML sniplet to find tag 'success'. If it has value '1'
     * look further for tag 'id' and return it back (or return an empty string
     * if ID is not there). Otherwise raise an exception with the 'culprit'
     * and with the message from the tag 'message'. <p>
     *
     * The return value is a two-element long array. The first element
     * is the ID (given by BioMobe registry), and the second element
     * is RDF corresponding with the registered object (BioMoby
     * returns this only for service instances, so for other objects
     * this will be null). <p>
     *
     * This is how the XML is supposed to look:
     *     <MOBYRegistration>
     *        <success> <!-- 1 | 0 | -1 --> </success>
     *        <id> <!-- some id number for your registration --> </id>  
     *        <message> <![CDATA[message here]]> </message>
     *        <RDF> <!-- RDF of your service instance here (if applicable) --> </RDF>
     *     </MOBYRegistration>
     *
     * Success takes the value "1" to indicate success, "0" to
     * indicate failure, and "-1" to indicate "Pending Curation".
     *************************************************************************/
    protected String[] checkRegistration (String xml, Object culprit)
	throws MobyException, NoSuccessException, PendingCurationException {

	String id = "", success = "0", message = "", rdf = "";

	// parse returned XML
	Document document = loadDocument (new ByteArrayInputStream (xml.getBytes()));
	Element root = document.getDocumentElement();

	NodeList children = root.getChildNodes();
	for (int i = 0; i < children.getLength(); i++) {
	    if (children.item (i).getNodeType() != Node.ELEMENT_NODE)
		continue;
	    Element elem = (Element)children.item (i);
	    if (elem.getNodeName().equals ("id")) {
		if (elem.getFirstChild() != null)
		    id = elem.getFirstChild().getNodeValue();
	    } else if (elem.getNodeName().equals("success")) {
		if (elem.getFirstChild() != null)
		    success = elem.getFirstChild().getNodeValue();
	    } else if (elem.getNodeName().equals ("message")) {
		if (elem.getFirstChild() != null)
		    message = elem.getFirstChild().getNodeValue();
	    } else if (elem.getNodeName().equals ("RDF")) {
		if (elem.getFirstChild() != null)
		    rdf = elem.getFirstChild().getNodeValue();
	    }
	}

	if (success.equals ("0"))
	    throw new NoSuccessException (message, culprit);
	else if (success.equals ("-1"))
	    throw new PendingCurationException();
	return new String[] { id, rdf };
    }

    /**************************************************************************
     * Return a piece of XML created from the definitions representing input
     * data types and their usage in the given service. Only data considered
     * primary are included. Note that the main job of converting to XML is
     * done by instances of MobyPrimaryData.
     *
     * The returned XML looks like this:
     *    <Input>
     *       <!-- zero or more Primary (Simple and/or Complex) articles -->
     *    </Input>
     *************************************************************************/
    protected String buildPrimaryInputTag (MobyService service) {
	StringBuffer buf = new StringBuffer();
	MobyData[] primaryInputs = service.getPrimaryInputs();
	buf.append ("<Input>\n");
	for (int i = 0; i < primaryInputs.length; i++)
	    buf.append (primaryInputs[i].toXML());
	buf.append ("</Input>\n");
	return new String (buf);
    }

    /**************************************************************************
     * Return a piece of XML created from the definitions representing input
     * data types and their usage in the given service. Only data considered
     * secondary are included. Note that the main job of converting to XML is
     * done by instances of MobySecondaryData.
     *
     * The returned XML looks like this:
     *    <secondaryArticles>
     *       <!-- zero or more INPUT Secondary articles -->
     *    </secondaryArticles>
     *************************************************************************/
    protected String buildSecondaryInputTag (MobyService service) {
	StringBuffer buf = new StringBuffer();
	MobyData[] secInputs = service.getSecondaryInputs();
	buf.append ("<secondaryArticles>\n");
	for (int i = 0; i < secInputs.length; i++) {
	    buf.append (secInputs[i].toXML());
	}
	buf.append ("</secondaryArticles>\n");
	return new String (buf);
    }

    /**************************************************************************
     * Return a piece of XML created from the definitions representing output
     * data types and their usage in the given service. Only data considered
     * primary are included. Note that the main job of converting to XML is
     * done by instances of MobyPrimaryData.
     *
     * The returned XML looks like this:
     *    <Output>
     *       <!-- zero or more Primary (Simple and/or Complex) articles --> 
     *    </Output>
     *
     *************************************************************************/
    protected String buildOutputTag (MobyService service) {
	StringBuffer buf = new StringBuffer();
	MobyData[] primaryOutputs = service.getPrimaryOutputs();
	buf.append ("<Output>\n");
	for (int i = 0; i < primaryOutputs.length; i++)
	    buf.append (primaryOutputs[i].toXML());
	buf.append ("</Output>\n");
	return new String (buf);
    }

    /**************************************************************************
     * Return a piece of XML represented a query object (an object used
     * to find a service).
     *
     * The returned XML looks like this:
     *
     *    <inputObjects>
     *      <Input>
     *           <!-- one or more Simple or Complex Primary articles -->
     *      </Input>
     *    </inputObjects>
     *    <outputObjects>
     *      <Output>
     *           <!-- one or more Simple or Complex Primary articles -->
     *      </Output>
     *    </outputObjects>
     *    <serviceType>ServiceTypeTerm</serviceType>
     *    <serviceName>ServiceName</serviceName>
     *    <Category>moby</Category>
     *    <authURI>http://desired.service.provider</authURI>;
     *    <expandObjects>1|0</expandObjects> 
     *    <expandServices>1|0</expandServices>
     *    <authoritative>1|0</authoritative>
     *    <keywords>
     *         <keyword>something</keyword>
     *         ....
     *         ....
     *    </keywords>
     *************************************************************************/
    protected String buildQueryObject (MobyService service,
				       String[] keywords,
				       boolean expandObjects,
				       boolean expandServices,
				       boolean authoritative) {
	if (service == null) {
	    service = new MobyService ("dummy");
	    service.setCategory ("");
	}
	StringBuffer buf = new StringBuffer();

	buf.append ("<inputObjects>\n<Input>\n");
	MobyData[] pi = service.getPrimaryInputs();
	if (pi.length > 0) {
	    for (int i = 0; i < pi.length; i++)
		buf.append (pi[i].toXML());
	}
	buf.append ("</Input>\n</inputObjects>\n");

	buf.append ("<outputObjects>\n<Output>\n");
	MobyData[] po = service.getPrimaryOutputs();
	if (po.length > 0) {
	    for (int i = 0; i < po.length; i++)
		buf.append (po[i].toXML());
	}
	buf.append ("</Output>\n</outputObjects>\n");

	buf.append ("<serviceType>" + service.getType() + "</serviceType>\n");

	String name = service.getName();
	if (!name.equals ("") && !name.equals ("dummy") && !name.equals (MobyService.DUMMY_NAME))
	    buf.append ("<serviceName>" + service.getName() + "</serviceName>\n");

	String sigURL = service.getSignatureURL();
	if (!sigURL.equals (""))
	    buf.append ("<signatureURL>" + sigURL + "</signatureURL>\n");
	
	buf.append ("<Category>" + service.getCategory() + "</Category>\n");
	buf.append ("<authURI>" + service.getAuthority() + "</authURI>\n");

	buf.append ("<expandObjects>");
	buf.append (expandObjects ? "1" : "0");
	buf.append ("</expandObjects>\n");

	buf.append ("<expandServices>");
	buf.append (expandServices ? "1" : "0");
	buf.append ("</expandServices>\n");

	buf.append ("<authoritative>");
 	buf.append (authoritative ? "1" : "0");
	buf.append ("</authoritative>\n");

	buf.append ("<keywords>\n");
	if (keywords != null && keywords.length > 0) {
	    for (int i = 0; i < keywords.length; i++) {
		buf.append ("<keyword>");
		buf.append (keywords[i]);
		buf.append ("</keyword>\n");
	    }
	}
	buf.append ("</keywords>\n");

	return new String (buf);
    }
 
    /**************************************************************************
     * Extract one or more MobyService objects from the given XML piece.
     * The XML should look like this:
     * <pre>
     *  &lt;Services&gt;
     *    &lt;Service authURI="authority.URI.here" lsid="..." serviceName="MyService"&gt;
     *      &lt;serviceType&gt;Service_Ontology_Term&lt;/serviceType&gt;
     *      &lt;Category&gt;moby&lt;/Category&gt; &lt;!-- or 'cgi' or 'soap' --&gt;
     *      &lt;contactEmail&gt;your@email.addy.here&lt;/contactEmail&gt;
     *      &lt;signatureURL&gt;http://service.RDF.here&lt;/signatureURL&gt;
     *      &lt;URL&gt;http://service.endpoint.here/scriptname&lt;/URL&gt;
     *      &lt;authoritative&gt;1&lt;/authoritative&gt;
     *      &lt;Input&gt;
     *           &lt;!-- one or more Simple and/or Complex Primary articles --&gt;
     *      &lt;/Input&gt;
     *      &lt;Output&gt;
     *           &lt;!-- one or more Simple and/or Complex Primary articles --&gt; 
     *      &lt;/Output&gt;
     *      &lt;secondaryArticles&gt;
     *           &lt;!-- one or more Secondary articles --&gt;
     *      &lt;/secondaryArticles&gt;
     *      &lt;Description&gt;&lt;![CDATA[free text description here]]&gt;&lt;/Description&gt;
     *    &lt;/Service&gt;
     *    ...  &lt;!--  one or more Service blocks may be returned --&gt;
     *    ...
     *    ...
     *  &lt;/Services&gt;
     * </pre>
     * @throws MobyException if the XML document is invalid
     *************************************************************************/
    public MobyService[] extractServices (String xml)
	throws MobyException {

	Document document = loadDocument (new ByteArrayInputStream (xml.getBytes()));
	NodeList list = document.getElementsByTagName ("Service");
	MobyService[] results = new MobyService [list.getLength()];
	for (int i = 0; i < list.getLength(); i++) {
	    Element elem = (Element)list.item (i);
	    MobyService service = new MobyService (elem.getAttribute ("serviceName"));
	    service.setAuthority (elem.getAttribute ("authURI"));
	    service.setLSID (elem.getAttribute ("lsid"));
	    NodeList children = elem.getChildNodes();
	    for (int j = 0; j < children.getLength(); j++) {
		String nodeName = children.item (j).getNodeName();
		if (nodeName.equals ("Description")) {
		    service.setDescription (getFirstValue (children.item (j)));
		} else if (nodeName.equals ("Category")) {
		    service.setCategory (getFirstValue (children.item (j)));
		} else if (nodeName.equals ("URL")) {
		    service.setURL (getFirstValue (children.item (j)));
		} else if (nodeName.equals ("signatureURL")) {
		    service.setSignatureURL (getFirstValue (children.item (j)));
		} else if (nodeName.equals ("contactEmail")) {
		    service.setEmailContact (getFirstValue (children.item (j)));
		} else if (nodeName.equals ("serviceType")) {
		    service.setType (getFirstValue (children.item (j)));
		    MobyServiceType mst = new MobyServiceType(service.getType());
		    NamedNodeMap map = (children.item (j).getAttributes());
			if (map != null) {
				Node node = map.getNamedItemNS(children.item(j).getNamespaceURI(),"lsid");
				if (node != null)
					mst.setLSID(node.getNodeValue());
			}
			service.setServiceType(mst);
		} else if (nodeName.equals ("authoritative")) {
		    String authoritative = getFirstValue (children.item (j));
		    service.setAuthoritative (authoritative.equals ("1") ? true : false);
		} else if (nodeName.equals ("Input")) {
		    // <Input>
		    //   <!-- one or more Simple and/or Complex Primary articles -->
		    //   <Simple articleName="NameOfArticle">
		    //      ...
		    //   </Simple>
		    //   <Collection articleName="NameOfArticle">
		    //      <Simple>......</Simple>
		    //      <Simple>......</Simple>
		    //   </Collection>
		    // </Input>
		    NodeList inputs = children.item (j).getChildNodes();
		    for (int k = 0; k < inputs.getLength(); k++) {
			if (inputs.item (k).getNodeName().equals ("Simple")) {
			    MobyPrimaryDataSimple data = new MobyPrimaryDataSimple ((Element)inputs.item (k));
			    service.addInput (data);
			} else if (inputs.item (k).getNodeName().equals ("Collection")) {
			    MobyPrimaryDataSet data = new MobyPrimaryDataSet ((Element)inputs.item (k));
			    service.addInput (data);
			}
		    }
		} else if (nodeName.equals ("Output")) {
		    // <Output>
		    //   <!-- one or more Simple and/or Complex Primary articles --> 
		    // </Output>
		    NodeList inputs = children.item (j).getChildNodes();
		    for (int k = 0; k < inputs.getLength(); k++) {
			if (inputs.item (k).getNodeName().equals ("Simple")) {
			    MobyPrimaryDataSimple data = new MobyPrimaryDataSimple ((Element)inputs.item (k));
			    service.addOutput (data);
			} else if (inputs.item (k).getNodeName().equals ("Collection")) {
			    MobyPrimaryDataSet data = new MobyPrimaryDataSet ((Element)inputs.item (k));
			    service.addOutput (data);
			}
		    }

		} else if (nodeName.equals ("secondaryArticles")) {
		    // <Parameter articleName="NameOfArticle">
		    //   ...
		    // </Parameter>
		    NodeList parameters = children.item (j).getChildNodes();
		    for (int k = 0; k < parameters.getLength(); k++) {
			if (parameters.item (k).getNodeName().equals ("Parameter")) {
			    MobySecondaryData data = new MobySecondaryData ((Element)parameters.item (k));
			    service.addInput (data);
			}
		    }
		}
	    }
	    results [i] = service;
	}
	return results;
    }

    // protect against null values
    protected String getFirstValue (Node child) {
	Node node = child.getFirstChild();
	if (node == null) return "";
	String value = node.getNodeValue();
	if (value == null) return "";
	return value;
    }
    
    protected String getFirstValue (NodeList children) {
	if (children.item(0) != null && children.item(0).hasChildNodes()) {
	    children.item(0).normalize();
	    return getFirstValue (children.item(0));
	}
	return "";
    }

    /**************************************************************************
     * 
     * Implementing SimpleCache interface.
     *
     * Why to have an interface for such trivial thing? Well, because
     * I needed to overwrite the caching mechanism in the subclasses
     * so I needed to have all caching functions as separate methods -
     * that's why I have collect them in an interface.
     *
     *************************************************************************/
    private Hashtable<String,Object> cache;   // this is the cache itself
    private boolean useCache;  // this signal that we are actually caching things

    // not used here
    public String createId (String rootName,
			    String semanticType, String syntaxType,
			    long lastModified,
			    Properties props) {
	return ""; // not used here
    }

    // check existence of a cached object
    public boolean existsInCache (String id) {
	synchronized (cache) {
	    if (useCache) return cache.containsKey (id);
	    else return false;
	}
    }

    // retrieve from cache
    public Object getContents (String id) {
	synchronized (cache) {
	    if (useCache) return cache.get (id);
	    else return null;
	}
    }

    // cache an object
    public void setContents (String id, java.lang.Object data) {
	synchronized (cache) {
	    if (useCache) cache.put (id, data);
	}
    }

    // in this implementation, it clears the whole cache, regardless
    // what 'id' is passed
    public void removeFromCache (String id) {
	cache.clear();
    }

    /**************************************************************************
     *
     * And the other methods related to caching (but not part of the
     * SimpleCache interface).
     *
     **************************************************************************/

    /**************************************************************************
     * By default, caching is enabled to reduce network traffic.
     * Setting this to false will clear the cache, and not cache any
     * further calls unless it is set to true again. <p>
     *
     * @param shouldCache whether retrieveXXX call results should be
     * cached in case they are called again (i.e. don't request
     * MobyCentral every time)
     **************************************************************************/
    public void setCacheMode (boolean shouldCache) {
	useCache = shouldCache;
	if (! useCache)
	    removeFromCache (null);
    }

    /**************************************************************************
     * Find if caching is currently enabled.
     *
     * @return true if caching is enabled
     **************************************************************************/
    public boolean getCacheMode(){
	return useCache;
    }

    /**************************************************************************
     * Parses and imports the following XML.
     * <pre>
     * &lt;serviceNames&gt;
     *   &lt;serviceName name="serviceName" authURI='authority.info.here'/&gt;
     *   ...
     *   ...
     * &lt;/serviceNames&gt;
     * </pre>
     *
     * @deprecated Replaced by {@link
     * #getServiceNamesByAuthority}. The reason is that this method
     * returns a random result if there are more services with the
     * same name but belonging to different authorities. <p>
     *
     *************************************************************************/
    public Map<String,String> getServiceNames()
	throws MobyException {

	String result = (String)doCall ("retrieveServiceNames",
					new Object[] {});
	// parse returned XML
	Map<String,String> results = new TreeMap<String,String> (getStringComparator());
	Document document = loadDocument (new ByteArrayInputStream (result.getBytes()));
	NodeList list = document.getElementsByTagName ("serviceName");
	for (int i = 0; i < list.getLength(); i++) {
	    Element elem = (Element)list.item (i);
	    results.put (elem.getAttribute ("name"),
			 elem.getAttribute ("authURI"));
	}

	return results;
    }

    /**************************************************************************
     * Parses and imports the following XML.
     * <pre>
     * &lt;serviceNames&gt;
     *   &lt;serviceName name="serviceName" lsid="..." authURI='authority.info.here'/&gt;
     *   ...
     *   ...
     * &lt;/serviceNames&gt;
     * </pre>
     *
     * @return a Map which has authorities as keys, and String arrays
     * with service names as a values.
     *************************************************************************/
    public Map getServiceNamesByAuthority()
	throws MobyException {
	String result = getServiceNamesByAuthorityAsXML();
	return createServicesByAuthorityFromXML (result, true);
    }

    /**************************************************************************
     * Similar to {@link #getServiceNamesByAuthority} but the
     * resulting Map contains slightly more. <p>
     *
     * @return a Map which has authorities as keys, and arrays of
     * MobyServices as a values. Each MobyService is filled with its
     * name, authority and LSID.
     *************************************************************************/
    public Map getServicesByAuthority()
	throws MobyException {
	String result = getServiceNamesByAuthorityAsXML();
	return createServicesByAuthorityFromXML (result, false);
    }

    //
    protected String getServiceNamesByAuthorityAsXML()
	throws MobyException {
	return (String)doCall ("retrieveServiceNames",
			       new Object[] {});
    }

    // if onlyNames == true
    //    Map: authority name -> String[]
    //                        (filled with service namea)
    // else
    //    Map: authority name -> MobyService[]
    //                        (filled with service name, authority and lsid)
    protected Map createServicesByAuthorityFromXML (String result,
						    boolean onlyNames)
	throws MobyException {

	// parse returned XML
	Map results = new TreeMap (getStringComparator());
	Document document = loadDocument (new ByteArrayInputStream (result.getBytes()));
	NodeList list = document.getElementsByTagName ("serviceName");
	for (int i = 0; i < list.getLength(); i++) {
	    Element elem = (Element)list.item (i);
	    String name = elem.getAttribute ("name");
	    String auth = elem.getAttribute ("authURI");
	    Vector<Object> v =
		(results.containsKey (auth) ? (Vector)results.get (auth) : new Vector<Object>());
	    if (onlyNames) {
		v.addElement (name);
	    } else {
		MobyService ms = new MobyService (name);
		ms.setAuthority (auth);
		ms.setLSID (elem.getAttribute ("lsid"));
		v.addElement (ms);
	    }
	    results.put (auth, v);
	}

	// change values of type Vector to MobyService[] or String[]
	for (Iterator it = results.entrySet().iterator(); it.hasNext(); ) {
	    Map.Entry entry = (Map.Entry)it.next();
	    Vector v = (Vector)entry.getValue();
	    if (onlyNames) {
		String[] sNames = new String [v.size()];
		v.copyInto (sNames);
		entry.setValue (sNames);
	    } else {
		MobyService[] mss = new MobyService [v.size()];
		v.copyInto (mss);
		entry.setValue (mss);
	    }
	}

	return results;
    }

    /**************************************************************************
     * Parses and imports the following XML.
     * <pre>
     *  &lt;serviceProviders&gt;
     *     &lt;serviceProvider name="authority.URI.here"/&gt;
     *          ...
     *          ...
     *  &lt;/serviceProviders&gt;
     * </pre>
     *************************************************************************/
    public String[] getProviders()
	throws MobyException {

	String cacheId = "retrieveServiceProviders";
	String[] cachedResults = (String[])getContents (cacheId);
	if (cachedResults != null)
	    return cachedResults;

	String result = (String)doCall ("retrieveServiceProviders",
					new Object[] {});

	// parse returned XML
	Document document = loadDocument (new ByteArrayInputStream (result.getBytes()));
	NodeList list = document.getElementsByTagName ("serviceProvider");
	String[] results = new String [list.getLength()];
	for (int i = 0; i < list.getLength(); i++)
	    results[i] = ((Element)list.item (i)).getAttribute ("name");

	// Add this data to the cache in case we get called again
	setContents (cacheId, results);

	return results;
    }

    /**************************************************************************
     * Parses and imports the following XML.
     * <pre>
     *  &lt;serviceTypes&gt;
     *     &lt;serviceType name="serviceName" lsid="..."&gt;
     *            &lt;Description&gt;&lt;![CDATA[free text description here]]&gt;&lt;/Description&gt;
     *            &lt;contactEmail&gt;...&lt;/contactEmail&gt;
     *            &lt;authURI&gt;...&lt;/authURI&gt;
     *     &lt;/serviceType&gt;
     *          ...
     *          ...
     *  &lt;/serviceTypes&gt;
     * </pre>
     *************************************************************************/
    public Map<String,String> getServiceTypes()
	throws MobyException {
	String result = getServiceTypesAsXML();
        Map<String,String> results = new TreeMap<String,String>(getStringComparator());
	MobyServiceType[] types = createServiceTypesFromXML (result);
	for (int i = 0; i < types.length; i++) {
	    results.put (types[i].getName(),
			 types[i].getDescription());
	}
	return results;
    }

    //
    protected String getServiceTypesAsXML()
	throws MobyException {
	return (String)doCall ("retrieveServiceTypes",
			       new Object[] {});
    }

    // but be aware that the created MobyServiceTypes are not complete
    // - they do not have the relationship information; that's why
    // this method is not public; the full service types are available
    // from CentralDigest implementations
    protected MobyServiceType[] createServiceTypesFromXML (String result)
	throws MobyException {

	// parse returned XML
	Document document = loadDocument (new ByteArrayInputStream (result.getBytes()));
	NodeList list = document.getElementsByTagName ("serviceType");
	if (list == null || list.getLength() == 0)
	    return new MobyServiceType[] {};
	MobyServiceType[] results = new MobyServiceType [list.getLength()];
	for (int i = 0; i < list.getLength(); i++) {
	    Element elem = (Element)list.item (i);
	    MobyServiceType st = new MobyServiceType (elem.getAttribute ("name"));
	    st.setLSID (elem.getAttribute ("lsid"));
	    st.setDescription (getFirstValue (elem.getElementsByTagName ("Description")));
	    st.setEmailContact (getFirstValue (elem.getElementsByTagName ("contactEmail")));
	    st.setAuthority (getFirstValue (elem.getElementsByTagName ("authURI")));
	    results[i] = st;
	}
 	java.util.Arrays.sort (results);
	return results;
    }

    /**************************************************************************
     * Parses and imports the following XML.
     * <pre>
     *  &lt;Namespaces&gt;
     *     &lt;Namespace name="namespace" lsid="..."&gt;
     *            &lt;Description&gt;&lt;![CDATA[free text description here]]&gt;&lt;/Description&gt;
     *            &lt;contactEmail&gt;...&lt;/contactEmail&gt;
     *            &lt;authURI&gt;...&lt;/authURI&gt;
     *     &lt;/Namespace&gt;
     *          ...
     *          ...
     *  &lt;/Namespaces&gt;
     * </pre>
     *************************************************************************/
    public MobyNamespace[] getFullNamespaces()
	throws MobyException {

	String result = getNamespacesAsXML();
	return createNamespacesFromXML (result);
    }

    //
    protected String getNamespacesAsXML()
	throws MobyException {
	return (String)doCall ("retrieveNamespaces",
			       new Object[] {});
    }

    //
    protected MobyNamespace[] createNamespacesFromXML (String result)
	throws MobyException {

	// parse returned XML
	Document document = loadDocument (new ByteArrayInputStream (result.getBytes()));
	NodeList list = document.getDocumentElement().getElementsByTagName ("Namespace");
	if (list == null || list.getLength() == 0) {
	    return new MobyNamespace[] {};
	}
	MobyNamespace[] results = new MobyNamespace [list.getLength()];
	for (int i = 0; i < list.getLength(); i++) {
	    Element elem = (Element)list.item (i);
	    MobyNamespace nm = new MobyNamespace (elem.getAttribute ("name"));
	    nm.setLSID (elem.getAttribute ("lsid"));
	    nm.setDescription (getFirstValue (elem.getElementsByTagName ("Description")));
	    nm.setEmailContact (getFirstValue (elem.getElementsByTagName ("contactEmail")));
	    nm.setAuthority (getFirstValue (elem.getElementsByTagName ("authURI")));
	    results[i] = nm;
	}

 	java.util.Arrays.sort (results);
	return results;
    }

    /**************************************************************************
     *
     * @deprecated Replaced by {@link #getFullNamespaces} that gives
     * more information for the same price. <p>
     *************************************************************************/
    public Map getNamespaces()
	throws MobyException {

 	Map results = new TreeMap (getStringComparator());
	MobyNamespace[] namespaces = getFullNamespaces();
	for (int i = 0; i < namespaces.length; i++) {
	    results.put (namespaces[i].getName(),
			 namespaces[i].getDescription());
	}
	return results;
    }

    /**************************************************************************
     * Parses and imports the following XML.
     * <pre>
     *  &lt;objectNames&gt;
     *     &lt;Object name="objectName" lsid="..."&gt;
     *            &lt;Description&gt;&lt;![CDATA[free text description here]]&gt;&lt;/Description&gt;
     *     &lt;/Object&gt;
     *          ...
     *          ...
     *  &lt;/objectNames&gt;
     * </pre>
     *************************************************************************/
    public Map getDataTypeNames()
	throws MobyException {
	String result = getDataTypeNamesAsXML();
	return createDataTypeNamesFromXML (result, true);
    }

    //
    protected String getDataTypeNamesAsXML()
	throws MobyException {
	return (String)doCall ("retrieveObjectNames",
			       new Object[] {});
    }

    // if onlyNames == true
    //    Map: data type name -> description (String)
    // else
    //    Map: data type name -> MobyDataType[]
    //                        (filled with name, description, and lsid)
    protected Map createDataTypeNamesFromXML (String result,
					      boolean onlyNames)
	throws MobyException {

	// parse returned XML
	Map results = new TreeMap (getStringComparator());
	Document document = loadDocument (new ByteArrayInputStream (result.getBytes()));
	NodeList list = document.getElementsByTagName ("Object");
	for (int i = 0; i < list.getLength(); i++) {
	    Element elem = (Element)list.item (i);
	    String name = elem.getAttribute ("name");
	    if (name == null)
		continue;  // ignore no-named data types
	    String desc = "";
	    NodeList children = elem.getChildNodes();
	    for (int j = 0; j < children.getLength(); j++) {
		if (children.item (j).getNodeName().equals ("Description")) {
		    desc = getFirstValue (children.item (j));
		    break;
		}
	    }
	    if (onlyNames) {
		results.put (name, desc);
	    } else {
		MobyDataType dt = new MobyDataType (name);
		dt.setDescription (desc);
		dt.setLSID (elem.getAttribute ("lsid"));
		results.put (name, dt);
	    }
	}
	return results;
    }


    /**************************************************************************
     * Parses and imports the following XML. An example:
     *
     * <pre>
     * &lt;retrieveObjectDefinition&gt;
     *   &lt;objectType lsid="..."&gt;go_term&lt;/objectType&gt;
     *   &lt;Description&gt;&lt;![CDATA[A very lightweight object holding a GO term name and its definition]]&gt;&lt;/Description&gt;
     *   &lt;authURI&gt;http://www.illuminae.com&lt;/authURI&gt;
     *   &lt;contactEmail&gt;markw@illuminae.com&lt;/contactEmail&gt;
     *   &lt;Relationship relationshipType='urn:lsid:biomoby.org:objectrelation:isa'&gt;
     *      &lt;objectType articleName=''&gt;urn:lsid:biomoby.org:objectclass:object&lt;/objectType&gt;
     *   &lt;/Relationship&gt;
     *   &lt;Relationship relationshipType='urn:lsid:biomoby.org:objectrelation:hasa'&gt;
     *      &lt;objectType articleName='Term'&gt;urn:lsid:biomoby.org:objectclass:string&lt;/objectType&gt;
     *      &lt;objectType articleName='Definition'&gt;urn:lsid:biomoby.org:objectclass:string&lt;/objectType&gt;
     *   &lt;/Relationship&gt;
     *   &lt;Relationship relationshipType='urn:lsid:biomoby.org:objectrelation:has'&gt;
     *      &lt;objectType articleName='Problems'&gt;urn:lsid:biomoby.org:objectclass:string&lt;/objectType&gt;
     *      &lt;objectType articleName='Issues'&gt;urn:lsid:biomoby.org:objectclass:string&lt;/objectType&gt;
     *   &lt;/Relationship&gt;
     * &lt;/retrieveObjectDefinition&gt;
     * </pre>
     *************************************************************************/
    public MobyDataType getDataType (String dataTypeName)
	throws MobyException, NoSuccessException {

	String result = getDataTypeAsXML (dataTypeName);
	return createDataTypeFromXML (result, dataTypeName);
    }

    public MobyDataType[] getDataTypes()
	throws MobyException, NoSuccessException {
	Map<String,String> datatypeMap = getDataTypeNames();
	MobyDataType[] datatypes = new MobyDataType[datatypeMap.size()];
	int i = 0;
	for(String dataTypeName: datatypeMap.keySet()){
	    datatypes[i++] = getDataType(dataTypeName);
	}
	return datatypes;
    }

    protected String getDataTypeAsXML (String dataTypeName)
	throws MobyException, NoSuccessException {

	return (String)doCall ("retrieveObjectDefinition",
			       new Object[] {
				   "<retrieveObjectDefinition>" +
				   "<objectType>" + dataTypeName + "</objectType>" +
				   "</retrieveObjectDefinition>"
			       });
    }

    protected MobyDataType createDataTypeFromXML (String xmlSource, String dataTypeName)
	throws MobyException, NoSuccessException {

	// parse returned XML
	Document document = loadDocument (new ByteArrayInputStream (xmlSource.getBytes()));
	NodeList list = document.getElementsByTagName ("retrieveObjectDefinition");
	if (list == null || list.getLength() == 0)
	    throw new NoSuccessException ("Data Type name was not found.",
					  dataTypeName);
	MobyDataType data = null;
	Element elem = (Element)list.item (0);
	NodeList children = elem.getChildNodes();

	// first find the "real" (LSID-ized) data type name
	for (int j = 0; j < children.getLength(); j++) {
	    String nodeName = children.item (j).getNodeName();
	    if (nodeName.equals ("objectType")) {
		data = new MobyDataType (getFirstValue (children.item (j)));
		data.setLSID ( ((Element)children.item (j) ).getAttribute ("lsid"));
		break;
	    }
	}

	// if not found (unprobable) use the name given by the caller
	if (data == null)
	    data = new MobyDataType (dataTypeName);

	// now fill the data type object with the rest of attributes
	for (int j = 0; j < children.getLength(); j++) {
	    String nodeName = children.item (j).getNodeName();
	    if (nodeName.equals ("Description")) {
		data.setDescription (getFirstValue (children.item (j)));
	    } else if (nodeName.equals ("authURI")) {
		data.setAuthority (getFirstValue (children.item (j)));
	    } else if (nodeName.equals ("contactEmail")) {
		data.setEmailContact (getFirstValue (children.item (j)));
	    } else if (nodeName.equals ("Relationship")) {
		String relationshipType = ((Element)children.item (j)).getAttribute ("relationshipType");
		if (relationshipType.endsWith ("isa")) {

		    NodeList parents = children.item (j).getChildNodes();
		    for (int k = 0; k < parents.getLength(); k++) {
			if (parents.item (k).getNodeName().equals ("objectType")) {
			    data.addParentName (getFirstValue (parents.item (k)));
			}
		    }
		} else if (relationshipType.endsWith ("hasa")) {

		    NodeList belows = children.item (j).getChildNodes();
		    for (int k = 0; k < belows.getLength(); k++) {
			if (belows.item (k).getNodeName().equals ("objectType")) {
			    data.addChild ( ((Element)belows.item (k)).getAttribute ("articleName"),
					    getFirstValue (belows.item (k)),
					    Central.iHASA );
			}
		    }
		} else if (relationshipType.endsWith ("has")) {

		    NodeList belows = children.item (j).getChildNodes();
		    for (int k = 0; k < belows.getLength(); k++) {
			if (belows.item (k).getNodeName().equals ("objectType")) {
			    data.addChild ( ((Element)belows.item (k)).getAttribute ("articleName"),
					    belows.item (k).getFirstChild().getNodeValue(),
					    Central.iHAS );
			}
		    }
		}
	    }
	}
	return data;
    }

    /**************************************************************************
     *
     *************************************************************************/
    public String getServiceWSDL (String serviceName)
	throws MobyException, NoSuccessException {

	Map names = getServiceNames();

	for (Iterator it = names.entrySet().iterator(); it.hasNext(); ) {
	    Map.Entry entry = (Map.Entry)it.next();
	    if ( ((String)entry.getKey()).equals (serviceName) )
		return getServiceWSDL (serviceName, (String)entry.getValue());
	}

	throw new NoSuccessException ("Service not found.", serviceName);
    }

    /**************************************************************************
     *
     *************************************************************************/
    public String getServiceWSDL (String serviceName, String authority)
	throws MobyException, NoSuccessException {

	String cacheId = "getServiceWSDL" + serviceName + ":" + authority;
	String cachedResults = (String)getContents (cacheId);
	if (cachedResults != null)
	    return cachedResults;
	
	String result =
	    (String)doCall ("retrieveService",
			    new Object[] {
				"<retrieveService>" +
				  "<Service authURI=\"" + authority + "\" serviceName=\"" + serviceName + "\"/>" +
				"</retrieveService>"
			    });

	// parse returned XML
	Document document = loadDocument (new ByteArrayInputStream (result.getBytes()));
	Element service = document.getDocumentElement();
	Node wsdl = service.getFirstChild();
	if (wsdl == null)
	    throw new NoSuccessException ("Service not found OR WSDL is not available.",
					   serviceName + " (" + authority + ")");

	String results = wsdl.getNodeValue();
	setContents (cacheId, results);
	return results;
    }

    /*************************************************************************
     *
     *************************************************************************/
    public String getRegisterDataTypeXML (MobyDataType dataType) {

	// build the ISA tag (expressing hierarchy of data types)
	String[] names = dataType.getParentNames();
	StringBuffer buf = new StringBuffer();
	for (int i = 0; i < names.length; i++) {
	    buf.append ("<objectType>");
	    buf.append (names[i]);
	    buf.append ("</objectType>");
	    buf.append ("\n");
	}

	// build the HASA/HAS tags (expressing containments of data types)
	MobyRelationship[] children = dataType.getChildren();
	StringBuffer buf2 = new StringBuffer();  // for HASA
	StringBuffer buf3 = new StringBuffer();  // for HAS
	for (int i = 0; i < children.length; i++) {
	    if (children[i].getRelationshipType() == Central.iHASA) {
		buf2.append ("<objectType articleName=\"");
		buf2.append (children[i].getName());
		buf2.append ("\">");
		buf2.append (children[i].getDataTypeName());
		buf2.append ("</objectType>");
	    } else if (children[i].getRelationshipType() == Central.iHAS) {
		buf3.append ("<objectType articleName=\"");
		buf3.append (children[i].getName());
		buf3.append ("\">");
		buf3.append (children[i].getDataTypeName());
		buf3.append ("</objectType>");
	    }
	}

	return
	    "<registerObjectClass>" +
	    "<objectType>" + dataType.getName() + "</objectType>" +
	    "<Description><![CDATA[" + dataType.getDescription() + "]]>" +
	    "</Description>" +
	    "<Relationship relationshipType=\"ISA\">" + new String (buf) +
	    "</Relationship>" +
	    "<Relationship relationshipType=\"HASA\">" + new String (buf2) +
	    "</Relationship>" +
	    "<Relationship relationshipType=\"HAS\">" + new String (buf3) +
	    "</Relationship>" +
	    "<authURI>" + dataType.getAuthority() + "</authURI>" +
	    "<contactEmail>" + dataType.getEmailContact() + "</contactEmail>" +
	    "</registerObjectClass>";
    }

    /*************************************************************************
     *
     *************************************************************************/
    public void registerDataType (MobyDataType dataType)
	throws MobyException, NoSuccessException, PendingCurationException {

	String result =
	    (String)doCall ("registerObjectClass",
			    new Object[] { getRegisterDataTypeXML (dataType) });
	dataType.setId (checkRegistration (result, dataType)[0]);
    }

    /*************************************************************************
     * B
     *************************************************************************/
    public void unregisterDataType (MobyDataType dataType)
	throws MobyException, NoSuccessException, PendingCurationException {
	String result =
	    (String)doCall ("deregisterObjectClass",
			    new Object[] {
				"<deregisterObjectClass>" +
				  "<objectType>" + dataType.getName() + "</objectType>" +
				"</deregisterObjectClass>"
			    });
	checkRegistration (result, dataType);
    }

    /*************************************************************************
     *
     *************************************************************************/
    public String getRegisterServiceTypeXML (MobyServiceType serviceType) {

	// build the ISA tag (expressing hierarchy of service types)
	String[] names = serviceType.getParentNames();
	StringBuffer buf = new StringBuffer();
	for (int i = 0; i < names.length; i++) {
	    buf.append ("<serviceType>");
	    buf.append (names[i]);
	    buf.append ("</serviceType>");
	    buf.append ("\n");
	}

	return
	    "<registerServiceType>" +
	    "<serviceType>" + serviceType.getName() + "</serviceType>" +
	    "<contactEmail>" + serviceType.getEmailContact() + "</contactEmail>" +
	    "<authURI>" + serviceType.getAuthority() + "</authURI>" +
	    "<Description><![CDATA[" + serviceType.getDescription() + "]]>" +
	    "</Description>" +
	    "<Relationship relationshipType=\"ISA\">" + new String (buf) +
	    "</Relationship>" +
	    "</registerServiceType>";
    }

    /*************************************************************************
     *
     *************************************************************************/
    public void registerServiceType (MobyServiceType serviceType)
	throws MobyException, NoSuccessException, PendingCurationException {

	String result =
	    (String)doCall ("registerServiceType",
			    new Object[] { getRegisterServiceTypeXML (serviceType) });
	serviceType.setId (checkRegistration (result, serviceType)[0]);
    }

    /*************************************************************************
     *
     *************************************************************************/
    public void unregisterServiceType (MobyServiceType serviceType)
	throws MobyException, NoSuccessException, PendingCurationException {
	String result =
	    (String)doCall ("deregisterServiceType",
			    new Object[] {
				"<deregisterServiceType>" +
				  "<serviceType>" + serviceType.getName() + "</serviceType>" +
				"</deregisterServiceType>"
			    });
	checkRegistration (result, serviceType);
    }

    /*************************************************************************
     *
     *************************************************************************/
    public String getRegisterNamespaceXML (MobyNamespace namespace) {
	return
	    "<registerNamespace>" +
	    "<namespaceType>" + namespace.getName() + "</namespaceType>" +
	    "<contactEmail>" + namespace.getEmailContact() + "</contactEmail>" +
	    "<authURI>" + namespace.getAuthority() + "</authURI>" +
	    "<Description><![CDATA[" + namespace.getDescription() + "]]>" +
	    "</Description>" +
	    "</registerNamespace>";
    }

    /*************************************************************************
     *
     *************************************************************************/
    public void registerNamespace (MobyNamespace namespace)
	throws MobyException, NoSuccessException, PendingCurationException {
	String result =
	    (String)doCall ("registerNamespace",
			    new Object[] { getRegisterNamespaceXML (namespace) });
	namespace.setId (checkRegistration (result, namespace)[0]);
    }

    /*************************************************************************
     *
     *************************************************************************/
    public void unregisterNamespace (MobyNamespace namespace)
	throws MobyException, NoSuccessException, PendingCurationException {
	String result =
	    (String)doCall ("deregisterNamespace",
			    new Object[] {
				"<deregisterNamespace>" +
				  "<namespaceType>" + namespace.getName() + "</namespaceType>" +
				"</deregisterNamespace>"
			    });
	checkRegistration (result, namespace);
    }

    /*************************************************************************
     *
     *************************************************************************/
    public String getRegisterServiceXML (MobyService service) {
	return
	    "<registerService>" +
	    "<Category>" + service.getCategory() + "</Category>" +
	    "<serviceName>" + service.getName() + "</serviceName>" +
	    "<serviceType>" + service.getType() + "</serviceType>" +
	    "<serviceLSID>" + (service.getLSID() == null ? "" : service.getLSID().trim() )+ "</serviceLSID>" +
	    "<authURI>" + service.getAuthority() + "</authURI>" +
	    "<signatureURL>" + escapeXML (service.getSignatureURL()) + "</signatureURL>" +
	    "<URL>" + escapeXML (service.getURL()) + "</URL>" +
	    "<contactEmail>" + service.getEmailContact() + "</contactEmail>" +
	    "<authoritativeService>" + (service.isAuthoritative() ? "1" : "0") + "</authoritativeService>" +
	    "<Description><![CDATA[" + service.getDescription() + "]]>" +
	    "</Description>" +
	    buildPrimaryInputTag (service) + 
	    buildSecondaryInputTag (service) + 
	    buildOutputTag (service) + 
	    "</registerService>";
    }

    /*************************************************************************
     *
     *************************************************************************/
    public void registerService (MobyService service)
	throws MobyException, NoSuccessException, PendingCurationException {

	String result =
	    (String)doCall ("registerService",
			    new Object[] { getRegisterServiceXML (service) });
	String[] registered = checkRegistration (result, service);
	service.setId (registered [0]);
	service.setRDF (registered [1]);
	String pathToRDF = service.getPathToRDF();
	if ( ! pathToRDF.equals ("") ) {
	    File fileRDF = new File (pathToRDF);
	    try {
		PrintStream fileout = new PrintStream (new FileOutputStream (fileRDF));
		fileout.println (registered [1]);
		fileout.close();
	    } catch (IOException e) {
		StringBuffer buf = new StringBuffer (100);
		buf.append ("Failed to save RDF in '");
		buf.append (fileRDF.getAbsolutePath() + "'. ");
		buf.append (e.toString());
		try {
		    File tmpFile = File.createTempFile (service.getName() + "-", ".rdf");
		    PrintStream fileout = new PrintStream (new FileOutputStream (tmpFile));
		    fileout.println (registered [1]);
		    fileout.close();
		    buf.append ("\nReturned RDF file was therefore stored in: ");
		    buf.append (tmpFile.getAbsolutePath());
		} catch (IOException e2) {
		    buf.append ("\nEven saving in a temporary file failed: ");
		    buf.append (e2.toString());
		}
		throw new MobyException (buf.toString());
	    }
	}
    }

    /*************************************************************************
     *
     *************************************************************************/
    public void unregisterService (MobyService service)
	throws MobyException, NoSuccessException, PendingCurationException {
	String result =
	    (String)doCall ("deregisterService",
			    new Object[] {
				"<deregisterService>" +
				  "<authURI>" + service.getAuthority() + "</authURI>" +
				  "<serviceName>" + service.getName() + "</serviceName>" +
				"</deregisterService>"
			    });
	checkRegistration (result, service);
    }

    /**************************************************************************
     *
     *************************************************************************/
    public MobyService[] findService (String serviceType)
	throws MobyException {
	if (serviceType == null)
	    return new MobyService[] {};
	MobyService pattern = new MobyService ("dummy");
	pattern.setCategory ("");
	pattern.setType (serviceType);
	return findService (pattern, null);
    }

    /**************************************************************************
     *
     *************************************************************************/
    public MobyService[] findService (String[] keywords)
	throws MobyException {
	if (keywords == null)
	    return new MobyService[] {};
	return findService (null, keywords);
    }

    /**************************************************************************
     *
     *************************************************************************/
    public MobyService[] findService (MobyService pattern)
	throws MobyException {
	if (pattern == null)
	    return new MobyService[] {};
	return findService (pattern, null);
    }

    /**************************************************************************
     *
     *************************************************************************/
    public MobyService[] findService (MobyService pattern, String[] keywords)
	throws MobyException {
	return findService (pattern, keywords, true, true);
    }

    /**************************************************************************
     * All 'findService' methods end up here.
     *************************************************************************/
    public MobyService[] findService (MobyService pattern, String[] keywords,
				      boolean includeChildrenServiceTypes,
				      boolean includeParentDataTypes)
	throws MobyException {
	if (pattern == null) {
	    pattern = new MobyService ("dummy");
	    pattern.setCategory ("");
	}

	String result =
	    getServicesAsXML (pattern, keywords, includeChildrenServiceTypes, includeParentDataTypes);
	MobyService[] services = extractServices (result);
	return services;
    }

    // ...actually all 'findService' methods end up here
    protected String getServicesAsXML (MobyService pattern, String[] keywords,
				       boolean includeChildrenServiceTypes,
				       boolean includeParentDataTypes)
	throws MobyException {
	String[] query = new String[] { 
				"<findService>" +
				buildQueryObject (pattern, keywords,
						  includeParentDataTypes,
						  includeChildrenServiceTypes,
						  false) + 
				"</findService>"
	};
	return (String)doCall ("findService", query);
    }

    /**************************************************************************
     *
     *************************************************************************/
    public String call (String methodName, String inputXML)
	throws MobyException {
	Object result;
	if (inputXML == null || inputXML.equals (""))
	    result = doCall (methodName, new Object[] { });
	else 
	    result = doCall (methodName, new Object[] { inputXML });
	return (String)result;
    }

    /**************************************************************************
     *
     *************************************************************************/
    protected static String resultToString (Object result)
	throws MobyException {
 	if (result == null)
 	    throw new MobyException ("Returned result is null.");
	if (result instanceof String)
	    return (String)result;
	if (result instanceof String[]) {
	    String[] tmp = (String[])result;
	    StringBuffer buf = new StringBuffer();
	    for (int i = 0; i < tmp.length; i++)
		buf.append (tmp[i]);
	    return new String (buf);
	}
	if (result instanceof byte[])
	    return new String ((byte[])result);

	throw new MobyException ("Unknown type of result: " + result.getClass().getName());
    }

    /**************************************************************************
     *
     *************************************************************************/
    public boolean setDebug (boolean enabled) {
	boolean oldMode = debug;
	debug = enabled;
	return oldMode;
    }

    /**************************************************************************
     * Parses and imports the following XML.
     * <pre>
     * &lt;Relationships&gt;
     *   &lt;Relationship relationshipType='urn:lsid:biomoby.org:servicerelation:isa'&gt;
     *     &lt;serviceType&gt;urn:lsid:biomoby.org:servicetype:analysis&lt;/serviceType&gt;
     *     &lt;serviceType&gt;urn:lsid:biomoby.org:servicetype:service&lt;/serviceType&gt;
     *   &lt;/Relationship&gt;
     * &lt;/Relationships&gt;
     * </pre>
     *************************************************************************/
    public String[] getServiceTypeRelationships (String serviceTypeName,
						 boolean expand)
	throws MobyException {
	String result = getServiceTypeRelationshipsAsXML (serviceTypeName, expand);
	return createServiceTypeRelationshipsFromXML (result);
    }

    //
    protected String getServiceTypeRelationshipsAsXML (String serviceTypeName,
						       boolean expand)
	throws MobyException {
	return
	    (String)doCall ("Relationships",
			    new Object[] {
				"<Relationship>" +
				"<serviceType>" + serviceTypeName + "</serviceType>" +
				"<relationshipType>" + Central.ISA + "</relationshipType>" +
				"<expandRelationship>" + (expand ? "1" : "0") + "</expandRelationship>" +
				"</Relationship>"
			    });
    }

    //
    protected String[] createServiceTypeRelationshipsFromXML (String result)
	throws MobyException {

	// parse returned XML
	Vector<String> v = new Vector<String>();
	Document document = loadDocument (new ByteArrayInputStream (result.getBytes()));
	NodeList list = document.getElementsByTagName ("Relationship");
	for (int i = 0; i < list.getLength(); i++) {
	    Element elem = (Element)list.item (i);
	    NodeList children = elem.getChildNodes();
	    for (int j = 0; j < children.getLength(); j++) {
		if (children.item (j).getNodeName().equals ("serviceType")) {
		    v.addElement (getFirstValue (children.item (j)));
		}
	    }
	}
	String[] results = new String [v.size()];
	v.copyInto (results);
	return results;
    }

    /**************************************************************************
     * Parses and imports the following XML.
     * <pre>
     *&lt;Relationships&gt;
     *  &lt;Relationship relationshipType='urn:lsid:biomoby.org:objectrelation:isa'&gt;
     *    &lt;objectType&gt;urn:lsid:biomoby.org:objectclass:virtualsequence&lt;/objectType&gt;
     *    &lt;objectType&gt;urn:lsid:biomoby.org:objectclass:object&lt;/objectType&gt;
     *  &lt;/Relationship&gt;
     *  &lt;Relationship relationshipType='urn:lsid:biomoby.org:objectrelation:hasa'&gt;
     *    &lt;objectType&gt;urn:lsid:biomoby.org:objectclass:string&lt;/objectType&gt;
     *    &lt;objectType&gt;urn:lsid:biomoby.org:objectclass:integer&lt;/objectType&gt;
     *  &lt;/Relationship&gt;
     *&lt;/Relationships&gt;
     * </pre>
     *
     * Added at Sun Feb 19 19:32:31 PHT 2006: it recognizes also an
     * attributes 'lsid' and 'articleName' in &lt;objectType&gt; element.
     *************************************************************************/
    public Map getDataTypeRelationships (String dataTypeName)
	throws MobyException {

	String cacheId = "getDataTypeRelationships_" + dataTypeName;
	Map cachedResults = (Map)getContents (cacheId);
	if (cachedResults != null)
	    return cachedResults;
	
	String result =
	    (String)doCall ("Relationships",
			    new Object[] {
				"<Relationships>" +
				"<objectType>" + dataTypeName + "</objectType>" +
				"<relationshipType>" + Central.ISA + "</relationshipType>" +
				"<relationshipType>" + Central.HASA + "</relationshipType>" +
				"<relationshipType>" + Central.HAS + "</relationshipType>" +
				"<expandRelationship>1</expandRelationship>" +
				"</Relationships>"
			    });

	// parse returned XML
	Map results = new HashMap();
	Document document = loadDocument (new ByteArrayInputStream (result.getBytes()));
	NodeList list = document.getElementsByTagName ("Relationship");

	for (int i = 0; i < list.getLength(); i++) {
	    Element elem = (Element)list.item (i);
	    String relType = elem.getAttribute ("relationshipType");
	    NodeList children = elem.getChildNodes();
	    Vector<String> v = new Vector<String>();
	    for (int j = 0; j < children.getLength(); j++) {
		if (children.item (j).getNodeName().equals ("objectType")) {
		    v.addElement (getFirstValue (children.item (j)));
		}
	    }
	    String[] names = new String [v.size()];
	    v.copyInto (names);
	    results.put (relType, names);
	}

	setContents (cacheId, results);
	return results;
    }

    /**************************************************************************
     * Parses and imports the following XML.
     * <pre>
     *&lt;Relationships&gt;
     *  &lt;Relationship relationshipType='urn:lsid:biomoby.org:objectrelation:isa'&gt;
     *    &lt;objectType&gt;urn:lsid:biomoby.org:objectclass:virtualsequence&lt;/objectType&gt;
     *    &lt;objectType&gt;urn:lsid:biomoby.org:objectclass:object&lt;/objectType&gt;
     *  &lt;/Relationship&gt;
     *&lt;/Relationships&gt;
     * </pre>
     *************************************************************************/
    public String[] getDataTypeRelationships (String dataTypeName,
					      String relationshipType)
	throws MobyException {

	String cacheId = "getDataTypeRelationships_" + dataTypeName + ":" + relationshipType;
	String[] cachedResults = (String[])getContents (cacheId);
	if (cachedResults != null)
	    return cachedResults;

	String result =
	    (String)doCall ("Relationships",
			    new Object[] {
				"<Relationships>" +
				"<objectType>" + dataTypeName + "</objectType>" +
				"<relationshipType>" + relationshipType + "</relationshipType>" +
				"<expandRelationship>1</expandRelationship>" +
				"</Relationships>"
			    });

	// parse returned XML
	Vector<String> v = new Vector<String>();
	Document document = loadDocument (new ByteArrayInputStream (result.getBytes()));
	NodeList list = document.getElementsByTagName ("Relationship");

	// it should always be just one element in this list
	for (int i = 0; i < list.getLength(); i++) {
	    Element elem = (Element)list.item (i);
	    NodeList children = elem.getChildNodes();
	    for (int j = 0; j < children.getLength(); j++) {
		if (children.item (j).getNodeName().equals ("objectType")) {
		    v.addElement (getFirstValue (children.item (j)));
		}
	    }
	}
	String[] results = new String [v.size()];
	v.copyInto (results);

	setContents (cacheId, results);
	return results;
    }

//     /**************************************************************************
//      *
//      *************************************************************************/
//     public MobyRelationship[] getRelationships (String dataTypeName)
// 	throws MobyException {
// 	return null;
//     }


    /**************************************************************************
     *
     *************************************************************************/
    public String getRegistryEndpoint() {
	return endpoint.toString();
    }

    /**************************************************************************
     *
     *************************************************************************/
    public String getRegistryNamespace() {
	return uri;
    }

    /**************************************************************************
     * Parses and imports the following XML.
     * <pre>
     * &lt;resourceURLs&gt;
     *   &lt;Resource name="Service"         url="..." /&gt;
     *   &lt;Resource name="Object"          url="..." /&gt;
     *   &lt;Resource name="Namespace"       url="..." /&gt;
     *   &lt;Resource name="ServiceInstance" url="..." /&gt;
     *   &lt;Resource name="Full"            url="..." /&gt;
     * &lt;/resourceURLs&gt;
     * </pre>
     *************************************************************************/
    public MobyResourceRef[] getResourceRefs()
	throws MobyException {

	String cacheId = "retrieveResourceURLs";
	MobyResourceRef[] cachedResults = (MobyResourceRef[])getContents (cacheId);
	if (cachedResults != null)
	    return cachedResults;

	String result = (String)doCall ("retrieveResourceURLs",
					new Object[] {});

	// parse returned XML
	Vector<MobyResourceRef> v = new Vector<MobyResourceRef>();
	Document document = loadDocument (new ByteArrayInputStream (result.getBytes()));
	NodeList list = document.getElementsByTagName ("Resource");
	for (int i = 0; i < list.getLength(); i++) {
	    Element elem = (Element)list.item (i);
	    try {
		v.addElement
		    (new MobyResourceRef (elem.getAttribute ("name"),
					  new URL ((String)elem.getAttribute ("url")),
					  elem.getAttribute ("type")));
	    } catch (MalformedURLException e2) {
		if (debug)
		    System.err.println ("Bad URL: " + elem.getAttribute ("url"));
	    }
	}

	MobyResourceRef[] results = new MobyResourceRef [v.size()];
	v.copyInto (results);

	// Add this data to the cache in case we get called again
	setContents (cacheId, results);

	return results;
    }

    /**************************************************************************
     *
     *************************************************************************/
    public InputStream getResource (String resourceName)
	throws MobyException {

	MobyResourceRef[] resourceRefs = getResourceRefs();
	for (int i = 0; i < resourceRefs.length; i++) {
	    if (resourceName.equalsIgnoreCase (resourceRefs[i].getResourceName())) {
		return Utils.getInputStream (resourceRefs[i].getResourceLocation());
	    }
	}
	throw new MobyException ("No resource found for '" + resourceName + "'.");
    }

   /**************************************************************************
     * Return a case-insensitive comparator of Strings. It is used to
     * create various TreeMaps where keys are strings.
     *************************************************************************/
    protected static Comparator getStringComparator() {
	return new Comparator() {
		public int compare (Object o1, Object o2) {
		    return ((String)o1).compareToIgnoreCase ((String)o2);
		}
	    };
    }
    
    // cache URL/URI so we only check once 
    private static String CHECKED_URL = null;
    private static String CHECKED_URI = null;
    
    /**
     * Using this method to get a Central object will ensure that other parts of the org.biomoby.shared 
     * class hierarchy that implicitly check the registry will use the same cache.  Otherwise, methods
     * such as MobyNamespace.getNamespace() must be passed a Central object parameter as well.
     * The actual CentralImpl subclass returned is based on info from META-INF/org.biomoby.shared.CentralDefaultImpl
     * or failing that the class specified in DEFAULT_CENTRAL_IMPL_CLASSNAME
     * @return a CentralImpl using the default Central URI, and currently a class implementing a caching mechanism
     */
    public static CentralImpl getDefaultCentral() throws MobyException{
	return getDefaultCentral(null);
    }

    public static CentralImpl getDefaultCentral(Registry reg) throws MobyException{
	if(reg == null && defaultCentrals.containsKey("")){
	    return defaultCentrals.get("");
	}
	else if(reg != null && defaultCentrals.containsKey(reg.getEndpoint())){
	    return defaultCentrals.get(reg.getEndpoint());
	}

	String className = DEFAULT_CENTRAL_IMPL_CLASSNAME;
	ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
	URL resURL = classLoader.getResource("META-INF/services/"+CENTRAL_IMPL_RESOURCE_NAME);
	if(resURL != null){
	    logger.log(Level.CONFIG, "Loading "+resURL);
	    try{
		LineNumberReader reader = new LineNumberReader(new InputStreamReader(resURL.openStream()));
		for(String line = reader.readLine(); line != null; line = reader.readLine()){
		    if(!line.trim().startsWith("#")){
			className = line.trim();
			break;
		    }
		}
	    } catch(Exception e){
		logger.log(Level.WARNING,
			   "Error reading " + resURL,
			   e);
	    }
	}
	try{
	    logger.log(Level.CONFIG, "Central class is  "+className+ " for " + reg);
	    Class clazz = Class.forName(className);
	    if(reg == null){  // should use default nullary c-tor
		defaultCentrals.put("", (CentralImpl) clazz.newInstance());
	    }
	    else{  // should have (String endpoint, String namespace) c-tor
		for(Constructor ctor: clazz.getDeclaredConstructors()){
		    Class[] params = ctor.getParameterTypes();
		    if(params.length == 2 && params[0].getName().equals("java.lang.String") &&
		       params[1].getName().equals("java.lang.String") ){
			defaultCentrals.put(reg.getEndpoint(),
					    (CentralImpl) ctor.newInstance(reg.getEndpoint(), reg.getNamespace()));
			break;
		    }
		}
		if(!defaultCentrals.containsKey(reg.getEndpoint())){
		    logger.log(Level.WARNING,
			       "Could not find required (String endpoint, String namespace)" +
			       "constructor for class " + className);
		}
	    }
	} catch(Exception e){
	    logger.log(Level.WARNING,
		       "Could not load class " + className,
		       e);
	    if(reg == null){
		defaultCentrals.put("", new CentralImpl());  //fallback to this class, no caching, etc.
	    }
	    else{
		defaultCentrals.put(reg.getEndpoint(), 
				    new CentralImpl(reg.getEndpoint(), reg.getNamespace()));
	    }
	}

	return defaultCentrals.get(reg == null ? "" : reg.getEndpoint());
    }

    /**
     * 
     * @return a String representing the Default mobycentral endpoint. If the
     *         system property 'moby.check.default' exists and is set to true,
     *         then the URL http://biomoby.org/mobycentral is queried and the
     *         default central endpoint is returned, otherwise DEFAULT_ENDPOINT
     *         is returned.
     */
    public static String getDefaultURL() {
	boolean check = false;
	try {
	    check = Boolean.getBoolean("moby.check.default");
	} catch (Exception e) {
	    
	}
	
	if (check) {
	    // return the last checked url if we have done this before
	    if (CHECKED_URL != null && CHECKED_URL.trim() != "") {
		return CHECKED_URL; 
	    }
	    
	    // create a HttpClient object
	    HttpClient client = new HttpClient();
	    // set up the Head method
	    HeadMethod method = new HeadMethod("http://biomoby.org/mobycentral");
	    // do not follow redirects or we will get a 411 error
	    method.setFollowRedirects(false);
	    // retry 3 times
	    method.getParams().setParameter(HttpMethodParams.RETRY_HANDLER,new DefaultHttpMethodRetryHandler(3, false));
	    // set the user agent ... should probably make this something more reasonable
	    method.getParams().setParameter(HttpMethodParams.USER_AGENT,"jMoby/1.0");
	    try {
		// Execute the method.
		int statusCode = client.executeMethod(method);

		if (statusCode != HttpStatus.SC_MOVED_PERMANENTLY) {
		    System.err.println("Method failed: "
			    + method.getStatusLine());
		} else {
		    try {
			String location = method.getResponseHeader("location").getValue();
			CHECKED_URL = location;
			try {
			    CHECKED_URI = "http://" + (new URL(CHECKED_URL).getAuthority()) + "/MOBY/Central";
			} catch (MalformedURLException murle ) {
			    CHECKED_URI = DEFAULT_NAMESPACE;
			}
			return CHECKED_URL;
		    } catch (NullPointerException npe) {
			return DEFAULT_ENDPOINT;
		    }
		}
	    } catch (HttpException e) {
		System.err.println("Fatal protocol violation: "
			+ e.getMessage());
		e.printStackTrace();
	    } catch (IOException e) {
		System.err.println("Fatal transport error: " + e.getMessage());
		e.printStackTrace();
	    } finally {
		// Release the connection.
		method.releaseConnection();
	    } 
	    
	} else {
	    return DEFAULT_ENDPOINT;
	}
	return DEFAULT_ENDPOINT;
    }
    
    /**
     * 
     * @return a String representing the default mobycentral uri. If the
     *         system property 'moby.check.default' exists and is set to true,
     *         then the URL http://biomoby.org/mobycentral is queried and the
     *         default central namespace is returned, otherwise DEFAULT_NAMESPACE
     *         is returned.
     */
    public static String getDefaultURI() {
	boolean check = false;
	try {
	    check = Boolean.getBoolean("moby.check.default");
	} catch (Exception e) {
	    
	}
	if (check) {
	    if (CHECKED_URI != null && CHECKED_URI.trim() != "") {
		return CHECKED_URI; 
	    }
	    // need to check ... 
	    getDefaultURL();
	    return CHECKED_URI;
	} else {
	    return DEFAULT_NAMESPACE;
	}
    }
    /**************************************************************************
     * Convert non-suitable characters in a XML string into their
     * entity references. <p>
     *
     * <em>Adapted from jDom.</em>
     *
     * @param str input to be converted
     * @return If there were any non-suitable characters, return a new
     * string with those characters escaped, otherwise return the
     * unmodified input string
     *
     *************************************************************************/
    public String escapeXML (String str) {
        StringBuffer buffer = null;
        char ch;
        String entity;
        for (int i = 0; i < str.length(); i++) {
            ch = str.charAt (i);
            switch (ch) {
	    case '<' :
		entity = "&lt;";
		break;
	    case '>' :
		entity = "&gt;";
		break;
	    case '&' :
		entity = "&amp;";
		break;
	    default :
		entity = null;
		break;
            }
            if (buffer == null) {
                if (entity != null) {
                    // An entity occurred, so we'll have to use StringBuffer
                    // (allocate room for it plus a few more entities).
                    buffer = new StringBuffer (str.length() + 20);
                    // Copy previous skipped characters and fall through
                    // to pickup current character
                    buffer.append (str.substring (0, i));
                    buffer.append (entity);
                }
            } else {
                if (entity == null) {
                    buffer.append (ch);
                } else {
                    buffer.append (entity);
                }
            }
        }

        // If there were any entities, return the escaped characters
        // that we put in the StringBuffer. Otherwise, just return
        // the unmodified input string.
        return (buffer == null) ? str : buffer.toString();
    }

    /*************************************************************************
     * Format an exception.
     *************************************************************************/
    public static String formatFault (AxisFault e, String endpoint, QName method) {
	ByteArrayOutputStream baos = new ByteArrayOutputStream();
	formatFault (e, new PrintStream (baos), endpoint, method);
	return baos.toString();
    }

    /*************************************************************************
     * Format an exception.
     *************************************************************************/
    public static void formatFault (AxisFault e, PrintStream out,
				    String endpoint, QName method) {
	    
	out.println ("===ERROR===");
	out.println ("Fault details:");
	// for some obvious errors I do not print all details (with a lenghty trace stack)
	String faultString = e.getFaultString();
	if ( (! faultString.startsWith ("java.net.ConnectException")) &&
	     (faultString.indexOf ("Could not find class for the service named:") == -1)
	     ) {
	    org.w3c.dom.Element[] details = e.getFaultDetails();
	    for (int i = 0; i < details.length; i++) {
		String s = details[i].toString().replaceAll ("&lt;", "<");
		s = s.replaceAll ("&gt;", ">");
		out.println (s);
	    }
	}
	out.println ("Fault string: " + faultString);
	out.println ("Fault code:   " + e.getFaultCode());
	out.println ("Fault actor:  " + e.getFaultActor());
	if (endpoint != null || method != null)
	    out.println ("When calling:");
	if (endpoint != null)
	    out.println ("\t" + endpoint);
	if (method != null)
	    out.println ("\t" + method);
	out.println ("===========");
    }


}
