/***********************************************************
Copyright (C) 2004 VeriSign, Inc.

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

http://www.verisign.com/nds/naming/namestore/techdocs.html
***********************************************************/
package com.verisign.epp.codec.gen;

import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.Vector;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Element;

/**
 * {@code EPPFactory} is a Singleton class that will create {@code EPPCommand}
 * or {@code EPPResponse} instances for any of the supported EPP Command
 * Mappings (e.g. domain, host, contact). When a create method is called,
 * {@code EPPFactory} will use the XML namespace of the element to call the
 * appropriate concrete {@code EPPMapFactory}, which will create the correct
 * concrete {@code EPPCommand} or {@code EPPResponse}.
 * {@code createCommand(String, String)} is provided to instantiate
 * {@code EPPCommand} objects associated with the general EPP Specification,
 * which includes {@code EPPLoginCmd} and {@code EPPLogout}. <br>
 * <br>
 * {@code EPPFactory} is initialized with the set of available EPP Command
 * Mappings. A method is provided to retrieve the list {@code EPPService}
 * descriptions of available EPP Command Mappings. The list of available EPP
 * Command Mappings can be used in {@code EPPGreeting} and in
 * {@code EPPLoginCmd}. <br>
 *
 * @see com.verisign.epp.codec.gen.EPPMapFactory
 * @see com.verisign.epp.codec.gen.EPPGreeting
 * @see com.verisign.epp.codec.gen.EPPLoginCmd
 */
public class EPPFactory {
  /** Single EPPFactory instance. */
  private static EPPFactory instance = new EPPFactory();

  /** SLF4J Logger */
  private static Logger cat = LoggerFactory.getLogger(EPPFactory.class);

  /** Hash of EPP Command Mappings indexed by XML Namespace URI. */
  private Hashtable<String, EPPMapFactory> factories = new Hashtable<String, EPPMapFactory>();

  /** Hash of EPP Extension Factories indexed by XML Namespace URI. */
  private Hashtable<String, EPPExtFactory> extFactories = new Hashtable<String, EPPExtFactory>();

  /**
   * Ordered list of registered map factories based on registration order. This
   * is needed to maintain the order of the registered factories for
   * pre-loading the XML schemas.
   */
  private Set<EPPMapFactory> factoriesSet = new LinkedHashSet<EPPMapFactory>();

  /**
   * Ordered list of registered ext factories based on registration order. This
   * is needed to maintain the order of the registered ext factories for
   * pre-loading the XML schemas.
   */
  private Set<EPPExtFactory> extFactoriesSet = new LinkedHashSet<EPPExtFactory>();

  /**
   * Protected constructor following the Singleton Design Pattern.
   */
  protected EPPFactory() {
  }

  /**
   * Singleton Design Pattern method to retrieve the single {@code EPPFactory}
   * instance.
   *
   * @return Single {@code EPPFactory} instance.
   */
  public static EPPFactory getInstance() {
    return instance;
  }

  /**
   * creates a concrete {@code EPPCommand} given a command type and a command
   * operation. This method should only be used for commands that don't have an
   * EPP Namespace extension. The only commands currently supported include
   * {@code EPPCommand.TYPE_LOGIN} and {@code EPPCommand.EPP_LOGOUT}.
   *
   * @param aCommandType
   *           {@code Command.TYPE_} constant associated with the
   *           {@code EPPCommand}.
   * @param aOp
   *           {@code Command.OP_} constant associated with {@code EPPCommand}.
   *           This should be {@code null} if there is no operation value
   *           associated with the command.
   *
   * @return Concrete command associated with the command tag and optional
   *         operation tag
   *
   * @exception EPPCodecException
   *               Unable to create command map.
   */
  public EPPCommand createCommand(String aCommandType, String aOp) throws EPPCodecException {
    String commandTypeLocalName = EPPUtil.getLocalName(aCommandType);

    if (commandTypeLocalName.equals(EPPUtil.getLocalName(EPPCommand.TYPE_LOGIN))) {
      return new EPPLoginCmd();
    }
    else if (commandTypeLocalName.equals(EPPUtil.getLocalName(EPPCommand.TYPE_LOGOUT))) {
      return new EPPLogoutCmd();
    }
    else if (commandTypeLocalName.equals(EPPUtil.getLocalName(EPPCommand.TYPE_POLL))) {
      return new EPPPollCmd();
    }
    else {
      throw new EPPCodecException("Invalid command type " + commandTypeLocalName + " in EPPFactory.createCommand");
    }
  }

