
package org.biomoby.service;
import org.biomoby.service.test.TestServletConfig;
import org.biomoby.service.test.TestServletContext;

import org.biomoby.registry.meta.Registry;
import org.biomoby.shared.*;
import org.biomoby.shared.data.*;
import org.biomoby.shared.parser.*;
import org.biomoby.client.CentralImpl;
import org.biomoby.client.MobyRequest;
import org.biomoby.client.rdf.builder.*;

import com.hp.hpl.jena.rdf.model.*;

import java.rmi.Remote;  // for Oracle Servlets
import javax.servlet.http.*;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import javax.xml.soap.*;
import java.util.*;
import java.math.*;

/**
 * This the base implementation of a MOBY servlet that can be 
 * easily extended to provide a meaningful service (by overriding
 * processRequest()).  Please see
 * the documentation on <a href="http://biomoby.open-bio.org/CVS_CONTENT/moby-live/Java/docs/deployingServices.html">how to use this servlet</a>.
 */

@mobyService(name="MobyServlet",
	     type="Testing", 
	     provider="moby.ucalgary.ca", 
	     author="gordonp@ucalgary.ca",
	     in={},
	     out={},
             description={"No-operation base service implementation"})
public class MobyServlet extends HttpServlet implements Remote{

    public static final String MOBY_CENTRAL_URL_PARAM = "mobyCentralURL";
    public static final String MOBY_AUTHORITATIVE_PARAM = "mobyAuthoritative";
    public static final String MOBY_CONTACT_PARAM = "mobyAuthorContact";
    public static final String MOBY_SERVICE_DESC_PARAM = "mobyServiceDescription";
    public static final String MOBY_PROVIDER_URI_PARAM = "mobyProviderURI";
    public static final String MOBY_SERVICETYPE_PARAM = "mobyServiceType";
    public static final String MOBY_SERVICENAME_PARAM = "mobyServiceName";
    public static final String MOBY_INPUT_PARAM = "mobyInput";
    public static final String MOBY_SECONDARYINPUT_PARAM = "mobySecondaryInput";
    public static final String MOBY_OUTPUT_PARAM = "mobyOutput";
    public static final String MODE_HTTP_PARAM = "mode";
    public static final String RDF_MODE = "rdf";
    public static final String ADMIN_MODE = "admin";
    public static final int INIT_OUTPUT_BUFFER_SIZE = 100000;

    private static boolean shouldExit = true;

    protected static MobyRequest mobyRequest;
    protected static DocumentBuilder docBuilder;
    protected static ServiceInstanceRDF serviceInstanceRDF;

    // Members used to wrap the response in SOAP
    protected static MessageFactory soapMessageFactory;
    protected static Name bodyName;
    protected static Name bodyContentsName;
    protected static Name stringEncAttrName;
    protected final static String stringEncAttrValue = "xsd:string";
    protected static Name faultName;
    protected static Registry registry; 

    protected MobyService thisService;
    protected boolean isInitialized = false;
    protected boolean hasDataTypeTemplates = false;  // Will we need to consider runtime changes to data type specs?
    /** Changing this value makes logging more or less verbose */
    protected boolean isDebug = false;
    /** Where data type, namespace and service type definition come from, if null, the default registry is used */

    // Only use for keep track of MobyServices if they have template values that are filled in at service invocation
    protected Map<Thread,MobyService> templatedServicesMap; 
    protected Map<Thread,MobyContentInstance> responseContentInstanceMap; 
    protected Map<String,String> extraConfig;

    public void doGet(HttpServletRequest request,
		      HttpServletResponse response)
	throws ServletException, java.io.IOException{
	if(thisService == null){
	    try{
		thisService = createServiceFromConfig(request);
	    } catch(Exception e){
		log("While attempting to configure service on first run:", e);
		throw new ServletException("While attempting to configure service on first run: " + e);
	    }
	}

	// Depending on the argument given, print the RDF signature, 
	// the admin page, or a Web interface to the program
	String mode = request.getParameter(MODE_HTTP_PARAM);
	if(mode == null){  // Normal web-browser form fill-in
	    writeHTMLForm(request, response);
	}
	else if(mode.equals(RDF_MODE)){
	    writeRDF(response);
	}
	else{  // Unrecognized, print form
	    writeHTMLForm(request, response);
	}
    }

    protected void writeHTMLForm(HttpServletRequest request,
				 HttpServletResponse response){
	java.io.OutputStream out = null;
	response.setContentType("text/html");
	try{
	    out = response.getOutputStream();
	}
	catch(java.io.IOException ioe){
	    log("While getting servlet output stream (for HTML form response to client)", ioe);
	    return;
	}

	try{
	    out.write("<html><body>This is where the HTML form would go</body></html>".getBytes());
	}
	catch(java.io.IOException ioe){
	    log("While printing HTML form to servlet output stream", ioe);
	    return;
	}
    }

    protected void writeRDF(HttpServletResponse response){
	java.io.OutputStream out = null;
	response.setContentType("text/xml");
	try{
	    out = response.getOutputStream();
	}
	catch(java.io.IOException ioe){
	    log("While getting servlet output stream (for RDF response to client)", ioe);
	    return;
	}

        boolean USE_LSIDS = true;
	Model model = serviceInstanceRDF.createRDFModel(ModelFactory.createDefaultModel(), 
							new MobyService[]{thisService},
                                                        USE_LSIDS);
	FilteredStream stream = new FilteredStream(new java.io.ByteArrayOutputStream(5000));
	RDFWriter writer = model.getWriter("RDF/XML-ABBREV");
	writer.setProperty("showXmlDeclaration", "false");
	writer.setProperty("tab", "4");
	writer.write(model, stream, null);
	
	try{
	    out.write(stream.getOutput().getBytes());
	}
	catch(java.io.IOException ioe){
	    log("While printing service RDF to servlet output stream", ioe);
	    return;
	}
    }

    public void doPost(HttpServletRequest request,
		       HttpServletResponse response)
	throws ServletException, java.io.IOException{

	if(thisService == null){
	    try{
		thisService = createServiceFromConfig(request);
	    } catch(Exception e){
		throw new ServletException("While attempting to configure service on first run: " + e);
	    }
	}

	// Check that the right method call was made
	// Run this after the config, because a subclass might not know the 
	// proper SOAP endpoint name until the first call has been made
	if(!validSOAPEndPoint(request, response)){
	    return;
	}

	MobyContentInstance mobyResultContents = new MobyContentInstance();
	try{
	    MobyContentInstance mobyRequestContents = getMobyContents(request);
	    
	    // Create as many output slots (blank) as there were input slots
	    // According to the MOBY spec, there must be a 1:1 relationship here
	    for(String jobName : mobyRequestContents.keySet()){
		MobyDataJob result = new MobyDataJob();
		result.setID(jobName);
		mobyResultContents.put(jobName, result);
	    }
	    mobyResultContents.setServiceAuthorityURI(thisService.getAuthority());
	    
	    // It should populate the mobyResultContents.
	    processRequests(mobyRequestContents, mobyResultContents);

	} catch(Exception e){
	    addException(mobyResultContents, e);
	} finally{
	    writeResponse(response, mobyResultContents);
	}
    }

