Log in Help
Print
Homegatesrcgate 〉 Factory.java
 
/*
 *  Factory.java
 *
 *  Copyright (c) 1995-2012, The University of Sheffield. See the file
 *  COPYRIGHT.txt in the software or at http://gate.ac.uk/gate/COPYRIGHT.txt
 *
 *  This file is part of GATE (see http://gate.ac.uk/), and is free
 *  software, licenced under the GNU Library General Public License,
 *  Version 2, June 1991 (in the distribution as file licence.html,
 *  and also available at http://gate.ac.uk/gate/licence.html).
 *
 *  Hamish Cunningham, 25/May/2000
 *
 *  $Id: Factory.java 15785 2012-05-14 16:00:05Z ian_roberts $
 */

package gate;

import gate.Gate.DirectoryInfo;
import gate.annotation.ImmutableAnnotationSetImpl;
import gate.creole.AbstractProcessingResource;
import gate.creole.AbstractResource;
import gate.creole.AnnotationSchema;
import gate.creole.ConditionalController;
import gate.creole.CustomDuplication;
import gate.creole.ParameterException;
import gate.creole.ParameterList;
import gate.creole.ResourceData;
import gate.creole.ResourceInstantiationException;
import gate.creole.gazetteer.Gazetteer;
import gate.event.CreoleEvent;
import gate.event.CreoleListener;
import gate.jape.constraint.ConstraintFactory;
import gate.jape.parser.ParseCpsl;
import gate.persist.PersistenceException;
import gate.persist.SerialDataStore;
import gate.security.SecurityException;
import gate.util.BomStrippingInputStreamReader;
import gate.util.Out;
import gate.util.SimpleFeatureMapImpl;
import gate.util.Strings;

import java.io.IOException;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.net.URL;
import java.util.Collection;
import java.util.EventListener;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import org.apache.log4j.Logger;

/** Provides static methods for the creation of Resources.
  */
public abstract class Factory {
  /** Debug flag */
  private static final boolean DEBUG = false;
  
  private static final boolean DEBUG_DUPLICATION = false;
  
  private static final Logger log = Logger.getLogger(Factory.class);

  /** An object to source events from. */
  private static CreoleProxy creoleProxy;

  /** Create an instance of a resource using default parameter values.
    * @see #createResource(String,FeatureMap)
    */
  public static Resource createResource(String resourceClassName)
  throws ResourceInstantiationException
  {
    // get the resource metadata
    ResourceData resData = (ResourceData) Gate.getCreoleRegister().get(resourceClassName);
    if(resData == null) {
      Set<DirectoryInfo> plugins = Gate.getDirectoryInfo(resourceClassName);
      
      StringBuilder msg = new StringBuilder();
      msg.append("Couldn't get resource data for ").append(resourceClassName).append(".\n\n");
            
      if (plugins.isEmpty()) {
        msg.append("You may need first to load the plugin that contains your resource.\n");
        msg.append("For example, to create a gate.creole.tokeniser.DefaultTokeniser\n");
        msg.append("you need first to load the ANNIE plugin.\n\n");
      }
      else if (plugins.size() == 1){
        msg.append(resourceClassName).append(" can be found in the ").append(plugins.iterator().next().getName()).append(" plugin\n\n");
      }
      else {
        msg.append(resourceClassName).append(" can be found in the following plugins\n   ");
        for (DirectoryInfo dInfo : plugins) {
          msg.append(dInfo.getName()).append(", ");
        }
        
        msg.setLength(msg.length()-2);
        msg.append("\n\n");
      }
      
      msg.append("Go to the menu File->Manage CREOLE plugins or use the method\n");
      msg.append("Gate.getCreoleRegister().registerDirectories(pluginDirectoryURL).");
      
      throw new ResourceInstantiationException(msg.toString());
    }

    // get the parameter list and default values
    ParameterList paramList = resData.getParameterList();
    FeatureMap parameterValues = null;
    try {
      parameterValues = paramList.getInitimeDefaults();
    } catch(ParameterException e) {
      throw new ResourceInstantiationException(
        "Couldn't get default parameters for " + resourceClassName + ": " + e
      );
    }

    return createResource(resourceClassName, parameterValues);
  } // createResource(resClassName)

  /** Create an instance of a resource, and return it.
    * Callers of this method are responsible for
    * querying the resource's parameter lists, putting together a set that
    * is complete apart from runtime parameters, and passing a feature map
    * containing these parameter settings.
    *
    * @param resourceClassName the name of the class implementing the resource.
    * @param parameterValues the feature map containing intialisation time
    *   parameterValues for the resource.
    * @return an instantiated resource.
    */
  public static Resource createResource(
    String resourceClassName, FeatureMap parameterValues
  ) throws ResourceInstantiationException
  {
    return createResource(resourceClassName, parameterValues, null, null);
  } // createResource(resClassName, paramVals, listeners)