  /**
   * creates a concrete {@code EPPCommand} given a DOM Command Mapping Element.
   * The DOM Command Mapping Element must have an XML namespace URI, which is
   * used to determine the concrete {@code EPPMapFactory} to use to create the
   * {@code EPPCommand}.
   *
   * @param aMapElement
   *           The DOM Element associated with the command mapping.
   *
   * @return EPPCommand Concrete {@code EPPCommand} associated with
   *         {@code aMapElement} parameter.
   *
   * @exception EPPCodecException
   *               Unable to create concrete {@code EPPCommand}.
   */
  public EPPCommand createCommand(Element aMapElement) throws EPPCodecException {
    String nsUri = aMapElement.getNamespaceURI();

    EPPMapFactory target = (EPPMapFactory) factories.get(nsUri);

    if (target == null) {
      throw new EPPCodecException("Unable to find factory for namespace " + aMapElement.getNamespaceURI());
    }

    return target.createCommand(aMapElement);
  }

  /**
   * creates a concrete {@code EPPResponse} given a DOM Command Mapping
   * Element. The DOM Command Mapping Element must have an XML Namespace URI,
   * which is used to determine the concrete {@code EPPMapFactory} to use to
   * create the {@code EPPResponse}.
   *
   * @param aMapElement
   *           The DOM Element associated with the command mapping.
   *
   * @return EPPCommand Concrete {@code EPPResponse} associated with
   *         {@code aMapElement} parameter.
   *
   * @exception EPPCodecException
   *               Unable to create concrete {@code EPPCommand}.
   */
  public EPPResponse createResponse(Element aMapElement) throws EPPCodecException {
    EPPMapFactory target = (EPPMapFactory) factories.get(aMapElement.getNamespaceURI());

    if (target == null) {
      throw new EPPCodecException("Unable to find factory for namespace " + aMapElement.getNamespaceURI());
    }

    return target.createResponse(aMapElement);
  }

  /**
   * creates a concrete {@code EPPCommand} given a DOM Command Mapping Element.
   * The DOM Command Mapping Element must have an XML namespace URI, which is
   * used to determine the concrete {@code EPPMapFactory} to use to create the
   * {@code EPPCommand}.
   *
   * @param aExtensionElm
   *           The DOM Element associated with the command mapping.
   *
   * @return EPPCommand Concrete {@code EPPCommand} associated with
   *         {@code aMapElement} parameter.
   *
   * @exception EPPCodecException
   *               Unable to create concrete {@code EPPCommand}.
   */
  public EPPCodecComponent createExtension(Element aExtensionElm) throws EPPCodecException {
    String nsUri = aExtensionElm.getNamespaceURI();

    EPPExtFactory target = (EPPExtFactory) extFactories.get(nsUri);

    if (target == null) {
      throw new EPPCodecException(
            "Unable to find extension factory for namespace " + aExtensionElm.getNamespaceURI());
    }

    return target.createExtension(aExtensionElm);
  }

  /**
   * creates a concrete {@code EPPProtocolExtension} given a DOM protocol
   * extension Element. The extension element must have an XML namespace URI,
   * which is used to determine the concrete {@code EPPExtFactory} to use to
   * create the {@code EPPProtocolExtension}.
   *
   * @param aExtensionElm
   *           The Protocol extension Element .
   *
   * @return EPPProtocolExtension.
   *
   * @exception EPPCodecException
   *               Unable to create concrete {@code EPPCommand}.
   */
  public EPPProtocolExtension createProtocolExtension(Element aExtensionElm) throws EPPCodecException {
    String nsUri = aExtensionElm.getNamespaceURI();

    EPPExtFactory target = (EPPExtFactory) extFactories.get(nsUri);

    if (target == null) {
      throw new EPPCodecException(
            "Unable to find extension factory for namespace " + aExtensionElm.getNamespaceURI());
    }

    return target.createProtocolExtension(aExtensionElm);
  }