    /**
     * The SOAP action called must have the same name as the service (which is the same as the servlet name).
     */
    protected boolean validSOAPEndPoint(HttpServletRequest request, 
					HttpServletResponse response){
	String methodName = "http://biomoby.org/#"+getServiceName();
	String qMethodName = "\""+methodName+"\"";
	String requestedActionName = null;
	boolean endPointMatches = false;
	// TODO: This is where we should add code to deal with asynchronous calls
	for(java.util.Enumeration actionsCalled = request.getHeaders("SOAPAction");
	    actionsCalled.hasMoreElements();){

	    requestedActionName = (String) actionsCalled.nextElement();

	    if(methodName.equals(requestedActionName) ||
	       qMethodName.equals(requestedActionName)){
		endPointMatches = true;
	    }
	}
	
	if(!endPointMatches){
	    // throw a SOAP fault
	    java.io.OutputStream out = null;
	    try{
		out = response.getOutputStream();
	    }
	    catch(java.io.IOException ioe){
		log("While getting servlet output stream (for SOAP fault response to client)", ioe);
		return false;
	    }

	    response.setContentType("text/xml");
	    try{
		String errMsg = "The SOAPAction header must match the MOBY Service name (" +
		    methodName + ", but was '" + requestedActionName + "'";
		log(errMsg);

		// The SOAP envelope (headerless) consists of a body with one string element inside it
		//PG SOAPMessage message = soapMessageFactory.createMessage();
		//PG SOAPBodyElement bodyElement = message.getSOAPBody().addFault(bodyName, errMsg);
		//PG message.writeTo(out);
		out.write(("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"+
			   "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" " +
			   "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" " +
			   "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" +
			   "<soapenv:Body><soapenv:Fault><soapenv:faultcode>Client</soapenv:faultcode>" +
			   "<soapenv:faultstring>"+errMsg+"<soapenv:faultactor/><soapenv:detail/>"+
			   "</soapenv:faultstring></soapenv:Fault></soapenv:Body></soapenv:Envelope>").getBytes());

		return false;
		
	    } catch(Exception e){
		log("While writing SOAP fault response to client for " + getServiceName(), e);
		return false;
	    }
	}

	// If we got here, we found the matching endpoint
	return true;
    }

    public MobyContentInstance getMobyContents(HttpServletRequest request) throws Exception{
	// Turn the POST body into an XML DOM
	ServletInputStream bodyInputStream = request.getInputStream();

	StringBuffer reqString = new StringBuffer();
	byte[] buffer = new byte[10000];
	for(int read = bodyInputStream.readLine(buffer, 0, buffer.length); 
	    read != -1;
	    read = bodyInputStream.readLine(buffer, 0, buffer.length)){
	    reqString.append(new String(buffer, 0, read));
	}
	//System.err.println("Printing request");
	//System.err.println(reqString.toString());

	org.w3c.dom.Document bodyDoc = null;
	// Synchronized to avoid Xerces exception FWK005: concurrent parsing is disallowed
	synchronized(docBuilder){
	    //	    bodyDoc = docBuilder.parse(bodyInputStream);
     	    bodyDoc = docBuilder.parse(new java.io.ByteArrayInputStream(reqString.toString().getBytes()));
	}

	// Parse the SOAP data envelope to get just the MOBY part
	MobyContentInstance mobyRequestContents = null;
	org.w3c.dom.Element mobyEnvelopeDOM = mobyRequest.decodeSOAPMessage(bodyDoc.getDocumentElement(),
									    //getServiceName(),
									    null, 
									    null);
	
	mobyRequestContents = MobyDataUtils.fromXMLDocument(mobyEnvelopeDOM, registry); 

	return mobyRequestContents;
    }