  /** Create an instance of a resource, and return it.
    * Callers of this method are responsible for
    * querying the resource's parameter lists, putting together a set that
    * is complete apart from runtime parameters, and passing a feature map
    * containing these parameter settings.
    *
    * @param resourceClassName the name of the class implementing the resource.
    * @param parameterValues the feature map containing intialisation time
    *   parameterValues for the resource.
    * @param features the features for the new resource
    * @return an instantiated resource.
    */
  public static Resource createResource(
    String resourceClassName, FeatureMap parameterValues,
    FeatureMap features
    ) throws ResourceInstantiationException
   {
      return createResource(resourceClassName, parameterValues,
                            features, null);
   }

  /** Create an instance of a resource, and return it.
    * Callers of this method are responsible for
    * querying the resource's parameter lists, putting together a set that
    * is complete apart from runtime parameters, and passing a feature map
    * containing these parameter settings.
    *
    * In the case of ProcessingResources they will have their runtime parameters
    * initialised to their default values.
    *
    * @param resourceClassName the name of the class implementing the resource.
    * @param parameterValues the feature map containing intialisation time
    *   parameterValues for the resource.
    * @param features the features for the new resource or null to not assign
    *   any (new) features.
    * @param resourceName the name to be given to the resource or null to assign
    *   a default name.
    * @return an instantiated resource.
    */
  public static Resource createResource(
    String resourceClassName, FeatureMap parameterValues,
    FeatureMap features, String resourceName
  ) throws ResourceInstantiationException
   {
    // get the resource metadata
    ResourceData resData = (ResourceData) Gate.getCreoleRegister().get(resourceClassName);
    if(resData == null) {
      Set<DirectoryInfo> plugins = Gate.getDirectoryInfo(resourceClassName);
      
      StringBuilder msg = new StringBuilder();
      msg.append("Couldn't get resource data for ").append(resourceClassName).append(".\n\n");
            
      if (plugins.isEmpty()) {
        msg.append("You may need first to load the plugin that contains your resource.\n");
        msg.append("For example, to create a gate.creole.tokeniser.DefaultTokeniser\n");
        msg.append("you need first to load the ANNIE plugin.\n\n");
      }
      else if (plugins.size() == 1){
        msg.append(resourceClassName).append(" can be found in the ").append(plugins.iterator().next().getName()).append(" plugin\n\n");
      }
      else {
        msg.append(resourceClassName).append(" can be found in the following plugins\n   ");
        for (DirectoryInfo dInfo : plugins) {
          msg.append(dInfo.getName()).append(", ");
        }
        
        msg.setLength(msg.length()-2);
        msg.append("\n\n");
      }
      
      msg.append("Go to the menu File->Manage CREOLE plugins or use the method\n");
      msg.append("Gate.getCreoleRegister().registerDirectories(pluginDirectoryURL).");
      
      throw new ResourceInstantiationException(msg.toString());
    }
    // get the default implementation class
    Class resClass = null;
    try {
      resClass = resData.getResourceClass();
    } catch(ClassNotFoundException e) {
      throw new ResourceInstantiationException(
        "Couldn't get resource class from the resource data:"+Strings.getNl()+e
      );
    }

    //create a pointer for the resource
    Resource res = null;

    //if the object is an LR and it should come from a DS then create that way
    DataStore dataStore;
    if(LanguageResource.class.isAssignableFrom(resClass) &&
       ((dataStore = (DataStore)parameterValues.
                     get(DataStore.DATASTORE_FEATURE_NAME)) != null)
      ){
      //ask the datastore to create our object
      if(dataStore instanceof SerialDataStore) {
        // SDS doesn't need a wrapper class; just check for serialisability
        if(! Serializable.class.isAssignableFrom(resClass))
          throw new ResourceInstantiationException(
            "Resource cannot be (de-)serialized: " + resClass.getName()
          );
      }

      // get the datastore instance id and retrieve the resource
      Object instanceId = parameterValues.get(DataStore.LR_ID_FEATURE_NAME);
      if(instanceId == null)
        throw new
          ResourceInstantiationException("No instance id for " + resClass);
      try {
        res = dataStore.getLr(resClass.getName(), instanceId);
      } catch(PersistenceException pe) {
        throw new ResourceInstantiationException("Bad read from DB: " + pe);
      } catch(SecurityException se) {
        throw new ResourceInstantiationException("Insufficient permissions: " + se);
      }
      resData.addInstantiation(res);
      if(features != null){
        if(res.getFeatures() == null){
          res.setFeatures(newFeatureMap());
        }
        res.getFeatures().putAll(features);
      }

      //set the name
      if(res.getName() == null){
        res.setName(resourceName == null ?
                    resData.getName() + "_" + Gate.genSym() :
                    resourceName);
      }

      // fire the event
      creoleProxy.fireResourceLoaded(
        new CreoleEvent(res, CreoleEvent.RESOURCE_LOADED)
      );

      return res;
    }

    //The resource is not a persistent LR; use a constructor

    // create an object using the resource's default constructor
    try {
      if(DEBUG) Out.prln("Creating resource " + resClass.getName());
      res = (Resource) resClass.newInstance();
    } catch(IllegalAccessException e) {
      throw new ResourceInstantiationException(
        "Couldn't create resource instance, access denied: " + e
      );
    } catch(InstantiationException e) {
      throw new ResourceInstantiationException(
        "Couldn't create resource instance due to newInstance() failure: " + e
      );
    }


    if(LanguageResource.class.isAssignableFrom(resClass)) {
      // type-specific stuff for LRs
      if(DEBUG) Out.prln(resClass.getName() + " is a LR");
    } else if(ProcessingResource.class.isAssignableFrom(resClass)) {
      // type-specific stuff for PRs
      if(DEBUG) Out.prln(resClass.getName() + " is a PR");
      //set the runtime parameters to their defaults
      try{
        FeatureMap parameters = newFeatureMap();
        parameters.putAll(resData.getParameterList().getRuntimeDefaults());
        res.setParameterValues(parameters);
      }catch(ParameterException pe){
        throw new ResourceInstantiationException(
                  "Could not set the runtime parameters " +
                  "to their default values for: " + res.getClass().getName() +
                  " :\n" + pe.toString()
                  );
      }
    // type-specific stuff for VRs
    } else if(VisualResource.class.isAssignableFrom(resClass)) {
      if(DEBUG) Out.prln(resClass.getName() + " is a VR");
    } else if(Controller.class.isAssignableFrom(resClass)){
      //type specific stuff for Controllers
    }


    //set the parameterValues of the resource
    try{
      FeatureMap parameters = newFeatureMap();
      //put the defaults
      parameters.putAll(resData.getParameterList().getInitimeDefaults());
      //overwrite the defaults with the user provided values
      parameters.putAll(parameterValues);
      res.setParameterValues(parameters);
    }catch(ParameterException pe){
        throw new ResourceInstantiationException(
                    "Could not set the init parameters for: " +
                    res.getClass().getName() + " :\n" + pe.toString()
                  );
    }

    //set the name
    // if we have an explicitly provided name, use that, otherwise generate a
    // suitable name if the resource doesn't already have one
    if(resourceName != null && resourceName.trim().length() > 0) {
      res.setName(resourceName);
    }
    else if(res.getName() == null) {
      //no name provided, and the resource doesn't have a name already (e.g. calculated in init())
      // -> let's try and find a reasonable one
      try{
        //first try to get a filename from the various parameters
        URL sourceUrl = null;
        if(res instanceof SimpleDocument){
          sourceUrl = ((SimpleDocument)res).getSourceUrl();
        } else if(res instanceof AnnotationSchema){
          sourceUrl = ((AnnotationSchema)res).getXmlFileUrl();
        } else if(res.getClass().getName().startsWith("gate.creole.ontology.owlim.")){
          // get the name for the OWLIM2 ontology LR
          java.lang.reflect.Method m = resClass.getMethod("getRdfXmlURL");
          sourceUrl = (java.net.URL)m.invoke(res);
          if(sourceUrl == null){
            m = resClass.getMethod("getN3URL");
            sourceUrl = (java.net.URL)m.invoke(res);
          }
          if(sourceUrl == null){
            m = resClass.getMethod("getNtriplesURL");
            sourceUrl = (java.net.URL)m.invoke(res);
          }
          if(sourceUrl == null){
            m = resClass.getMethod("getTurtleURL");
            sourceUrl = (java.net.URL)m.invoke(res);
          }
        } else if (res.getClass().getName().startsWith("gate.creole.ontology.impl.")) {
          java.lang.reflect.Method m = resClass.getMethod("getSourceURL");
          sourceUrl = (java.net.URL)m.invoke(res);
        }
        if(sourceUrl != null){
          URI sourceURI = sourceUrl.toURI();
          resourceName = sourceURI.getPath().trim();
          if(resourceName == null ||
             resourceName.length() == 0 ||
             resourceName.equals("/")){
            //this URI has no path -> use the whole string
            resourceName = sourceURI.toString();
          }else{
            //there is a significant path value -> get the last element
            int lastSlash = resourceName.lastIndexOf('/');
            if(lastSlash >=0){
              String subStr = resourceName.substring(lastSlash + 1);
              if(subStr.trim().length() > 0) resourceName = subStr;
            }
          }
        }
      } catch (RuntimeException t) {
        //even runtime exceptions are safe to ignore at this point
      } catch(Exception t) {
        //there were problems while trying to guess a name
        //we can safely ignore them
      } finally{
        //make sure there is a name provided, whatever happened
        if(resourceName == null || resourceName.trim().length() == 0){
          resourceName = resData.getName();
        }
      }
      resourceName += "_" + Gate.genSym();
      res.setName(resourceName);
    } // else if(res.getName() == null)
    // if res.getName() != null, leave it as it is

    Map<String, EventListener> listeners = new HashMap<String, EventListener>(gate.Gate.getListeners());
    // set the listeners if any
    if(listeners != null && !listeners.isEmpty()) {
      try {
        if(DEBUG) Out.prln("Setting the listeners for  " + res.toString());
        AbstractResource.setResourceListeners(res, listeners);
      } catch(Exception e) {
        if(DEBUG) Out.prln("Failed to set listeners for " + res.toString());
        throw new
          ResourceInstantiationException("Parameterisation failure" + e);
      }
    }

    // if the features of the resource have not been explicitly set,
    // set them to the features of the resource data
    if(res.getFeatures() == null || res.getFeatures().isEmpty()){
      FeatureMap fm = newFeatureMap();
      fm.putAll(resData.getFeatures());
      res.setFeatures(fm);
    }
    // add the features specified by the user
    if(features != null) res.getFeatures().putAll(features);

    // initialise the resource
    if(DEBUG) Out.prln("Initialising resource " + res.toString());
    res = res.init();

    // remove the listeners if any
    if(listeners != null && !listeners.isEmpty()) {
      try {
        if(DEBUG) Out.prln("Removing the listeners for  " + res.toString());
        AbstractResource.removeResourceListeners(res, listeners);
      } catch(Exception e) {
        if (DEBUG) Out.prln(
          "Failed to remove the listeners for " + res.toString()
        );
        throw new
          ResourceInstantiationException("Parameterisation failure" + e);
      }
    }
    // record the instantiation on the resource data's stack
    resData.addInstantiation(res);
    // fire the event
    creoleProxy.fireResourceLoaded(
      new CreoleEvent(res, CreoleEvent.RESOURCE_LOADED)
    );
    return res;
  } // create(resourceClassName, parameterValues, features, listeners)

