/***********************************************************
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.domain;

import java.util.Vector;

import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import com.verisign.epp.codec.gen.EPPAuthInfo;
import com.verisign.epp.codec.gen.EPPCodecComponent;
import com.verisign.epp.codec.gen.EPPDecodeException;
import com.verisign.epp.codec.gen.EPPEncodeException;
import com.verisign.epp.codec.gen.EPPFactory;
import com.verisign.epp.codec.gen.EPPUtil;
import com.verisign.epp.util.EPPCatFactory;

/**
 * Represents attributes to add, remove or change with a
 * {@code EPPDomainUpdateCmd}. In {@code EPPDomainUpdateCmd}, an instance of
 * {@code EPPDomainAddRemove} is used to specify the attributes to add; an
 * instance of {@code EPPDomainAddRemove} is used to specify the attributes to
 * remove, and an instance of {@code EPPDomainAddRemove} is used to specify the
 * attributes to change<br>
 * <br>
 * The Domain Mapping Specification describes the following attributes:<br>
 *
 * <ul>
 * <li>Zero or more &lt;domain:ns&gt; elements that contain the fully qualified
 * host name of a known host object. Use {@code getServers} and
 * {@code setServers} to get and set the element.</li>
 * <li>Zero or more &lt;domain:contact&gt; elements that contain the registrant,
 * administrative, technical, and billing contact identifiers to be associated
 * with the domain. Use {@code getContacts} and {@code setContacts} to get and
 * set the element. This attribute will only be allowed if the Contact Mapping
 * is supported.</li>
 * <li>One or two &lt;domain:status&gt; elements that contain status values to
 * be applied to or removed from the domain object. Use {@code getStatuses} and
 * {@code setStatuses} to get and set the element.</li>
 * <li>For {@code change} only, A &lt;domain:registrant&gt; element that
 * contains the identifier for the human or organizational social information
 * (contact) object to be associated with the domain object as the object
 * registrant. This object identifier MUST be known to the server before the
 * contact object can be associated with the domain object. Use
 * {@code getRegistrant} and {@code setRegistrant} to get and set the
 * element.</li>
 * </ul>
 *
 * <br>
 * It is important to note that the maximum number of domain attribute elements
 * is subject to the number of values currently associated with the domain
 * object. {@code EPPDomainAddRemove} will delegate the validation of the
 * cardinality of the domain attributes elements to the EPP Server.
 *
 * @see com.verisign.epp.codec.domain.EPPDomainUpdateCmd
 */
public class EPPDomainAddRemove implements EPPCodecComponent {

	/** 
	 * mode of {@code EPPDomainAddRemove} is not specified. 
	 */
	private final static short MODE_NONE = 0;

	/** 
	 * mode of {@code EPPDomainAddRemove} is to add attributes. 
	 */
	public final static short MODE_ADD = 1;

	/** 
	 * mode of {@code EPPDomainAddRemove} is to remove attributes. 
	 */
	public final static short MODE_REMOVE = 2;

	/**
	 * XML tag name when the {@code mode} attribute is
	 * {@code EPPDomainAddRemove.MODE_ADD}. This is a package constant, so the
	 * container {@code EPPCodecComponent} can use it on a decode operation.
	 */
	final static String ELM_ADD = "add";

	/**
	 * XML tag name when the {@code mode} attribute is
	 * {@code EPPDomainAddRemove.MODE_REMOVE}. This is a package constant, so the
	 * container {@code EPPCodecComponent} can use it on a decode operation.
	 */
	final static String ELM_REMOVE = "rem";

	/**
	 * XML tag name when the {@code mode} attribute is
	 * {@code EPPDomainAddRemove.MODE_CHANGE}. This is a package constant, so the
	 * container {@code EPPCodecComponent} can use it on a decode operation.
	 */
	final static String ELM_CHANGE = "chg";

	/** mode of {@code EPPDomainAddRemove} is to change attributes. */
	final static short MODE_CHANGE = 3;

	/** XML tag name for the {@code servers} attribute. */
	private final static String ELM_SERVER = "ns";

	/** XML tag name for host object reference */
	private final static String ELM_HOST_OBJ = "hostObj";

	/** XML tag name for host attribute */
	private final static String ELM_HOST_ATTR = EPPHostAttr.ELM_NAME;

	/** XML tag name for the {@code contacts} attribute. */
	private final static String ELM_CONTACT = "contact";

	/** XML tag name for the {@code statuses} attribute. */
	private final static String ELM_STATUS = "status";