    /**
     * Throws an exception if the data in the job does not match the input spec of the service.
     */
    public static void validateArguments(MobyDataJob job, 
					 MobyService service, 
					 String errMessagePrefix) throws Exception{
	MobyPrimaryData[] template = service.getPrimaryInputs();

	MobyDataInstance[] values = job.getPrimaryData();
	if(values.length != template.length){
	    throw new IllegalArgumentException(errMessagePrefix + 
					       ": wrong number of primary values (" + values.length + 
					       ") for job '" + job.getID() + "', should be " + 
					       template.length);
	}

	for(int i = 0; i < template.length; i++){
	    MobyPrimaryData param = template[i];
	    MobyDataInstance nameMatch = job.get(param.getName());
	    if(nameMatch == null){
                MobyContentInstance mci = new MobyContentInstance();
                mci.put(job);
                MobyDataUtils.toXMLDocument(System.err, mci);
		throw new IllegalArgumentException(errMessagePrefix + 
					       ": required primary value \"" + param.getName() + 
					       "\" in job " + job.getID() + " is missing");
	    }
	    if(!(nameMatch instanceof MobyPrimaryData)){
		throw new IllegalArgumentException(errMessagePrefix + 
					       ": value \"" + param.getName() + 
					       "\" in job " + job.getID() + 
					       " is not a primary value as expected");
	    }
	    MobyPrimaryData match = (MobyPrimaryData) nameMatch;

	    if(param instanceof MobyPrimaryDataSimple && !(match instanceof MobyPrimaryDataSimple)){
		throw new IllegalArgumentException(errMessagePrefix + 
					       ": value \"" + param.getName() + 
					       "\" in job " + job.getID() + 
					       " is not a simple value as expected");
	    }
	    if(param instanceof MobyPrimaryDataSet && !(match instanceof MobyPrimaryDataSet)){
		throw new IllegalArgumentException(errMessagePrefix + 
					       ": value \"" + param.getName() + 
					       "\" in job " + job.getID() + 
					       " is not a collection as expected");
	    }	       

	    MobyDataType valueType = match.getDataType();
	    if(valueType == null){
		throw new IllegalArgumentException(errMessagePrefix + 
						   ": the value '" + param.getName() + 
						   "' in job " + job.getID() + 
						   " has a null datatype (not in the " +
						   "MOBY registry)!");
	    }

	    if(!valueType.equals(param.getDataType()) && 
	       !valueType.inheritsFrom(param.getDataType())){
		throw new IllegalArgumentException(errMessagePrefix + 
						   ": the value '" + param.getName() + 
						   "' in job " + job.getID() + 
						   " has the wrong datatype (does not " +
						   "inherit from " + 
						   param.getDataType().getName()+
						   " in the MOBY Central object ontology)");
	    }
	    if(param.getDataType() instanceof MobyDataTypeTemplate){
		// First instance. The runtime data type constraint is being defined here,
		// i.e. the effective datatype for output parameters using the same template
		// object is now set.
		MobyDataTypeTemplate dtTemplate = (MobyDataTypeTemplate) param.getDataType();
		// Do not change the global definition of the template, but rather just an instance for this invocation
		MobyDataType dtBoundTemplate = valueType;  
		param.setDataType(dtBoundTemplate);
		// Propagate the change to other params using this data type
		for(MobyPrimaryData primary: service.getPrimaryInputs()){
		    if(primary.getDataType() == dtTemplate){  //refers to original template we just bound
			primary.setDataType(dtBoundTemplate);
		    }
		}
		for(MobyPrimaryData primary: service.getPrimaryOutputs()){
		    if(primary.getDataType() == dtTemplate){  //refers to original template we just bound
			primary.setDataType(dtBoundTemplate);
		    }
		}
		dtTemplate.bindTemplate(param.getDataType());
	    }
	    // Looks good so far for a match.  Make sure namespace matches
	    // if part of the template too.
	    MobyNamespace[] reqNS = param.getNamespaces();
	    MobyNamespace[] valueNS = match.getNamespaces();
	    boolean foundNSMatch = false;
	    search: for(int j = 0; reqNS != null && j < reqNS.length; j++){
		for(int k = 0; valueNS != null && k < valueNS.length; k++){
		    if(valueNS[k] != null && reqNS[j].equals(valueNS[k])){
			foundNSMatch = true;
			break search;  // labeled break
		    }
		}
	    }
	    if(reqNS.length != 0 && !foundNSMatch){
		throw new IllegalArgumentException(errMessagePrefix + 
						   ": the value '" + param.getName() + 
						   "' in job " + job.getID() + 
						   " does not have one of the required namespaces"); 
	    }
	}

	// Check for secondary parameters, and fill in missing ones with the default values
	MobySecondaryData[] template2 = service.getSecondaryInputs();
	MobyDataSecondaryInstance[] values2 = job.getSecondaryData();

	for(MobySecondaryData param: template2){
	    MobyDataSecondaryInstance matchingValue = null;
	    for(MobyDataSecondaryInstance value: values2){
		if(value.getName().equals(param.getName())){
		    matchingValue = value;
		    break;
		}
	    }

	    if(matchingValue != null){
		String dataType = param.getDataType();
		// This is the first point at which we have enough info to assign
		// a data type to the secondary parameter in the job (it is string by default
		// when parsing the XML).
		try{
		    matchingValue.setDataType(dataType);
		} catch(Exception e){
		    e.printStackTrace();  // will default to String if unrecognized
		}
		// Check that the value given is valid
		if(dataType.equals(MobySecondaryData.INTEGER_TYPE)){
		    BigInteger val = matchingValue.asInteger();
		    if(param.getMinValue() != null && param.getMinValue().length() > 0){
			BigInteger min = new BigInteger(param.getMinValue());
			if(min.compareTo(val) > 0){
			    // Out of range (too small), reset the value
			    matchingValue.setValue(min.toString());
			}
		    }
		    if(param.getMaxValue() != null && param.getMaxValue().length() > 0){
			BigInteger max = new BigInteger(param.getMaxValue());
			if(max.compareTo(val) < 0){
			    // Out of range (too big), reset the value
			    matchingValue.setValue(max.toString());
			}
		    }
		}
		else if(dataType.equals(MobySecondaryData.FLOAT_TYPE)){
		    BigDecimal val = matchingValue.asFloat();
		    if(param.getMinValue() != null && param.getMinValue().length() > 0){
			BigDecimal min = new BigDecimal(param.getMinValue());
			if(min.compareTo(val) > 0){
			    // Out of range (too small), reset the value
			    matchingValue.setValue(min.toString());
			}
		    }
		    if(param.getMaxValue() != null && param.getMaxValue().length() > 0){
			BigDecimal max = new BigDecimal(param.getMaxValue());
			if(max.compareTo(val) < 0){
			    // Out of range (too big), reset the value
			    matchingValue.setValue(max.toString());
			}
		    }
		}
		else if(dataType.equals(MobySecondaryData.STRING_TYPE)){
		    // Enumerated?
		    String val = matchingValue.asString();
		    String[] allowedVals = param.getAllowedValues();
		    if(allowedVals != null && allowedVals.length > 0){
			boolean isInList = false;
			for(String allowedVal: allowedVals){
			    if(allowedVal != null && allowedVal.equals(val)){
				isInList = true;
				break;
			    }			    
			}
			if(!isInList){
			    // Illegal value given, use the default instead
			    (new IllegalArgumentException("Illegal value '"+ val + "' for secondary param '" +
							 param.getName() + "' being replaced with default value '" +
							 param.getDefaultValue() + "'")).printStackTrace();
			    matchingValue.setValue(param.getDefaultValue());
			}
		    }
		}
		else if(dataType.equals(MobySecondaryData.DATETIME_TYPE)){
		    // TODO
		}
		else if(dataType.equals(MobySecondaryData.BOOLEAN_TYPE)){
		    // no issues here...
		}
		else{
		    // what type is this ???
		}
	    }
	    else{
		// Missing value, fill it in with the default
		job.put(param.getName(), new MobyDataSecondaryInstance(param, param.getDefaultValue()));
	    }
	}
    }

    /**
     * Allocates thread-specific (depending on service specification) variables to use during an invocation.
     */
    protected synchronized MobyService startServiceInvocation(MobyContentInstance mci){
	if(responseContentInstanceMap == null){  // init() not called yet?
	    return null;
	}
	responseContentInstanceMap.put(Thread.currentThread(), mci);

	if(!hasDataTypeTemplates){
	    return thisService;
	}
	MobyService invocationSpecificService = thisService.clone();
	templatedServicesMap.put(Thread.currentThread(), invocationSpecificService);
	return invocationSpecificService;
    }

    /**
     * Cleans up invocation-specific variables.
     */
    protected synchronized void endServiceInvocation(){
	if(responseContentInstanceMap != null){
	    responseContentInstanceMap.remove(Thread.currentThread());
	}
	if(hasDataTypeTemplates && templatedServicesMap != null){
	    templatedServicesMap.remove(Thread.currentThread());
	}
    }

    /**
     * Returns the MobyContentInstance for the invocation response 
     * (or null if not in the middle of an invocation).
     */
    protected MobyContentInstance getResponseContentInstance(){
	// The only time we don't return thisService is if an invocation-specific datatype is defined
	if(responseContentInstanceMap != null &&
	   responseContentInstanceMap.containsKey(Thread.currentThread())){
	    return responseContentInstanceMap.get(Thread.currentThread());
	}
	else{
	    return null;
	}
    }

    /**
     * Returns the (possibly invocation-specific) MobyService definition
     */
    protected MobyService getService(){
	// The only time we don't return thisService is if an invocation-specific datatype is defined
	if(templatedServicesMap != null &&
	   hasDataTypeTemplates && 
	   templatedServicesMap.containsKey(Thread.currentThread())){
	    return templatedServicesMap.get(Thread.currentThread());
	}
	else{
	    return thisService;
	}
    }