  /** Delete an instance of a resource. This involves removing it from
    * the stack of instantiations maintained by this resource type's
    * resource data. Deletion does not guarantee that the resource will
    * become a candidate for garbage collection, just that the GATE framework
    * is no longer holding references to the resource.
    *
    * @param resource the resource to be deleted.
    */
  public static void deleteResource(Resource resource) {
    ResourceData rd =
      (ResourceData) Gate.getCreoleRegister().get(resource.getClass().getName());
    if(rd!= null && rd.removeInstantiation(resource)) {
      creoleProxy.fireResourceUnloaded(
        new CreoleEvent(resource, CreoleEvent.RESOURCE_UNLOADED)
      );
      resource.cleanup();
    }
  } // deleteResource

  /** Create a new transient Corpus. */
  public static Corpus newCorpus(String name)
                                          throws ResourceInstantiationException
  {
    return (Corpus) createResource("gate.corpora.CorpusImpl", newFeatureMap(), newFeatureMap(), name);
  } // newCorpus

  /** Create a new transient Document from a URL. */
  public static Document newDocument(URL sourceUrl)
                                          throws ResourceInstantiationException
  {
    FeatureMap parameterValues = newFeatureMap();
    parameterValues.put(Document.DOCUMENT_URL_PARAMETER_NAME, sourceUrl);
    return
      (Document) createResource("gate.corpora.DocumentImpl", parameterValues);
  } // newDocument(URL)