  /**
   * Gets a {@code Vector} of {@link EPPService} objects that represent the supported EPP
   * Command Mappings. EPP can support a dynamic set of command mappings, and
   * each command mapping will have an {@link EPPService} object that contains
   * information about the command mapping.
   *
   * @return {@code Vector} of {@link EPPService} objects, each representing an EPP
   *         Command Mapping.
   */
  public Vector<EPPService> getServices() {
    Vector<EPPService> retServices = new Vector<EPPService>();

    Enumeration<EPPMapFactory> factoryElms = factories.elements();

    while (factoryElms.hasMoreElements()) {
      EPPMapFactory currFactory = factoryElms.nextElement();

      retServices.addElement(currFactory.getService());
    }

    return retServices;
  }

  /**
   * Gets a Vector of EPPService objects that represent the supported EPP
   * Extensions. EPP can support a dynamic set of command and response
   * extensions. an {@code EPPService}. object that contains information about
   * the extension.
   *
   * @return {@code Vector} of {@code EPPService} objects, each representing an
   *         EPP Extension.
   */
  public Vector<EPPService> getExtensions() {
    Vector<EPPService> retServices = new Vector<EPPService>();

    Enumeration<EPPExtFactory> factoryElms = extFactories.elements();

    while (factoryElms.hasMoreElements()) {
      EPPExtFactory currFactory = factoryElms.nextElement();

      retServices.addElement(currFactory.getService());
    }

    return retServices;
  }

  /**
   * Does the EPP support a service specified by the service namespace URI? For
   * example, the service namespace URI for the Domain Command Mapping is
   * "urn:iana:xmlns:domain".
   *
   * @param aNamespace
   *           XML Namespace URI associated with service. For example
   *           "urn:iana:xmlns:domain" for the Domain Command Mapping.
   *
   * @return {@code true} if EPP supports the service; {@code false} otherwise.
   */
  public boolean hasService(String aNamespace) {
    return factories.containsKey(aNamespace);
  }

  /**
   * Does the EPP support an extension specified by the namespace URI? For
   * example, the service namespace URI for the Domain Command Mapping is
   * "urn:verisign:xmlns:pricing".
   *
   * @param aNamespace
   *           XML Namespace URI associated with extension. For example
   *           "urn:verisign:xmlns:pricing" for the Domain Command Mapping.
   *
   * @return {@code true} if EPP supports the extension; {@code false}
   *         otherwise.
   */
  public boolean hasExtension(String aNamespace) {
    return extFactories.containsKey(aNamespace);
  }

  /**
   * Initialize the Singleton EPP instance with the list of available concrete
   * EPPMapFactory's. The concrete EPPMapFactory's are specified by their fully
   * qualified class names. For example, the EPPDomainMapFactory can be
   * initialized in {@code EPPFactory} with the following calls:<br>
   * 
   * <pre>
   * Vector theFactories = new Vector();
   * theFactories.addElement("com.verisign.epp.codec.domain.EPPDomainMapFactory");
   * EPPFactory.getInstance().init(theFactories);
   * </pre>
   *
   * @param aMapFactories
   *           a Vector of concrete {@code EPPMapFactory} fully qualified class
   *           names.
   *
   * @exception EPPCodecException
   *               Error initializing {@code EPPFactory}
   */
  public void init(Vector<String> aMapFactories) throws EPPCodecException {
    cat.debug("init(Vector): enter");

    EPPService currService = null;
    EPPMapFactory currFactory = null;
    String currFactoryName = null;

    factories = new Hashtable<String, EPPMapFactory>();

    // For each concrete EPPMapFactory
    for (int i = 0; i < aMapFactories.size(); i++) {
      currFactoryName = aMapFactories.elementAt(i);
      cat.debug("init(Vector): Add Map Factory <" + currFactoryName + ">");
      addMapFactory(currFactoryName);
    }

    // end for each factory
    extFactories = new Hashtable<String, EPPExtFactory>();

    cat.debug("init(Vector): exit");
  }