    private void writeResponse(HttpServletResponse response, MobyContentInstance mobyResults){

	response.setContentType("text/xml");

	java.io.OutputStream out = null;
	try{
	    out = response.getOutputStream();
	}
	catch(java.io.IOException ioe){
	    log("While getting servlet output stream (for service response to client)", ioe);
	    return;
	}

	try{
//PG 	    if(bodyContentsName == null){ //first call
//PG 		SOAPFactory soapFactory = SOAPFactory.newInstance();
//PG 		bodyContentsName = soapFactory.createName(getServiceName()+"Return",
//PG 							  MobyPrefixResolver.MOBY_TRANSPORT_PREFIX,
//PG 							  MobyPrefixResolver.MOBY_TRANSPORT_NAMESPACE);
//PG 		bodyName = soapFactory.createName(getServiceName()+"Response",
//PG 						  MobyPrefixResolver.MOBY_TRANSPORT_PREFIX, 
//PG 						  MobyPrefixResolver.MOBY_TRANSPORT_NAMESPACE);
//PG 		stringEncAttrName = soapFactory.createName("type",
//PG 							   MobyPrefixResolver.XSI2001_PREFIX,
//PG 							   MobyPrefixResolver.XSI_NAMESPACE2001);
//PG 		faultName = soapFactory.createName("Client",
//PG 						   "", 
//PG 						   SOAPConstants.URI_NS_SOAP_ENVELOPE);
//PG 	    }

	    java.io.ByteArrayOutputStream bytesOut = new java.io.ByteArrayOutputStream(INIT_OUTPUT_BUFFER_SIZE);
	    MobyDataUtils.toXMLDocument(bytesOut, mobyResults);

	    // The SOAP envelope (headerless) consists of a body with one string element inside it
//PG 	    if(soapMessageFactory == null){
//PG 		soapMessageFactory = MessageFactory.newInstance();
//PG 	    }
//PG 	    SOAPMessage message = soapMessageFactory.createMessage();
//PG 	    message.getSOAPHeader().detachNode();

//PG 	    SOAPBodyElement bodyElement = message.getSOAPBody().addBodyElement(bodyName);
//PG 	    SOAPElement bodyContentsElement = bodyElement.addChildElement(bodyContentsName);
//PG 	    bodyContentsElement.addAttribute(stringEncAttrName, stringEncAttrValue);
//PG 	    bodyContentsElement.addTextNode(bytesOut.toString());

	    response.setStatus(HttpServletResponse.SC_OK);
//	    message.writeTo(out);
	    out.write(("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"+
		       "<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" " +
		       "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" " +
		       "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"><soapenv:Body>"+
		       "<ns1:"+getServiceName()+"Response"+
		        " soapenv:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"" +
		        " xmlns:ns1=\""+MobyPrefixResolver.MOBY_TRANSPORT_NAMESPACE+"\">"+
		         "<ns1:"+getServiceName()+"Return xsi:type=\""+stringEncAttrValue+"\">"+
		          bytesOut.toString().replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;")+
		         "</ns1:"+getServiceName()+"Return>"+
		       "</ns1:"+getServiceName()+"Response>"+
		       "</soapenv:Body></soapenv:Envelope>").getBytes());
	    out.flush();
	    out.close();

	} catch(Exception e){
	    log("While writing SOAP response to client for " + getServiceName(), e);
	    return;
	}

    }

    public void log(java.lang.String msg){log(msg, null);}
    public void log(java.lang.String msg,
		    Exception ex){
	String fullMsg = msg+"\n";
	if(ex != null){
	    fullMsg += ex.getClass().getName()+": "+ex.getMessage()+"\n";
	    for(StackTraceElement ste: ex.getStackTrace()){
		fullMsg += ste.toString()+"\n";
	    }
	}
	System.err.println(fullMsg);
    }

    /**
     * Writes the error to the servlet log, and adds a MobyException block to the response
     */
    protected void addException(ServiceException se){
	MobyContentInstance currentContent = getResponseContentInstance();
	if(currentContent != null){
	    currentContent.addException(se);
	    if(isInitialized){
		log("While executing Moby Service (initialized) " + getServiceName(), se);
	    }
	    else if(getService() != null){
		System.err.println("While executing Moby Service (uninitialized) " + getClass().getName());
		se.printStackTrace();
	    }
	}
	else{
	    if(isInitialized){
		log("Caught exception, but had no results contents to which it could be appended", se);
	    }
	    else{
		System.err.println("Caught exception, but had no results contents to which it could be appended");
	    }
	}
    }

    /**
     * Writes the error to the servlet log, and adds a MobyException block to the response
     */
    protected void addException(MobyContentInstance response, Throwable ex){
	if(ex instanceof ServiceException){
	    response.addException((ServiceException) ex);
	}
	else{
	    response.addException(new ServiceException(ServiceException.ERROR,
						       ServiceException.INTERNAL_PROCESSING_ERROR,
						       ex.toString().replaceAll("<","&lt;")));
	}
	if(isInitialized){
	    log("While executing Moby Service (initialized) " + getServiceName(), ex);
	}
	else if(getService() != null){
	    System.err.println("While executing Moby Service (uninitialized) " + getClass().getName());
	    ex.printStackTrace();
	}
    }

    /**
     * Writes the error to the servlet log, and adds a MobyException block to the response
     * with the details filled in for the job that caused the exception.
     */
    protected void addException(MobyContentInstance response, MobyDataJob job, Throwable ex){
	if(ex instanceof ServiceException){
	    response.addException((ServiceException) ex);
	}
	else{
	    response.addException(new ServiceException(ServiceException.ERROR,
						       ServiceException.INTERNAL_PROCESSING_ERROR,
						       job.getID(),
						       null,
						       ex.toString().replaceAll("<","&lt;")));
	}
	if(isInitialized){
	    log("While executing Moby Service (initialized) " + getServiceName() + ", job '" + job.getID()+"'", ex);
	}
	else if(getService() != null){
	    System.err.println("While executing Moby Service (uninitialized) " + getClass().getName() + ", job '" + job.getID()+"'");
	    ex.printStackTrace();
	}
    }