  /** Create a new transient Document from a URL and an encoding. */
  public static Document newDocument(URL sourceUrl, String encoding)
                                          throws ResourceInstantiationException
  {
    FeatureMap parameterValues = newFeatureMap();
    parameterValues.put(Document.DOCUMENT_URL_PARAMETER_NAME, sourceUrl);
    parameterValues.put(Document.DOCUMENT_ENCODING_PARAMETER_NAME, encoding);
    return
      (Document) createResource("gate.corpora.DocumentImpl", parameterValues);
  } // newDocument(URL)

  /** Create a new transient textual Document from a string. */
  public static Document newDocument(String content)
                                          throws ResourceInstantiationException
  {
    FeatureMap params = newFeatureMap();
    params.put(Document.DOCUMENT_STRING_CONTENT_PARAMETER_NAME, content);
    Document doc =
      (Document) createResource("gate.corpora.DocumentImpl", params);
/*
    // laziness: should fit this into createResource by adding a new
    // document parameter, but haven't time right now...
    doc.setContent(new DocumentContentImpl(content));
*/
    // various classes are in the habit of assuming that a document
    // inevitably has a source URL...  so give it a dummy one
/*    try {
      doc.setSourceUrl(new URL("http://localhost/"));
    } catch(MalformedURLException e) {
      throw new ResourceInstantiationException(
        "Couldn't create dummy URL in newDocument(String): " + e
      );
    }
*/
    doc.setSourceUrl(null);
    return doc;
  } // newDocument(String)

  
  /**
   * Utility method to create an immutable annotation set. If the provided 
   * collection of annotations is <code>null</null>, the newly created set will
   * be empty.
   * @param document the document this set belongs to. 
   * @param annotations the set of annotations that should be contained in the
   * returned {@link AnnotationSet}. 
   * @return an {@link AnnotationSet} that throws exceptions on all attempts 
   * to modify it.
   */
  public static AnnotationSet createImmutableAnnotationSet(Document document, 
          Collection<Annotation> annotations) {
    return new ImmutableAnnotationSetImpl(document, annotations);
  }
  
