/*
 *  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 20037 2017-02-01 06:17:21Z markagreenwood $
 */

package gate;

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.Plugin;
import gate.creole.ResourceData;
import gate.creole.ResourceInstantiationException;
import gate.event.CreoleEvent;
import gate.event.CreoleListener;
import gate.persist.PersistenceException;
import gate.persist.SerialDataStore;
import gate.util.Out;
import gate.util.SimpleFeatureMapImpl;
import gate.util.Strings;

import java.io.Serializable;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 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 = LoggerFactory.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 = Gate.getCreoleRegister().get(resourceClassName);
    if(resData == null) {
      Set<Plugin> plugins = Gate.getPlugins(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(Plugin 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("\"registerPlugin\" on Gate.getCreoleRegister()");

      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 = Gate.getCreoleRegister().get(resourceClassName);
    if(resData == null) {
      Set<Plugin> plugins = Gate.getPlugins(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(Plugin 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().registerPlugin(plugin).");

      throw new ResourceInstantiationException(msg.toString());
    }
    // get the default implementation class
    Class<? extends Resource> 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 = 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
      if(DEBUG) Out.prln(resClass.getName() + " is a Controller");
    }

    // 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().toURL();
        } 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();
          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
            resourceName = resourceName.trim();
            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.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);
      }
    }

    try {
      // 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();

    } finally {
      // remove the listeners if any
      if(!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 =
            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</code>, 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>{@code 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");
    }
  }

  /** 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);

    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