	/** XML tag name for the {@code servers} attribute. */
	private final static String ELM_REGISTRANT = "registrant";

	/** Log4j category for logging */
	private static Logger cat = Logger.getLogger(EPPDomainAddRemove.class.getName(),
	      EPPCatFactory.getInstance().getFactory());

	/**
	 * Mode of EPPDomainAddRemove. Must be {@code MODE_ADD} or
	 * {@code MODE_REMOVE} to be valid. This attribute will be set by the parent
	 * container {@code EPPCodecComponent}. For example,
	 * {@code EPPDomainUpdateCmd} will set the mode for its
	 * {@code EPPDomainAddRemove} instances.
	 */
	private short mode = MODE_NONE;

	/** Name Servers to add or remove. */
	private Vector servers = null;

	/** Contacts to add or remove. */
	private Vector contacts = null;

	/** Status to add or remove. */
	private Vector statuses = null;

	/** authorization information to change. */
	private EPPAuthInfo authInfo = null;

	/** registrant to change. */
	private java.lang.String registrant = null;

	/**
	 * Default constructor for {@code EPPDomainAddRemove}. All of the attribute
	 * default to {@code null} to indicate no modification.
	 */
	public EPPDomainAddRemove() {
		this.servers = null;
		this.contacts = null;
		this.statuses = null;
		this.registrant = null;
		this.authInfo = null;
	}

	/**
	 * Constructor for {@code EPPDomainAddRemove} that includes the attributes as
	 * arguments.
	 *
	 * @param aServers
	 *           Vector of Name Server {@code String}'s. Is {@code null} or empty
	 *           for no modifications.
	 * @param aContacts
	 *           Vector of {@code EPPDomainContact} instances. Is {@code null} or
	 *           empty for no modifications. If the Contact Mapping is not
	 *           supported, this value should be {@code null}.
	 * @param aStatuses
	 *           Vector of status {@code String}'s. One of the
	 *           {@code EPPDomainInfoResp.STATUS_} contants can be used for each
	 *           of the status values. Is {@code null} or empty for no
	 *           modifications.
	 */
	public EPPDomainAddRemove(Vector aServers, Vector aContacts, Vector aStatuses) {
		this.servers = aServers;
		this.contacts = aContacts;
		this.statuses = aStatuses;
	}

	/**
	 * Constructor for {@code EPPDomainAddRemove} that includes the attributes as
	 * arguments.
	 *
	 * @param aRegistrant
	 *           {@code String} registrant for the change mode
	 * @param aAuthInfo
	 *           {@code EPPAuthInfo} authorization information for the change
	 *           mode
	 */
	public EPPDomainAddRemove(String aRegistrant, EPPAuthInfo aAuthInfo) {
		this.registrant = aRegistrant;
		setAuthInfo(aAuthInfo);
	}

	/**
	 * Gets the name servers. The name servers can either be {@code String}
	 * instances containing the fully qualified name of a known name server host
	 * object, or {@code EPPHostAttr} instances containing the fully qualified
	 * name of a host and optionally the host IP addresses.
	 *
	 * @return {@code Vector} of name server {@code String} instances for host
	 *         object references or {@code EPPHostAttr} instances for host
	 *         attribute values if exists; {@code null} otherwise.
	 */
	public Vector getServers() {
		return this.servers;
	}

	/**
	 * Sets the name servers. The name servers can either be {@code String}
	 * instances containing the fully qualified name of a known name server host
	 * object, or {@code EPPHostAttr} instances containing the fully qualified
	 * name of a host and optionally the host IP addresses.
	 *
	 * @param aServers
	 *           {@code Vector} of name server {@code String} instances for host
	 *           object references or {@code EPPHostAttr} instances for host
	 *           attribute values.
	 */
	public void setServers(Vector aServers) {
		this.servers = aServers;
	}

	/**
	 * Gets the contacts to add or remove.
	 *
	 * @return Vector of {@code EPPDomainContact} instances.
	 */
	public Vector getContacts() {
		return this.contacts;
	}

	/**
	 * Sets the contacts to add or remove.
	 *
	 * @param aContacts
	 *           The contacts to add or remove.
	 */
	public void setContacts(Vector aContacts) {
		this.contacts = aContacts;
	}

	/**
	 * Gets the statuses to add or remove. The {@code EPPDomainInfoResp.STATUS_}
	 * constants can be used for the statuses.
	 *
	 * @return Vector of status {@code String} instances.
	 */
	public Vector getStatuses() {
		return this.statuses;
	}