  /**
   * <p>
   * Create a <i>duplicate</i> of the given resource.  A duplicate is a
   * an independent copy of the resource that has the same name and the
   * same behaviour.  It does <i>not necessarily</i> have the same concrete
   * class as the original, but if the original resource implements any of
   * the following interfaces then the duplicate can be assumed to
   * implement the same ones:
   * </p>
   * <ul>
   * <li>{@link ProcessingResource}</li>
   * <li>{@link LanguageAnalyser}</li>
   * <li>{@link Controller}</li>
   * <li>{@link CorpusController}</li>
   * <li>{@link ConditionalController}</li>
   * <li>{@link Gazetteer}</li>
   * <li>{@link LanguageResource}</li>
   * <li>{@link gate.creole.ontology.Ontology}</li>
   * <li>{@link Document}</li>
   * <li>{@link Corpus}</li>
   * </ul>
   * <p>
   * The default duplication algorithm simply calls
   * {@link #createResource(String, FeatureMap, FeatureMap, String) createResource}
   * with the type and name of the original resource, and with parameters
   * and features which are copies of those from the original resource,
   * but any Resource values in the maps will themselves be duplicated.
   * A context is passed around all the duplicate calls that stem from the
   * same call to this method so that if the same resource is referred to
   * in different places, the same duplicate can be used in the
   * corresponding places in the duplicated object graph.
   * </p>
   * <p>
   * This default behaviour is sufficient for most resource types (and
   * is roughly the equivalent of saving the resource's state using the
   * persistence manager and then reloading it), but individual resource
   * classes can override it by implementing the {@link CustomDuplication}
   * interface.  This may be necessary for semantic reasons (e.g.
   * controllers need to recursively duplicate the PRs they contain),
   * or desirable for performance or memory consumption reasons (e.g. the
   * behaviour of a DefaultGazetteer can be duplicated by a
   * SharedDefaultGazetteer that shares the internal data structures).
   * </p>
   *
   * @param res the resource to duplicate
   * @return an independent duplicate copy of the resource
   * @throws ResourceInstantiationException if an exception occurs while
   *         constructing the duplicate.
   */
  public static Resource duplicate(Resource res)
          throws ResourceInstantiationException {
    DuplicationContext ctx = new DuplicationContext();
    try {
      return duplicate(res, ctx);
    }
    finally {
      // de-activate the context
      ctx.active = false;
    }
  }

  private static long dupIndex = 0;
  