    /**
     * If you override this method, be sure to call super.init()
     */
    public void init(){
	try{
	    DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
	    dbf.setNamespaceAware(true);	
	    docBuilder = dbf.newDocumentBuilder();

	    if(System.getProperty("javax.xml.transform.TransformerFactory") == null){
		System.setProperty("javax.xml.transform.TransformerFactory",
				   "org.apache.xalan.processor.TransformerFactoryImpl");
	    }

	    // This is a better implementation than Axis, so force it (avoids servlet container issues too)
	    if(System.getProperty("javax.xml.soap.MessageFactory") == null){
		System.setProperty("javax.xml.soap.MessageFactory", 
				   "org.apache.axis.soap.MessageFactoryImpl");
	    }
	    if(System.getProperty("javax.xml.soap.SOAPFactory") == null){
		System.setProperty("javax.xml.soap.SOAPFactory", 
				   "org.apache.axis.soap.SOAPFactoryImpl");
	    }
	    //PG soapMessageFactory = MessageFactory.newInstance();

	    // Maps storing invocation-specific context variable
	    templatedServicesMap = new HashMap<Thread,MobyService>();
	    responseContentInstanceMap = new HashMap<Thread,MobyContentInstance>();

	    // Determine the Moby Central (and hence ontologies) to associate with the service
	    String centralURL = null;
	    mobyService ann = 
		this.getClass().getAnnotation(mobyService.class);
	    if(ann != null){
		centralURL = ann.centralEndpoint();
	    }
 	    if(getCoCInitParameter(MOBY_CENTRAL_URL_PARAM) != null){
		centralURL = getCoCInitParameter(MOBY_CENTRAL_URL_PARAM);
	    }

	    if(centralURL != null && centralURL.length() > 0){
 		mobyRequest = new MobyRequest(CentralImpl.getDefaultCentral(new Registry("any_synonym",
											 centralURL,
											 "any_ns")));
 	    }
 	    else{
 		mobyRequest = new MobyRequest(CentralImpl.getDefaultCentral());
 	    }
	    registry = new Registry("any_synonym", 
				    mobyRequest.getCentralImpl().getRegistryEndpoint(),
				    mobyRequest.getCentralImpl().getRegistryNamespace());
            serviceInstanceRDF = new ServiceInstanceRDF(registry);
	    // Forces a preload of the current MOBY Object ontology
	    MobyDataType.getDataType("Object", registry);

	    mobyRequest.setDebugMode(isDebug);
	    
	}catch(Exception e){
	    System.err.println("Could not create required resources:"+e);
	    e.printStackTrace();
	}
	isInitialized = true;
    }

    /**
     * Returns the Moby Central server that the service is associated with,
     * based on the web.xml and the servlet annotation.  If the servlet hasn't
     * been initialized yet (init() hasn't been called), null will be returned.
     */
    public Central getCentralImpl(){
	return mobyRequest == null ? null : mobyRequest.getCentralImpl();
    }

    protected String getCoCInitParameter(String paramName){
        if(extraConfig != null && extraConfig.containsKey(paramName)){
            return extraConfig.get(paramName);
        }
	javax.servlet.ServletConfig config = getServletConfig();
	if(config != null){
	    if(config.getInitParameter(paramName) != null){
		return config.getInitParameter(paramName);
	    }
	}
	javax.servlet.ServletContext context = getServletContext();
	if(context != null){
	    if(context.getInitParameter(paramName) != null){
		return context.getInitParameter(paramName);
	    }
	}
	return getInitParameter(paramName);
    }

    public synchronized void setCoCInitParameter(String param, String value){
        if(extraConfig == null){
          extraConfig = new HashMap<String,String>();
        }
        extraConfig.put(param, value);
    }

    public synchronized MobyService createServiceFromConfig(HttpServletRequest request) throws Exception{
	MobyService service = new MobyService(getServiceName());

	Vector<MobyData> inputTypes = new Vector<MobyData>();
	Vector<MobyPrimaryData> outputTypes = new Vector<MobyPrimaryData>();

	mobyService ann = 
	    this.getClass().getAnnotation(mobyService.class);
	if(ann == null){
	    throw new Exception("The servlet does not have a @mobyService() class annotation as required, " +
				"cannot configure the service");
	}
	else{
	    //log("Reading service meta-data from Java class annotation for "+ann.name());
	}

	javax.servlet.ServletConfig config = getServletConfig();

	String paramNames = "";
	if(config != null){
	    java.util.Enumeration paramNamesEnum = config.getInitParameterNames();
	    if(paramNamesEnum.hasMoreElements()){
		paramNames = paramNamesEnum.nextElement().toString();
	    }
	    while(paramNamesEnum.hasMoreElements()){
		paramNames += ", "+paramNamesEnum.nextElement();
	    }
	}

	// Inputs and outputs and service type must be defined
	String[] ins = ann.in();
	if(getCoCInitParameter(MOBY_INPUT_PARAM) != null){
	    ins = getCoCInitParameter(MOBY_INPUT_PARAM).split(",");
	}
	if(ins == null){
	    throw new Exception("Could not find required " + MOBY_INPUT_PARAM + 
				" parameter in servlet config, available parameters were: " + paramNames);
	}
	// The map allows definitions like <T extends GenericSequence>
	Map<String,MobyDataTypeTemplate> templateDataTypeMap = new HashMap<String,MobyDataTypeTemplate>();
	for(int i = 0; i < ins.length; i++){
	    // non-void param
	    if(ins[i] != null && ins[i].length() > 0){
		inputTypes.add(stringToPrimaryDataTemplate(ins[i], templateDataTypeMap));
	    }
	}

	String[] outs = ann.out();
	if(getCoCInitParameter(MOBY_OUTPUT_PARAM) != null){
	    outs = getCoCInitParameter(MOBY_OUTPUT_PARAM).split(",");
	}
	if(outs == null){
	    throw new Exception("Could not find required " + MOBY_OUTPUT_PARAM + 
				" parameter in servlet config ");
	}
	// The templateDataTypeMap map allows us to now use T in an output data type spec
	for(int i = 0; i < outs.length; i++){
	    if(outs[i] != null && outs[i].length() > 0){// non-void param
		outputTypes.add(stringToPrimaryDataTemplate(outs[i], templateDataTypeMap));
	    }
	}
	if(!templateDataTypeMap.isEmpty()){
	    hasDataTypeTemplates = true;  // affects runtime service behaviours
	}

	String[] secondaries = ann.secondaryParams();
	if(getCoCInitParameter(MOBY_SECONDARYINPUT_PARAM) != null){
	    secondaries = getCoCInitParameter(MOBY_SECONDARYINPUT_PARAM).split("\t");
	}
	if(secondaries != null && secondaries.length > 0){
	    for(String secondary: secondaries){
		if(secondary != null && secondary.length() > 0){
		    inputTypes.add(stringToSecondaryDataTemplate(secondary));
		}
	    }
	}

	service.setInputs(inputTypes.toArray(new MobyData[inputTypes.size()]));
	service.setOutputs(outputTypes.toArray(new MobyData[outputTypes.size()]));

	// A description and provider URI must be available too
	String param = ann.type();
	// Did we override the service type in the web.xml?
	if(getCoCInitParameter(MOBY_SERVICETYPE_PARAM) != null){
	    param = getCoCInitParameter(MOBY_SERVICETYPE_PARAM);
	}
	if(param == null){
	    throw new Exception("Could not find required " + MOBY_SERVICETYPE_PARAM + 
				" parameter in servlet config ");
	}
	if(param.length() == 0){
	    throw new Exception("Required " + MOBY_SERVICETYPE_PARAM + 
				" parameter in servlet config cannot be blank");
	}
	MobyServiceType serviceType = MobyServiceType.getServiceType(param, registry);
	if(serviceType == null){
	    throw new Exception("The service type specified (" + param + 
				") was not found in the service type ontology");
	}
	service.setServiceType(serviceType);

	String[] desc = ann.description();
	// Did we override the service type in the web.xml?
	if(getCoCInitParameter(MOBY_SERVICE_DESC_PARAM) != null){
	    desc = new String[1];
	    desc[0] = getCoCInitParameter(MOBY_SERVICE_DESC_PARAM);
	}
	if(desc == null){
	    throw new Exception("Could not find required " + MOBY_SERVICE_DESC_PARAM + 
				" parameter in servlet config ");
	}
	if(desc.length == 0){
	    throw new Exception("Required " + MOBY_SERVICE_DESC_PARAM + 
				" parameter in servlet config cannot be blank");
	}
	StringBuffer descBuffer = new StringBuffer();
	for(int i = 0; i < desc.length; i++){
	    descBuffer.append(desc[i]);
	}
	service.setDescription(descBuffer.toString());

	param = ann.provider();
	// Did we override the provider info in web.xml?
	if(getCoCInitParameter(MOBY_PROVIDER_URI_PARAM) != null){
	    param = getCoCInitParameter(MOBY_PROVIDER_URI_PARAM);
	}
	if(param == null){
	    throw new Exception("Could not find required " + MOBY_PROVIDER_URI_PARAM + 
				" parameter in servlet config ");
	}
	if(param.length() == 0){
	    throw new Exception("Required " + MOBY_PROVIDER_URI_PARAM + 
				" parameter in servlet config cannot be blank");
	}
	service.setAuthority(param);

	// Now we have all the info we need to create the LSID for the service
	String time = MobyDataDateTime.getString(
			  new java.util.GregorianCalendar(
				 java.util.TimeZone.getTimeZone("Zulu"))).replaceAll(":", "-");
	service.setLSID("urn:lsid:biomoby.org:serviceinstance:"+param+","+getServiceName()+":"+time);

	param = ann.author();
	// Did we override the contact info in web.xml?
	if(getCoCInitParameter(MOBY_CONTACT_PARAM) != null){
	    param = getCoCInitParameter(MOBY_CONTACT_PARAM);
	}
	if(param == null){
	    throw new Exception("Could not find required " + MOBY_CONTACT_PARAM + 
				" parameter in servlet config ");
	}
	if(param.length() == 0){
	    throw new Exception("Required " + MOBY_CONTACT_PARAM + 
				" parameter in servlet config cannot be blank " +
				"(must have form name@mail.domain)");
	}
	if(param.indexOf("@") < 2 || param.indexOf("@") > param.length()-2 ||
	   param.indexOf(".") < 2 || param.indexOf(".") > param.length()-2 ||
	   param.lastIndexOf("@") > param.lastIndexOf(".")){
	    throw new Exception("Required " + MOBY_CONTACT_PARAM + 
				" parameter in servlet config did not have the form \"name@mail.domain\" (found "+param+")");
	}
	
	service.setEmailContact(param);

	// From the request, determine the URL used to access this service
	String endPointURL = request == null ? "" : request.getRequestURL().toString();
	// When we POST this URL, the service is executed
	service.setURL(endPointURL);
	// When we GET this URL, the RDF is returned
	service.setSignatureURL(endPointURL+"?"+MODE_HTTP_PARAM+"="+RDF_MODE);
	// Other fields (authoritative and contact info) are highly recommended, but optional
	if(config != null){
	    param = getCoCInitParameter(MOBY_AUTHORITATIVE_PARAM);
	    if(param != null){
		if("YES".equals(param.toUpperCase()) || "Y".equals(param.toUpperCase()) || "1".equals(param)){
		    service.setAuthoritative(true);
		}
		else{
		    log("Interpreting config parameter '" + param + "' as NO for authoritative property of the service");
		    service.setAuthoritative(false);
		}
	    }
	    else{
		service.setAuthoritative(ann.authoritative());
	    }
	}
	else{
	    service.setAuthoritative(ann.authoritative());
	}

	mobyRequest.setService(service);

	return service;
    }