	/**
	 * Sets the statuses to add or remove. The {@code EPPDomainInfoResp.STATUS_}
	 * constants can be used for the statuses.
	 *
	 * @param aStatuses
	 *           Vector of status {@code String} instances.
	 */
	public void setStatuses(Vector aStatuses) {
		this.statuses = aStatuses;
	}

	/**
	 * Return if Domain Contacts is supported.
	 *
	 * @return {@code true} if contacts are supported; {@code false} otherwise.
	 */
	public boolean contactsSupported() {
		return EPPFactory.getInstance().hasService(EPPDomainMapFactory.NS_CONTACT);
	}

	/**
	 * Encode a DOM Element tree from the attributes of the
	 * {@code EPPDomainAddRemove} instance.
	 *
	 * @param aDocument
	 *           DOM Document that is being built. Used as an Element factory.
	 *
	 * @return Root DOM Element representing the {@code EPPDomainAddRemove}
	 *         instance.
	 *
	 * @exception EPPEncodeException
	 *               Unable to encode {@code EPPDomainAddRemove} instance.
	 */
	@Override
	public Element encode(Document aDocument) throws EPPEncodeException {
		Element root;

		// Change mode
		if (this.mode == MODE_CHANGE) {
			root = aDocument.createElementNS(EPPDomainMapFactory.NS, EPPDomainMapFactory.NS_PREFIX + ":" + ELM_CHANGE);

			if (this.registrant != null) {
				EPPUtil.encodeString(aDocument, root, this.registrant, EPPDomainMapFactory.NS, EPPDomainMapFactory.NS_PREFIX + ":" + ELM_REGISTRANT);
			}

			if (this.authInfo != null) {
				EPPUtil.encodeComp(aDocument, root, this.authInfo);
			}

			return root;
		}

		// Add or Remove mode
		if (this.mode == MODE_ADD) {
			root = aDocument.createElementNS(EPPDomainMapFactory.NS, EPPDomainMapFactory.NS_PREFIX + ":" + ELM_ADD);
		}
		else if (this.mode == MODE_REMOVE) {
			root = aDocument.createElementNS(EPPDomainMapFactory.NS, EPPDomainMapFactory.NS_PREFIX + ":" + ELM_REMOVE);
		}
		else {
			throw new EPPEncodeException("Invalid EPPDomainAddRemove mode of " + this.mode);
		}

		// Domain Name Servers
		if ((this.servers != null) && (this.servers.size() > 0)) {
			Element theServersElm = aDocument.createElementNS(EPPDomainMapFactory.NS, EPPDomainMapFactory.NS_PREFIX + ":" + ELM_SERVER);
			root.appendChild(theServersElm);

			Object theNS = this.servers.get(0);

			// Name Server Host objects?
			if (theNS instanceof String) {
				EPPUtil.encodeVector(aDocument, theServersElm, this.servers, EPPDomainMapFactory.NS, EPPDomainMapFactory.NS_PREFIX + ":" + ELM_HOST_OBJ);
			}

			// Name Server Host attributes?
			else if (theNS instanceof EPPHostAttr) {
				EPPUtil.encodeCompVector(aDocument, theServersElm, this.servers);
			}
			else {
				throw new EPPEncodeException(
				      "EPPDomainAddRemove.encode: Invalid NS server class " + theNS.getClass().getName());
			}
		}

		// end if (this.servers != null) && (this.servers.size()) > 0)
		// Contacts
		if (this.contacts != null) {
			if (contactsSupported()) {
				EPPUtil.encodeCompVector(aDocument, root, this.contacts);
			}
			else {
				throw new EPPEncodeException("Contacts specified when the Contact Mapping is not supported");
			}
		}

		// Statuses
		EPPUtil.encodeCompVector(aDocument, root, this.statuses);

		return root;
	}