  /**
   * Create a duplicate of the given resource, using the provided context.
   * This method is intended for use by resources that implement the
   * {@link CustomDuplication} interface when they need to duplicate
   * their child resources.  Calls made to this method outside the scope of
   * such a {@link CustomDuplication#duplicate CustomDuplication.duplicate}
   * call will fail with a runtime exception.
   *
   * @see #duplicate(Resource)
   * @param res the resource to duplicate
   * @param ctx the current context as passed to the
   *         {@link CustomDuplication#duplicate} method.
   * @return the duplicated resource
   * @throws ResourceInstantiationException if an error occurs while
   *         constructing the duplicate.
   */
  public static Resource duplicate(Resource res,
          DuplicationContext ctx)
            throws ResourceInstantiationException {
    long myDupIndex = -1, startTime = -1;
    if(DEBUG_DUPLICATION) {
      myDupIndex = dupIndex++;
      log.debug(myDupIndex + ": Duplicating \"" + ((res == null) ? "null" : res.getName())
          + "\" (a " + ((res == null) ? "null" : res.getClass().getName()) + ")");
      startTime = System.currentTimeMillis();
    }
    try {
      checkDuplicationContext(ctx);
      // check for null
      if(res == null) {
        return null;
      }
      // check if we've seen this resource before
      else if(ctx.knownResources.containsKey(res)) {
        if(DEBUG_DUPLICATION) {
          log.debug(myDupIndex + ": Resource already duplicated in context");
        }
        return ctx.knownResources.get(res);
      }
      else {
        // create the duplicate
        Resource newRes = null;
        if(res instanceof CustomDuplication) {
          // use custom duplicate if available
          newRes = ((CustomDuplication)res).duplicate(ctx);
        }
        else {
          newRes = defaultDuplicate(res, ctx);
        }
        // remember this duplicate in the context
        ctx.knownResources.put(res, newRes);
        return newRes;
      }
    } finally {
      if(DEBUG_DUPLICATION) {
        log.debug(myDupIndex + ": Duplication took " + (System.currentTimeMillis() - startTime) + " ms");
      }
    }
  }

  /**
   * Implementation of the default duplication algorithm described
   * in the comment for {@link #duplicate(Resource)}.  This method is
   * public for the benefit of resources that implement
   * {@link CustomDuplication} but only need to do some post-processing
   * after the default duplication algorithm; they can call this method
   * to obtain an initial duplicate and then post-process it before
   * returning.  If they need to duplicate child resources they should
   * call {@link #duplicate(Resource, DuplicationContext)} in the normal
   * way.  Calls to this method made outside the context of a
   * {@link CustomDuplication#duplicate CustomDuplication.duplicate}
   * call will fail with a runtime exception.
   *
   * @param res the resource to duplicate
   * @param ctx the current context
   * @return a duplicate of the given resource, constructed using the
   *         default algorithm.  In particular, if <code>res</code>
   *         implements {@link CustomDuplication} its own duplicate method
   *         will <i>not</i> be called.
   * @throws ResourceInstantiationException if an error occurs
   *         while duplicating the given resource.
   */
  public static Resource defaultDuplicate(Resource res,
          DuplicationContext ctx)
            throws ResourceInstantiationException {
    checkDuplicationContext(ctx);
    String className = res.getClass().getName();
    ResourceData resData = Gate.getCreoleRegister().get(className);
    if(resData == null) {
      throw new ResourceInstantiationException(
          "Could not find CREOLE data for " + className);
    }
    String resName = res.getName();

    FeatureMap newResFeatures = duplicate(res.getFeatures(), ctx);

    // init parameters
    FeatureMap initParams = AbstractResource.getInitParameterValues(res);
    // remove parameters that are also sharable properties
    for(String propName : resData.getSharableProperties()) {
      initParams.remove(propName);
    }
    // duplicate any Resources in the params map (excluding sharable ones)
    initParams = duplicate(initParams, ctx);
    // add sharable properties to the params map (unduplicated).  Some of these
    // may be registered parameters but others may not be.
    for(String propName : resData.getSharableProperties()) {
      initParams.put(propName, res.getParameterValue(propName));
    }

    // create the new resource
    Resource newResource = createResource(className, initParams, newResFeatures, resName);
    if(newResource instanceof ProcessingResource) {
      // runtime params
      FeatureMap runtimeParams = AbstractProcessingResource.getRuntimeParameterValues(res);
      // remove parameters that are also sharable properties
      for(String propName : resData.getSharableProperties()) {
        runtimeParams.remove(propName);
      }
      // duplicate any Resources in the params map (excluding sharable ones)
      runtimeParams = duplicate(runtimeParams, ctx);
      // do not need to add sharable properties here, they have already
      // been injected by createResource
      
      newResource.setParameterValues(runtimeParams);
    }

    return newResource;
  }