    /**
     * Reads the service name from the mobyService annotation.
     */
    public String getServiceName(){
	mobyService ann = 
	    this.getClass().getAnnotation(mobyService.class);

	// The best way to get the name is via the configured service, if it exists
	if(thisService != null && thisService.getName() != null && thisService.getName().trim().length() > 0){
	    return thisService.getName();
	}

	String param = null;
	// Did we override the service type in the web.xml?
	if(getCoCInitParameter(MOBY_SERVICENAME_PARAM) != null){
	    param = getCoCInitParameter(MOBY_SERVICENAME_PARAM);
	    if(param != null && param.length() != 0){
		return param;
	    }
	}

	if(ann == null){
	    return "AnonymousService"; // this should never happen unless the class files are out of sync...
	}

	return ann.name();
    }

    /**
     * Strings have the form name:paramType:default:spec where spec depends on the parameter type.
     * Currently, these are the valid paramTypes and their specs:
     *
     *   <ul><li>Integer: blank or [,max] or [min,] or [min,max]</li>
     *       <li>Float: blank or [,max] or [min,] or [min,max]</li>
     *       <li>String: blank or [choice1,choice2,...]</li>
     *       <li>DateTime: blank or [,max] or [min,] or [min,max]</li>
     *       <li>Boolean: blank</li></ul>
     * 
     * e.g. <code>db:String:nr:[nr,nt,est,swissprot]</code>
     *
     * e.g. <code>filter:Boolean:true</code>
     *
     * e.g. <code>hits:Integer:100:[0,]</code>
     */
    public static MobySecondaryData stringToSecondaryDataTemplate(String template) throws Exception{
	String[] fields = template.split(":", -2); // -2 == don't strip trailing blank fields
	if(fields.length < 3){
	    throw new Exception("The parameter specification (" + 
				template + ") must have the minimal form " +
				"\"name:paramType:defaultValue\", aborting!");
	}
	if(fields[0].length() < 1){
	    throw new Exception("The parameter specification (" + 
				template + ") has a blank parameter name, this is not allowed");
	}
	if(fields[1].length() < 1){
	    throw new Exception("The parameter specification (" + 
				template + ") has a blank parameter type, this is not allowed");
	}
	MobySecondaryData dataTemplate = new MobySecondaryData(fields[0]);
	String dataType = fields[1];
	try{
	    dataTemplate.setDataType(dataType);  // should throw an exception if invalid param type
	} catch(Exception e){
	    e.printStackTrace();
	    throw new Exception("Error in parameter type (" + dataType + "') of secondary " +
				"parameters specification '" + template + "': " + e);
	}

	String defaultValue = fields[2];
	dataTemplate.setDefaultValue(defaultValue);

	String spec = "";
	int i = 3;
	for(; i < fields.length-1; i++){
	    spec = spec + fields[i] + ":";
	}
	if(i == fields.length-1){
	    spec = spec + fields[i];
	}

	// Ensure the [] are there, and strip them
	if(spec.length() > 0){
	    if(spec.length() < 3 || spec.indexOf('[') != 0 || spec.lastIndexOf(']') != spec.length()-1){
		throw new Exception("Parameter options specification (4th field of '" + 
				    template + "') " +
				    "does not have the required form '[...]', but rather: " + spec);
	    }
	    spec = spec.substring(1, spec.length()-1);
	}
	String[] specValues = new String[0];
	if(spec.length() > 0){
	    specValues = spec.split(",", -2); // -2 == keep trailing blank values
	}

	if(dataType.equals(MobySecondaryData.INTEGER_TYPE) ||
	   dataType.equals(MobySecondaryData.FLOAT_TYPE)){
	    if(specValues.length > 0){
		if(specValues.length != 2){
		    throw new Exception("Numeric parameter options specification (4th field of " +
					template + "') " +
					"does not have on of the required forms [min,] or [,max] or [min,max]");
		}
		BigDecimal min = null;
		BigDecimal max = null;
		if(specValues[0].length() != 0){
		    min = new BigDecimal(specValues[0]);  // will throw exception if not a number
		}
		if(specValues[1].length() != 0){
		    max = new BigDecimal(specValues[1]);  // will throw exception if not a number
		}
		// We will actually accept [,] as an unlimited range
		if(min != null && max != null && min.compareTo(max) > 0){
		    throw new Exception("Numeric parameter range (4th field of " + template + "') " +
					"has a minimum value greater than the maximum value");
		}
		BigDecimal defaultNumber = new BigDecimal(defaultValue); // will throw exception if not a number
		if(min != null){
		    if(min.compareTo(defaultNumber) > 0){
			throw new Exception("Numeric parameter default (3rd field of " + template + "') " +
					    "is less than the minimum value specified");
		    }
		}
		if(max != null){
		    if(max.compareTo(defaultNumber) < 0){
			throw new Exception("Numeric parameter default (3rd field of " + template + "') " +
					    "is greater than the maximum value specified");
		    }
		}
	    }
	}
	else if(dataType.equals(MobySecondaryData.STRING_TYPE)){
	    if(specValues.length > 0){  // an enumeration
		boolean defaultListed = false;
		for(String value: specValues){
		    if(defaultValue.equals(value)){
			defaultListed = true;
			break;
		    }
		}
		if(!defaultListed){
		    throw new Exception("The default value (" + defaultValue + 
					") was not listed in the enumeration (" + 
					spec + ")");
		}

		dataTemplate.setAllowedValues(specValues);
	    }
	    // otherwise it's a freeform string
	}
	else if(dataType.equals(MobySecondaryData.BOOLEAN_TYPE)){
	    if(specValues.length > 0){
		throw new Exception("Boolean secondary parameter specification provided " +
				    "superfluous fields (only 3 needed): " + spec);
	    }
	}
	else if(dataType.equals(MobySecondaryData.DATETIME_TYPE)){
	    // TODO
	}
	else{
	    throw new Exception("Unrecognized secondary data type encountered: " + dataType);
	}

	return dataTemplate;
    }