	/**
	 * Decode the {@code EPPDomainAddRemove} attributes from the aElement DOM
	 * Element tree.
	 *
	 * @param aElement
	 *           Root DOM Element to decode {@code EPPDomainAddRemove} from.
	 *
	 * @exception EPPDecodeException
	 *               Unable to decode aElement.
	 */
	@Override
	public void decode(Element aElement) throws EPPDecodeException {
		if (aElement.getLocalName().equals(EPPUtil.getLocalName(ELM_ADD))) {
			this.mode = MODE_ADD;
		}
		else if (aElement.getLocalName().equals(EPPUtil.getLocalName(ELM_REMOVE))) {
			this.mode = MODE_REMOVE;
		}
		else if (aElement.getLocalName().equals(EPPUtil.getLocalName(ELM_CHANGE))) {
			this.mode = MODE_CHANGE;
		}
		else {
			throw new EPPDecodeException("Invalid EPPDomainAddRemove mode of " + aElement.getLocalName());
		}

		// Change Mode
		if (this.mode == MODE_CHANGE) {
			// Registrant
			this.registrant = EPPUtil.decodeString(aElement, EPPDomainMapFactory.NS, ELM_REGISTRANT);

			// AuthInfo
			this.authInfo = (EPPAuthInfo) EPPUtil.decodeComp(aElement, EPPDomainMapFactory.NS,
			      EPPDomainMapFactory.ELM_DOMAIN_AUTHINFO, EPPAuthInfo.class);
		}
		else { // Add & Remove Mode

			// Servers
			Element theServersElm = EPPUtil.getElementByTagNameNS(aElement, EPPDomainMapFactory.NS, ELM_SERVER);

			if (theServersElm != null) {
				Element theServerElm = EPPUtil.getFirstElementChild(theServersElm);

				if (theServerElm != null) {
					if (theServerElm.getLocalName().equals(EPPUtil.getLocalName(ELM_HOST_OBJ))) {
						this.servers = EPPUtil.decodeVector(theServersElm, EPPDomainMapFactory.NS, ELM_HOST_OBJ);
					}
					else if (theServerElm.getLocalName().equals(EPPUtil.getLocalName(ELM_HOST_ATTR))) {
						this.servers = EPPUtil.decodeCompVector(theServersElm, EPPDomainMapFactory.NS, ELM_HOST_ATTR,
						      EPPHostAttr.class);
					}
					else {
						throw new EPPDecodeException(
						      "EPPDomainAddRemove.doDecode: Invalid host child element " + theServersElm.getLocalName());
					}

					if (this.servers.size() == 0) {
						this.servers = null;
					}
				}

				// end if (theServerElm != null)
			}

			// end if (theServersElm != null)
			// Contacts
			this.contacts = EPPUtil.decodeCompVector(aElement, EPPDomainMapFactory.NS, ELM_CONTACT,
			      EPPDomainContact.class);

			if (this.contacts.size() == 0) {
				this.contacts = null;
			}

			// Statuses
			this.statuses = EPPUtil.decodeCompVector(aElement, EPPDomainMapFactory.NS, ELM_STATUS, EPPDomainStatus.class);
			if (this.statuses.size() == 0) {
				this.statuses = null;
			}
		}
	}

	/**
	 * implements a deep {@code EPPDomainAddRemove} compare.
	 *
	 * @param aObject
	 *           {@code EPPDomainAddRemove} instance to compare with
	 *
	 * @return {@code true} when equal; {@code false} otherwise.
	 */
	@Override
	public boolean equals(Object aObject) {
		if (!(aObject instanceof EPPDomainAddRemove)) {
			cat.error("EPPDomainAddRemove.equals(): " + aObject.getClass().getName() + " not EPPDomainAddRemove instance");

			return false;
		}

		EPPDomainAddRemove theComp = (EPPDomainAddRemove) aObject;

		// Mode
		if (this.mode != theComp.mode) {
			cat.error("EPPDomainAddRemove.equals(): mode not equal");

			return false;
		}

		// Servers
		if (!EPPUtil.equalVectors(this.servers, theComp.servers)) {
			cat.error("EPPDomainAddRemove.equals(): servers not equal");

			return false;
		}

		// Contacts
		if (contactsSupported()) {
			if (!EPPUtil.equalVectors(this.contacts, theComp.contacts)) {
				cat.error("EPPDomainAddRemove.equals(): contacts not equal");

				return false;
			}
		}

		// Statuses
		if (!EPPUtil.equalVectors(this.statuses, theComp.statuses)) {
			cat.error("EPPDomainAddRemove.equals(): statuses not equal");

			return false;
		}

		// Registrant
		if (!((this.registrant == null) ? (theComp.registrant == null) : this.registrant.equals(theComp.registrant))) {
			cat.error("EPPDomainAddRemove.equals(): registrant not equal");

			return false;
		}

		// AuthInfo
		if (!((this.authInfo == null) ? (theComp.authInfo == null) : this.authInfo.equals(theComp.authInfo))) {
			cat.error("EPPDomainAddRemove.equals(): authInfo not equal");

			return false;
		}

		return true;
	}