  /**
   * Construct a feature map that is a copy of the one provided except
   * that any {@link Resource} values in the map are replaced by their
   * duplicates.  This method is public for the benefit of resources
   * that implement {@link CustomDuplication} and will fail if called
   * outside of a
   * {@link CustomDuplication#duplicate CustomDuplication.duplicate}
   * implementation.
   *
   * @param fm the feature map to duplicate
   * @param ctx the current context
   * @return a duplicate feature map
   * @throws ResourceInstantiationException if an error occurs while
   *         duplicating any Resource in the feature map.
   */
  public static FeatureMap duplicate(FeatureMap fm,
          DuplicationContext ctx)
            throws ResourceInstantiationException {
    checkDuplicationContext(ctx);
    FeatureMap newFM = Factory.newFeatureMap();
    for(Map.Entry<Object, Object> entry : fm.entrySet()) {
      Object value = entry.getValue();
      if(value instanceof Resource) {
        value = duplicate((Resource)value, ctx);
      }
      newFM.put(entry.getKey(), value);
    }
    return newFM;
  }

  /**
   * Opaque memo object passed to
   * {@link CustomDuplication#duplicate CustomDuplication.duplicate}
   * methods to encapsulate the state of the current duplication run.
   * If the duplicate method itself needs to duplicate any objects it
   * should pass this context back to
   * {@link #duplicate(Resource,DuplicationContext)}.
   */
  public static class DuplicationContext {
    IdentityHashMap<Resource, Resource> knownResources =
      new IdentityHashMap<Resource, Resource>();

    /**
     * Whether this duplication context is part of an active duplicate
     * call.
     */
    boolean active = true;

    /**
     * Overridden to ensure no public constructor.
     */
    DuplicationContext() {
    }
  }

  /**
   * Throws an exception if the specified duplication context is
   * null or not active.  This is to ensure that the Factory
   * helper methods that take a DuplicationContext parameter can
   * only be called in the context of a
   * {@link #duplicate(Resource)} call.
   * @param ctx the context to check.
   * @throws NullPointerException if the provided context is null.
   * @throws IllegalStateException if the provided context is not
   *         active.
   */
  protected static void checkDuplicationContext(DuplicationContext ctx) {
    if(ctx == null) {
      throw new NullPointerException("No DuplicationContext provided");
    }
    if(!ctx.active) {
      throw new IllegalStateException(
              new Throwable().getStackTrace()[1].getMethodName()
              + " helper method called outside an active duplicate call");
    }
  }

  static Class<ParseCpsl> japeParserClass = ParseCpsl.class;
  public static Class<ParseCpsl> getJapeParserClass() {
      return japeParserClass;
  }
  @SuppressWarnings("unchecked")
  public static void setJapeParserClass(Class newClass) {
      if (! ParseCpsl.class.isAssignableFrom(newClass))
          throw new IllegalArgumentException("Parser class must inherit from " + ParseCpsl.class);
      japeParserClass = newClass;
  }

  public static ParseCpsl newJapeParser(java.io.Reader stream, Map existingMacros) {
      try {
          Constructor<ParseCpsl> c = japeParserClass.getConstructor
              (new Class[] {java.io.Reader.class, existingMacros.getClass()});
          return (ParseCpsl) c.newInstance(new Object[] {stream, existingMacros});
      } catch (NoSuchMethodException e) { // Shouldn't happen
          throw new RuntimeException(e);
      } catch (IllegalArgumentException e) { // Shouldn't happen
          throw new RuntimeException(e);
      } catch (InstantiationException e) { // Shouldn't happen
          throw new RuntimeException(e);
      } catch (IllegalAccessException e) { // Shouldn't happen
          throw new RuntimeException(e);
      } catch (InvocationTargetException e) { // Happens if the constructor throws an exception
          throw new RuntimeException(e);
      }
  }

  public static ParseCpsl newJapeParser(URL japeURL, String encoding) throws IOException {
    // the stripping stream is buffered, no need to buffer the URL stream.
      java.io.Reader stream = new BomStrippingInputStreamReader(japeURL.openStream(), encoding);

      ParseCpsl parser = newJapeParser(stream, new HashMap());
      parser.setBaseURL(japeURL);
      parser.setEncoding(encoding);
      return parser;
  }

  /**
   * Active ConstraintFactory for creating and initializing Jape <b>Constraint</b>s.
   */
  private static ConstraintFactory japeConstraintFactory = new ConstraintFactory();

  /**
   * Return the active {@link ConstraintFactory} for creating and initializing Jape
   * <b>Constraint</b>s.
   */
  public static ConstraintFactory getConstraintFactory() {
    return japeConstraintFactory;
  }

  /** Create a new FeatureMap. */
  public static FeatureMap newFeatureMap() {
    return new SimpleFeatureMapImpl();
  } // newFeatureMap

  /** Open an existing DataStore. */
  public static DataStore openDataStore(
    String dataStoreClassName, String storageUrl
  ) throws PersistenceException {
    DataStore ds = instantiateDataStore(dataStoreClassName, storageUrl);
    ds.open();
    if(Gate.getDataStoreRegister().add(ds))
      creoleProxy.fireDatastoreOpened(
        new CreoleEvent(ds, CreoleEvent.DATASTORE_OPENED)
      );

    return ds;
  } // openDataStore()