  /**
   * Initialize the Singleton EPP instance with the list of available concrete
   * {@code EPPMapFactory}'s and concrete {@code EPPExtFactory}'s. The concrete
   * {@code EPPMapFactory}'s and {@code EPPExtFactory}'s are specified by their
   * fully qualified class names. For example, the EPPDomainMapFactory and
   * EPPPricingExtFactory can be initialized in {@code EPPFactory} with the
   * following calls:<br>
   * 
   * <pre>
   * Vector theMapFactories = new Vector();
   * theMapFactories.addElement("com.verisign.epp.codec.domain.EPPDomainMapFactory");
   * theExtFactories = new Vector();
   * theExtFactories.addElement("com.verisign.epp.codec.pricing.EPPPricingExtFactory");
   * EPPFactory.getInstance().init(theMapFactories, theExtFactories);
   * </pre>
   *
   * @param aMapFactories
   *           a {@code Vector} of concrete {@code EPPMapFactory} fully
   *           qualified class names.
   * @param aExtFactories
   *           a {@code Vector} of concrete {@code EPPExtFactory} fully
   *           qualified class names.
   *
   * @exception EPPCodecException
   *               Error initializing {@code EPPFactory}
   */
  public void init(Vector<String> aMapFactories, Vector<String> aExtFactories) throws EPPCodecException {
    cat.debug("init(Vector, Vector): enter");

    // Initialize the EPPMapFactory's
    init(aMapFactories);

    EPPService currService = null;
    EPPExtFactory currFactory = null;
    String currFactoryName = null;

    // For each concrete EPPExtFactory
    for (int i = 0; i < aExtFactories.size(); i++) {
      currFactoryName = aExtFactories.elementAt(i);
      cat.debug("init(Vector, Vector): Add Ext Factory <" + currFactoryName + ">");
      addExtFactory(currFactoryName);
    }

    // end for each factory
    cat.debug("init(Vector, Vector): exit");
  }

  /**
   * Add a concrete {@code EPPMapFactory} if it does not already exist in
   * {@code EPPFactory}.
   *
   * @param aMapFactory
   *           a concrete {@code EPPMapFactory} fully qualified class names.
   *
   * @exception EPPCodecException
   *               Error add {@code EPPMapFactory} to {@code EPPFactory}.
   */
  public void addMapFactory(String aMapFactory) throws EPPCodecException {
    cat.debug("addMapFactory(String): enter");

    try {
      // Does aMapFactory not already exist?
      if (!mapFactoryExists(aMapFactory)) {
        cat.info("addMapFactory(String): Loading <" + aMapFactory + ">");

        // Instantiate EPPMapFactory and add to factories Hashtable.
        Class factoryClass = Class.forName(aMapFactory);

        if (!EPPMapFactory.class.isAssignableFrom((factoryClass))) {
          throw new EPPCodecException(aMapFactory + " is not a subclass of EPPMapFactory");
        }

        EPPMapFactory theFactory = (EPPMapFactory) factoryClass.getDeclaredConstructor().newInstance();

        EPPService theService = theFactory.getService();

        if (theService == null) {
          throw new EPPCodecException(aMapFactory + " returned null on call to getService");
        }

        cat.info("addMapFactory(String): Adding <" + aMapFactory + "> with Namespace <"
              + theService.getNamespaceURI() + ">");

        factories.put(theService.getNamespaceURI(), theFactory);

        this.factoriesSet.add(theFactory);
      }
    }
    catch (Exception e) {
      throw new EPPCodecException(aMapFactory + " " + e.getClass().getName() + ": " + e);
    }

    cat.debug("addMapFactory(String): exit");
  }

  /**
   * Add a concrete {@code EPPExtFactory} if it does not already exist in
   * {@code EPPFactory}.
   *
   * @param aExtFactory
   *           a concrete {@code EPPExtFactory} fully qualified class names.
   *
   * @exception EPPCodecException
   *               Error add {@code EPPExtFactory} to {@code EPPFactory}.
   */
  public void addExtFactory(String aExtFactory) throws EPPCodecException {
    cat.debug("addExtFactory(String): enter");

    try {
      // Does aExtFactory not already exist?
      if (!extFactoryExists(aExtFactory)) {
        cat.info("addExtFactory(String): Loading <" + aExtFactory + ">");

        // Instantiate EPPExtFactory and add to factories Hashtable.
        Class factoryClass = Class.forName(aExtFactory);

        if (!EPPExtFactory.class.isAssignableFrom((factoryClass))) {
          throw new EPPCodecException(aExtFactory + " is not a subclass of EPPExtFactory");
        }

        EPPExtFactory theFactory = (EPPExtFactory) factoryClass.getDeclaredConstructor().newInstance();

        EPPService theService = theFactory.getService();

        if (theService == null) {
          throw new EPPCodecException(aExtFactory + " returned null on call to getService");
        }

        cat.info("addExtFactory(String): Adding <" + aExtFactory + "> with Namespace <"
              + theService.getNamespaceURI() + ">");

        extFactories.put(theService.getNamespaceURI(), theFactory);

        this.extFactoriesSet.add(theFactory);
      }
    }
    catch (Exception e) {
      throw new EPPCodecException(aExtFactory + " " + e.getClass().getName() + ": " + e);
    }

    cat.debug("addExtFactory(String): exit");
  }