	/**
	 * Clone {@code EPPDomainAddRemove}.
	 *
	 * @return clone of {@code EPPDomainAddRemove}
	 *
	 * @exception CloneNotSupportedException
	 *               standard Object.clone exception
	 */
	@Override
	public Object clone() throws CloneNotSupportedException {
		EPPDomainAddRemove clone = null;

		clone = (EPPDomainAddRemove) super.clone();

		if (this.servers != null) {
			clone.servers = (Vector) this.servers.clone();
		}

		if (this.contacts != null) {
			clone.contacts = (Vector) this.contacts.clone();

			for (int i = 0; i < this.contacts.size(); i++) {
				clone.contacts.setElementAt(((EPPDomainContact) this.contacts.elementAt(i)).clone(), i);
			}
		}

		if (this.statuses != null) {
			clone.statuses = (Vector) this.statuses.clone();
		}

		if (this.registrant != null) {
			clone.registrant = this.registrant;
		}

		if (this.authInfo != null) {
			clone.authInfo = (EPPAuthInfo) this.authInfo.clone();
		}

		return clone;
	}

	/**
	 * Implementation of {@code Object.toString}, which will result in an
	 * indented XML {@code String} representation of the concrete
	 * {@code EPPCodecComponent}.
	 *
	 * @return Indented XML {@code String} if successful; {@code ERROR}
	 *         otherwise.
	 */
	@Override
	public String toString() {
		return EPPUtil.toString(this);
	}

	/**
	 * Get authorization information for the change mode
	 *
	 * @return com.verisign.epp.codec.domain.EPPDomainAuthInfo
	 */
	public EPPAuthInfo getAuthInfo() {
		return this.authInfo;
	}

	/**
	 * Get registrant for the change mode
	 *
	 * @return java.lang.String
	 */
	public String getRegistrant() {
		return this.registrant;
	}

	/**
	 * Set authorization information for the change mode
	 *
	 * @param aAuthInfo
	 *           com.verisign.epp.codec.domain.EPPDomainAuthInfo
	 */
	public void setAuthInfo(EPPAuthInfo aAuthInfo) {
		if (aAuthInfo != null) {
			this.authInfo = aAuthInfo;
			this.authInfo.setRootName(EPPDomainMapFactory.NS, EPPDomainMapFactory.ELM_DOMAIN_AUTHINFO);
		}
	}

	/**
	 * Set registrant for the change mode
	 *
	 * @param newRegistrant
	 *           java.lang.String
	 */
	public void setRegistrant(String newRegistrant) {
		this.registrant = newRegistrant;
	}

	/**
	 * Is the {@code EPPDomainAddRemove} empty?
	 *
	 * @return {@code true} if all of the attributes are null; {@code false}
	 *         otherwise.
	 */
	public boolean isEmpty() {
		return (this.servers == null) && (this.contacts == null) && (this.statuses == null) && (this.registrant == null)
		      && (this.authInfo == null);
	}

	/**
	 * Gets the mode of {@code EPPDomainAddRemove}. There are two valid modes
	 * {@code EPPDomainAddRemove.MODE_ADD} and
	 * {@code EPPDomainAddRemove.MODE_REMOVE}. If no mode has been satisfied,
	 * than the mode is set to {@code EPPDomainAddRemove.MODE_NONE}.
	 *
	 * @return One of the {@code EPPDomainAddRemove_MODE} constants.
	 */
	short getMode() {
		return this.mode;
	}

	/**
	 * Sets the mode of {@code EPPDomainAddRemove}. There are two valid modes
	 * {@code EPPDomainAddRemove.MODE_ADD} and
	 * {@code EPPDomainAddRemove.MODE_REMOVE}. If no mode has been satisfied,
	 * than the mode is set to {@code EPPDomainAddRemove.MODE_NONE}
	 *
	 * @param aMode
	 *           {@code EPPDomainAddRemove.MODE_ADD} or
	 *           {@code EPPDomainAddRemove.MODE_REMOVE}.
	 */
	void setMode(short aMode) {
		this.mode = aMode;
	}

	/**
	 * Returns the XML namespace associated with the {@code EPPCodecComponent}.
	 *
	 * @return XML namespace for the {@code EPPCodecComponent}.
	 */
	@Override
	public String getNamespace() {
		return EPPDomainMapFactory.NS;
	}
}