    /**
     * Strings have the form name:objectType:namespace, with ":namespace" optional
     * If the input is expected to be a Collection, then the syntax is name:Collection(objectType):namespace
     */
    public static MobyPrimaryData stringToPrimaryDataTemplate(String spec, Map<String,MobyDataTypeTemplate> runTimeTemplates) 
	throws Exception{
	StringTokenizer st = new StringTokenizer(spec, ":");
	String name = st.nextToken();
	if(name == null || name.length() == 0){
	    throw new Exception("Anonymous parameters are not allowed in service calls (" + 
				spec + "), aborting!");
	}
        if(!st.hasMoreTokens()){
	    throw new Exception("The parameter specification (" + 
				spec + ") must have the minimal form " +
				"\"name:objectType\", aborting!");
	}
	String objectType = st.nextToken().trim();
	if(objectType == null || objectType.length() == 0){
	    throw new Exception("The parameter specification (" + 
			       spec + ") has a blank object type, aborting!");
	}

	MobyPrimaryData dataTemplate = null;

	boolean isCollection = false;
	if(objectType.indexOf("Collection(") == 0 && 
	   objectType.lastIndexOf(")") == objectType.length()-1){
	    objectType = objectType.substring(11, objectType.length()-1).trim();
	    dataTemplate = new MobyPrimaryDataSet(name);
	    isCollection = true;
	}
	else{
	    dataTemplate = new MobyPrimaryDataSimple(name);
	}

	// Allow runtime templating like <T extends GenericSequence>, so for example we can
	// restrict output to the same type T as the input that was provided.
	String newTemplateToken = null;
	if(objectType.indexOf("<") == 0 &&
	   objectType.lastIndexOf(">") == objectType.length()-1){
	    String[] tokens = objectType.substring(2, objectType.length()-1).split("\\s+");
	    if(tokens.length != 3 || !"extends".equals(tokens[1]) ||
	       tokens[0] == null || tokens[0].length() == 0 ||
	       tokens[2] == null || tokens[2].length() == 0 ){
		throw new Exception("The parameter specification (" + 
				spec + ") attempts to define a template data type, but it is not of the " +
				    "expected form <T extends ClassName>");
	    }
	    if(runTimeTemplates.containsKey(tokens[0])){
		throw new Exception("The parameter specification (" + 
				    spec + ") attempts to define a template data type " + tokens[0] +
				    ", but that token has already been bound to " +
				    " a template definition in this context (" +
				    runTimeTemplates.get(tokens[0]).getName() + ")");
	    }
	    newTemplateToken = tokens[0];
	    if(MobyDataType.getDataType(newTemplateToken, registry) != null){
		System.err.println("The parameter specification (" + 
				   spec + ") defines a template data type " + tokens[0] +
				   ", but that token conflicts with the name of an existing " +
				   "data type in Moby Central.  The template definition will " +
				   "be used, but this may have unintended consequences.  " +
				   "It is suggested that you change the template name " +
				   " to an unambiguous one.");	   
	    }
	    objectType = tokens[2];
	}
	MobyDataType type = null;
	// Using a previously defined template data type
	if(runTimeTemplates.containsKey(objectType)){
	    type = runTimeTemplates.get(objectType);
	}
	else{  // Using a literal data type name
	    type = MobyDataType.getDataType(objectType, registry);
	}
	if(type == null){
	    throw new Exception("The parameter specification (" + 
				spec + ") has a data type (" + objectType + ") not found in the " +
				"MOBY registry (" + registry.getEndpoint() + 
				"), aborting! Either correct the type," +
				" or register it as a new type in MOBY Central");
	}
	// Defining a template data type, MobyDataTypeTemplate will 
	// bind to a subclass during service execution.
	if(newTemplateToken != null){  
	    runTimeTemplates.put(newTemplateToken, new MobyDataTypeTemplate(type));
	    type = runTimeTemplates.get(newTemplateToken);
	}
	dataTemplate.setDataType(type);
	if(isCollection){
	    // Example data element in set needed for RDF creator to recognize the data type
	    MobyPrimaryDataSimple exampleData = new MobyPrimaryDataSimple(name);
	    exampleData.setDataType(type);
	    ((MobyPrimaryDataSet) dataTemplate).addElement(exampleData);  
	}
	
	// namespace is optional
	if(st.hasMoreTokens()){
	    String namespaceValues = st.nextToken();
	    // There may actually be multiple namespaces, separated by commas
	    for(String namespaceValue: namespaceValues.split(",")){
		MobyNamespace nsObject = MobyNamespace.getNamespace(namespaceValue, registry);
		if(nsObject == null){
		    System.err.println("WARNING: The parameter specification (" + 
				       spec + ") has a namespace not found in the " +
				       "MOBY registry! I'm proceeding anyway, but please " +
				       "either correct the value, " +
				       "or register it as a new namespace in MOBY Central");
		    nsObject = new MobyNamespace(namespaceValue);
		}
		dataTemplate.addNamespace(nsObject);
	    }
	}

	if(st.hasMoreTokens()){
	    System.err.println("WARNING: The parameter specification (" + 
			       spec + ") has more than three fields, " +
			       "it should be \"name:objectType:namespace\"" +
			       "I'm ignoring the extra field(s), but please correct this.");
	}

	return dataTemplate;
    }