  /**
   * Gets the list of XML schemas that need to be pre-loaded into the XML
   * Parser. Each {@link EPPMapFactory} and {@link EPPExtFactory} include a
   * list of XML schemas that need to be loaded into the XML Parser, so
   * {@code getXmlSchemas()} will aggregate them into a single {@code List}.
   * Each XML schemas should not include the path, since the schema will be
   * loaded as a resource from schemas folder of the classpath.
   * 
   * @return {@code Set} of {@code String} XML Schema names that should be
   *         pre-loaded in the XML Parser. For example, at a minimum, the
   *         following two XML schemas will be returned:<br>
   *         <br>
   *         <ul>
   *         <li>{@code eppcom-1.0.xsd}
   *         <li>{@code epp-1.0.xsd}
   *         </ul>
   */
  public Set getXmlSchemas() {
    cat.debug("getXmlSchemas(): enter");

    Set<String> theSchemas = new LinkedHashSet<String>();

    // Pre-load the two required EPP XML schemas
    cat.debug("getXmlSchemas(): Loading required EPP XML schemas");
    theSchemas.add("eppcom-1.0.xsd");
    theSchemas.add("epp-1.0.xsd");

    // Load the EPP XML schemas from the EPPMapFactory instances
    Iterator<EPPMapFactory> theFactoryIter = this.factoriesSet.iterator();

    while (theFactoryIter.hasNext()) {
      EPPMapFactory currFactory = theFactoryIter.next();

      cat.debug("getXmlSchemas(): Loading EPP XML schemas from " + currFactory.getClass().getName());

      if (currFactory.getXmlSchemas() != null) {
        theSchemas.addAll(currFactory.getXmlSchemas());
      }
    }

    // Load the EPP XML schemas from the EPPExtFactory instances
    Iterator<EPPExtFactory> theExtFactoryIter = this.extFactoriesSet.iterator();

    while (theExtFactoryIter.hasNext()) {
      EPPExtFactory currFactory = theExtFactoryIter.next();

      cat.debug("getXmlSchemas(): Loading EPP XML schemas from " + currFactory.getClass().getName());

      if (currFactory.getXmlSchemas() != null) {
        theSchemas.addAll(currFactory.getXmlSchemas());
      }
    }

    if (cat.isDebugEnabled()) {
      cat.debug("getXmlSchemas(): XML Schemas = " + theSchemas);
    }

    cat.debug("getXmlSchemas(): exit");
    return theSchemas;
  }

  /**
   * Does the concrete {@code EPPMapFactory} exist in {@code EPPFactory}?
   *
   * @param aMapFactory
   *           a concrete {@code EPPMapFactory} fully qualified class names.
   *
   * @return {@code true} if {@code aMapFactory} exists; {@code false}
   *         otherwise.
   */
  boolean mapFactoryExists(String aMapFactory) {
    Enumeration<EPPMapFactory> enumeration = factories.elements();

    while (enumeration.hasMoreElements()) {
      if (enumeration.nextElement().getClass().getName().equals(aMapFactory)) {
        return true;
      }
    }

    return false;
  }

  /**
   * Does the concrete {@code EPPExtFactory} exist in {@code EPPFactory}?
   *
   * @param aExtFactory
   *           a concrete {@code EPPExtFactory} fully qualified class names.
   *
   * @return {@code true} if {@code aExtFactory} exists; {@code false}
   *         otherwise.
   */
  boolean extFactoryExists(String aExtFactory) {
    Enumeration<EPPExtFactory> enumeration = extFactories.elements();

    while (enumeration.hasMoreElements()) {
      if (enumeration.nextElement().getClass().getName().equals(aExtFactory)) {
        return true;
      }
    }

    return false;
  }

}