  /** Create a new DataStore and open it. <B>NOTE:</B> for some data stores
    * creation is an system administrator task; in such cases this
    * method will throw an UnsupportedOperationException.
    */
  public static DataStore createDataStore(
    String dataStoreClassName, String storageUrl
  ) throws PersistenceException, UnsupportedOperationException {
    DataStore ds = instantiateDataStore(dataStoreClassName, storageUrl);
    ds.create();
    ds.open();
    if(Gate.getDataStoreRegister().add(ds))
      creoleProxy.fireDatastoreCreated(
        new CreoleEvent(ds, CreoleEvent.DATASTORE_CREATED)
      );

    return ds;
  } // createDataStore()

  /** Instantiate a DataStore (not open or created). */
  protected static DataStore instantiateDataStore(
    String dataStoreClassName, String storageUrl
  ) throws PersistenceException {
    DataStore godfreyTheDataStore = null;
    try {
      godfreyTheDataStore =
        (DataStore) Gate.getClassLoader().
                    loadClass(dataStoreClassName).newInstance();
    } catch(Exception e) {
      throw new PersistenceException("Couldn't create DS class: " + e);
    }

    godfreyTheDataStore.setStorageUrl(storageUrl.toString());

    return godfreyTheDataStore;
  } // instantiateDS(dataStoreClassName, storageURL)

  /** Add a listener */
  public static synchronized void addCreoleListener(CreoleListener l){
    creoleProxy.addCreoleListener(l);
  } // addCreoleListener(CreoleListener)

  /** Static initialiser to set up the CreoleProxy event source object */
  static {
    creoleProxy = new CreoleProxy();
  } // static initialiser

} // abstract Factory


/**
 * Factory is basically a collection of static methods but events need to
 * have as source an object and not a class. The CreolProxy class addresses
 * this issue acting as source for all events fired by the Factory class.
 */
class CreoleProxy {

  public synchronized void removeCreoleListener(CreoleListener l) {
    if (creoleListeners != null && creoleListeners.contains(l)) {
      @SuppressWarnings("unchecked")
      Vector<CreoleListener> v = (Vector<CreoleListener>) creoleListeners.clone();
      v.removeElement(l);
      creoleListeners = v;
    }// if
  }// removeCreoleListener(CreoleListener l)

  public synchronized void addCreoleListener(CreoleListener l) {
    @SuppressWarnings("unchecked")
    Vector<CreoleListener> v =
      creoleListeners == null ? new Vector<CreoleListener>(2) : (Vector<CreoleListener>) creoleListeners.clone();
    if (!v.contains(l)) {
      v.addElement(l);
      creoleListeners = v;
    }// if
  }// addCreoleListener(CreoleListener l)

  protected void fireResourceLoaded(CreoleEvent e) {
    if (creoleListeners != null) {
      int count = creoleListeners.size();
      for (int i = 0; i < count; i++) {
        creoleListeners.elementAt(i).resourceLoaded(e);
      }// for
    }// if
  }// fireResourceLoaded(CreoleEvent e)

  protected void fireResourceUnloaded(CreoleEvent e) {
    if (creoleListeners != null) {
      int count = creoleListeners.size();
      for (int i = 0; i < count; i++) {
        creoleListeners.elementAt(i).resourceUnloaded(e);
      }// for
    }// if
  }// fireResourceUnloaded(CreoleEvent e)

  protected void fireDatastoreOpened(CreoleEvent e) {
    if (creoleListeners != null) {
      int count = creoleListeners.size();
      for (int i = 0; i < count; i++) {
        creoleListeners.elementAt(i).datastoreOpened(e);
      }// for
    }// if
  }// fireDatastoreOpened(CreoleEvent e)

  protected void fireDatastoreCreated(CreoleEvent e) {
    if (creoleListeners != null) {
      int count = creoleListeners.size();
      for (int i = 0; i < count; i++) {
        creoleListeners.elementAt(i).datastoreCreated(e);
      }// for
    }// if
  }// fireDatastoreCreated(CreoleEvent e)

  protected void fireDatastoreClosed(CreoleEvent e) {
    if (creoleListeners != null) {
      int count = creoleListeners.size();
      for (int i = 0; i < count; i++) {
        creoleListeners.elementAt(i).datastoreClosed(e);
      }// for
    }// if
  }// fireDatastoreClosed(CreoleEvent e)

  private transient Vector<CreoleListener> creoleListeners;
}//class CreoleProxy