    /**
     * By default, this method does nothing.  If any additional resources were allocated
     * in an override of init(), they should be freed in an override of this method.
     */
    public void destroy(){
	
    }

    /**
     * By default, this method will sequentially execute the individual jobs from the request
     * payload (processRequest).  Override this method if you want to implement some
     * division-of-labour scheme.
     *
     * @param resultContents is a Map prepopulated with <jobID,MobyDataJob> pairs for the response
     */
    public void processRequests(MobyContentInstance requestContents, MobyContentInstance resultContents) 
	throws Exception{
	// Set up the per-invocation variable context
	startServiceInvocation(resultContents);  
	for(String jobName: requestContents.keySet()){
	    try{
		MobyDataJob currentRequest = requestContents.get(jobName);
		if(getService() != null){
		    // Should happen, unless main() was called
		    validateArguments(currentRequest, getService(), "While executing service");
		}
		MobyDataJob currentResult = resultContents.get(jobName);
		if(currentResult == null){
		    currentResult = new MobyDataJob();
		    currentResult.setID(jobName);
		    resultContents.put(jobName, currentResult);
		}
		processRequest(currentRequest, currentResult);
	    }catch(Throwable e){
		addException(resultContents, requestContents.get(jobName), e);
	    }
	}
	// Makes sure per-invocation variables can get GC'ed
	endServiceInvocation();
    }

    /**
     * The real worker method: implementing classes must override this 
     * method to do something useful.  By default, nothing is done.
     *
     * If a ServiceException is thrown in this method, it is copied verbatim into the
     * MOBY response.  Any other exceptions thrown will be automatically wrapped 
     * in a ServiceException of severity ERROR and type INTERNAL_PROCESSING_ERROR.
     * To add any other type of non-fatal error (e.g. a WARN), call addException() explicitly,
     * rather than throwing a Java Exception which stops the job's execution.
     *
     * @param request the request to process, with params guaranteed to be of the type specified in the mobyService annotation, or values overriding the annjotation in a WAR file's WEB-INF/web.xml
     */
    public void processRequest(MobyDataJob request, MobyDataJob result) throws Exception{
    }

    /**
     * Determines whether call to main() that terminate on error should call System.exit()
     * or not.  This is the default behaviour, and should generally be switched only for unit testing
     * purposes.
     */
    public static void setMainTerminationExit(boolean b){
	shouldExit = b;
    }

    /**
     * Expects one argument, an example MOBY XML input file or URL.
     * processRequest() is then called for every job in the input data, 
     * and the results are printed to the screen.
     */
    public static void main(String[] args) throws Exception{

	if(args.length != 2){
            System.err.println("MobyServlet is ignoring the main method (called with " + args.length + " args) " +
			       ", executing the subclass's main method.");
	    System.err.println("If you wish to run the Servlet test, the usage is: ");
            System.err.println("java ServletName SerlvetName <mobyInputExample.xml | mobyExampleURL>");
            return;
	}

        
        MobyServlet servlet = new MobyServlet();
	ClassLoader classLoader = servlet.getClass().getClassLoader();
        try{
	    // This line can throw many different exception if you didn't get the class right! 
	    Class servletClass = classLoader.loadClass(args[0]);
            if(servletClass == null){
                System.err.println("The Class to test (" + args[0] + ") was not found");
                System.err.println("Assuming you don't want to run a servlet test, executing main() method from subclass");
                return;
            }
	    servlet = (MobyServlet) servletClass.newInstance();
        } catch(Exception e){
            System.err.println("The Class to test was not specified properly:");
            e.printStackTrace();
            System.err.println("Assuming you don't want to run a servlet test, executing main() method from subclass");
            return;
        }

	java.net.URL source = null;
	try{
	    source = new java.net.URL(args[1]);
	} catch(java.net.MalformedURLException murle){
	    try{
                source = (new java.io.File(args[1])).toURI().toURL();
            } catch (Exception e){
	        System.err.println("FATAL: Could not resolve passed in example MOBY XML location (" +
	    		           args[1] + " as a valid file or URL");
                System.err.println("Assuming you don't want to run a servlet test, executing main() method from subclass");
	        return;
            }
	}

        MobyContentInstance testData = null;
	try{
           testData = MobyDataUtils.fromXMLDocument(source.openStream(), registry);
        } catch(Exception e){
           System.err.println("The test data was invalid:");
           e.printStackTrace();
           if(shouldExit)
	       System.exit(2);
	   else
	       throw e;
        }
	// TO DO: Should we validate the input somehow?
	// They'll find out when they run the servlet tests later, but the earlier the better...

	MobyContentInstance testResults = new MobyContentInstance();

	// Configure the service, minimally, to check the input data
	java.net.URL webXMLURL = classLoader.getResource("WEB-INF/web.xml");
	if(webXMLURL == null){
	    System.err.println("No WEB-INF/web.xml was found, assuming you don't need any of its settings for the servlet to work!");
	    servlet.init(new TestServletConfig(args[0], new TestServletContext()));
	}
	else{
	    servlet.init(new TestServletConfig(new TestServletContext(), webXMLURL));
	}
	servlet.thisService = servlet.createServiceFromConfig(null);

	// It should populate the testResults
        try{
	    servlet.processRequests(testData, testResults);
        } catch(Exception e){
            System.err.println("An error occured while testing your code's processRequest method:");
            e.printStackTrace();
	    if(shouldExit){
		System.exit(2);
	    } 
	    else{
		throw e;
	    }
	}

	if(testResults.hasExceptions(ServiceException.WARNING)){
	    for(ServiceException se: testResults.getExceptions()){
		se.printStackTrace();
	    }
	    throw new Exception("Executing service " + args[0] + " caused exceptions");
	}
	else{
	    System.out.println(testResults.toString());
	}
        if(shouldExit){
	    System.exit(0); //so that subclasses's main methods don't executed if the test was run
	}
    }
}
