/*
 * Copyright 2005-2010 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.ldap.core;

import java.util.ArrayList;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.NameNotFoundException;
import javax.naming.NameParser;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.apache.commons.lang.builder.HashCodeBuilder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.ldap.NoSuchAttributeException;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.util.StringUtils;

/**
 * Adapter that implements the interesting methods of the DirContext interface.
 * In particular it contains utility methods for getting and setting attributes.
 * Using the
 * {@link org.springframework.ldap.core.support.DefaultDirObjectFactory} in your
 * <code>ContextSource</code> (which is the default) you will receive instances
 * of this class from searches and lookups. This can be particularly useful when
 * updating data, since this class implements
 * {@link AttributeModificationsAware}, providing a
 * {@link #getModificationItems()} method. When in update mode, an object of
 * this class keeps track of the changes made to its attributes, making them
 * available as an array of <code>ModificationItem</code> objects, suitable as
 * input to {@link LdapTemplate#modifyAttributes(DirContextOperations)}.
 * 
 * Note that this is not a complete implementation of DirContext. Several
 * methods are not relevant for the intended usage of this class, so they
 * throw UnsupportOperationException.
 *
 * @see #setAttributeValue(String, Object)
 * @see #setAttributeValues(String, Object[])
 * @see #getStringAttribute(String)
 * @see #getStringAttributes(String)
 * @see #getObjectAttribute(String)
 * @see #addAttributeValue(String, Object)
 * @see #removeAttributeValue(String, Object)
 * @see #setUpdateMode(boolean)
 * @see #isUpdateMode()
 * 
 * @author Magnus Robertsson
 * @author Andreas Ronge
 * @author Adam Skogman
 * @author Mattias Hellborg Arthursson
 */
public class DirContextAdapter implements DirContextOperations {

	private static final boolean DONT_ADD_IF_DUPLICATE_EXISTS = false;

	private static final String EMPTY_STRING = "";

	private static final boolean ORDER_DOESNT_MATTER = false;

	private static Log log = LogFactory.getLog(DirContextAdapter.class);

	private final Attributes originalAttrs;

	private DistinguishedName dn;

	private DistinguishedName base;

	private boolean updateMode = false;

	private Attributes updatedAttrs;

	private String referralUrl;

	/**
	 * Default constructor.
	 */
	public DirContextAdapter() {
		this(null, null, null);
	}

	/**
	 * Create a new DirContextAdapter from the supplied DN String.
	 * @param dnString the DN string. Must be syntactically correct, or an
	 * exception will be thrown.
	 */
	public DirContextAdapter(String dnString) {
		this(new DistinguishedName(dnString));
	}

	/**
	 * Create a new adapter from the supplied dn.
	 * 
	 * @param dn the dn.
	 */
	public DirContextAdapter(Name dn) {
		this(null, dn);
	}

	/**
	 * Create a new adapter from the supplied attributes and dn.
	 * 
	 * @param attrs the attributes.
	 * @param dn the dn.
	 */
	public DirContextAdapter(Attributes attrs, Name dn) {
		this(attrs, dn, null);
	}

	/**
	 * Create a new adapter from the supplied attributes, dn, and base.
	 * 
	 * @param attrs the attributes.
	 * @param dn the dn.
	 * @param base the base name.
	 */
	public DirContextAdapter(Attributes attrs, Name dn, Name base) {
		this(attrs, dn, base, null);
	}

	/**
	 * Create a new adapter from the supplied attributes, dn, base, and referral
	 * url.
	 * @param attrs the attributes.
	 * @param dn the dn.
	 * @param base the base.
	 * @param referralUrl the referral url (if this instance results from a
	 * referral).
	 */
	public DirContextAdapter(Attributes attrs, Name dn, Name base,
			String referralUrl) {
		if (attrs != null) {
			this.originalAttrs = attrs;
		}
		else {
			this.originalAttrs = new BasicAttributes(true);
		}
		if (dn != null) {
			this.dn = new DistinguishedName(dn);
		}
		else {
			this.dn = new DistinguishedName();
		}
		if (base != null) {
			this.base = new DistinguishedName(base);
		}
		else {
			this.base = new DistinguishedName();
		}
		if (referralUrl != null) {
			this.referralUrl = referralUrl;
		}
		else {
			this.referralUrl = EMPTY_STRING;
		}
	}

	/**
	 * Constructor for cloning an existing adapter.
	 * 
	 * @param master The adapter to be copied.
	 */
	protected DirContextAdapter(DirContextAdapter master) {
		this.originalAttrs = (Attributes) master.originalAttrs.clone();
		this.dn = master.dn;
		this.updatedAttrs = (Attributes) master.updatedAttrs.clone();
		this.updateMode = master.updateMode;
	}

	/**
	 * Sets the update mode. The update mode should be <code>false</code> for a
	 * new entry and <code>true</code> for an existing entry that is being
	 * updated.
	 * 
	 * @param mode Update mode.
	 */
	public void setUpdateMode(boolean mode) {
		this.updateMode = mode;
		if (updateMode) {
			updatedAttrs = new BasicAttributes(true);
		}
	}

	/*
	 * @see org.springframework.ldap.support.DirContextOperations#isUpdateMode()
	 */
	public boolean isUpdateMode() {
		return updateMode;
	}

	/*
	 * @seeorg.springframework.ldap.support.DirContextOperations#
	 * getNamesOfModifiedAttributes()
	 */
	public String[] getNamesOfModifiedAttributes() {

		List tmpList = new ArrayList();

		NamingEnumeration attributesEnumeration;
		if (isUpdateMode()) {
			attributesEnumeration = updatedAttrs.getAll();
		}
		else {
			attributesEnumeration = originalAttrs.getAll();
		}

		try {
			while (attributesEnumeration.hasMore()) {
				Attribute oneAttribute = (Attribute) attributesEnumeration
						.next();
				tmpList.add(oneAttribute.getID());
			}
		}
		catch (NamingException e) {
			throw LdapUtils.convertLdapException(e);
		}
		finally {
			closeNamingEnumeration(attributesEnumeration);
		}

		return (String[]) tmpList.toArray(new String[0]);
	}

	private void closeNamingEnumeration(NamingEnumeration enumeration) {
		try {
			if (enumeration != null) {
				enumeration.close();
			}
		}
		catch (NamingException e) {
			// Never mind this
		}
	}

	/*
	 * @seeorg.springframework.ldap.support.AttributeModificationsAware#
	 * getModificationItems()
	 */
	public ModificationItem[] getModificationItems() {
		if (!updateMode) {
			return new ModificationItem[0];
		}

		List tmpList = new LinkedList();
		NamingEnumeration attributesEnumeration = null;
		try {
			attributesEnumeration = updatedAttrs.getAll();

			// find attributes that have been changed, removed or added
			while (attributesEnumeration.hasMore()) {
				Attribute oneAttr = (Attribute) attributesEnumeration.next();

				collectModifications(oneAttr, tmpList);
			}
		}
		catch (NamingException e) {
			throw LdapUtils.convertLdapException(e);
		}
		finally {
			closeNamingEnumeration(attributesEnumeration);
		}

		if (log.isDebugEnabled()) {
			log.debug("Number of modifications:" + tmpList.size());
		}

		return (ModificationItem[]) tmpList
				.toArray(new ModificationItem[tmpList.size()]);
	}

	/**
	 * Collect all modifications for the changed attribute. If no changes have
	 * been made, return immediately. If modifications have been made, and the
	 * original size as well as the updated size of the attribute is 1, replace
	 * the attribute. If the size of the updated attribute is 0, remove the
	 * attribute. Otherwise, the attribute is a multi-value attribute; if it's
	 * an ordered one it should be replaced in its entirety to preserve the new
	 * ordering, if not all modifications to the original value (removals and
	 * additions) will be collected individually.
	 * 
	 * @param changedAttr the value of the changed attribute.
	 * @param modificationList the list in which to add the modifications.
	 * @throws NamingException if thrown by called Attribute methods.
	 */
	private void collectModifications(Attribute changedAttr,
			List modificationList) throws NamingException {
		Attribute currentAttribute = originalAttrs.get(changedAttr.getID());

		if (changedAttr.equals(currentAttribute)) {
			// No changes
			return;
		}
		else if (currentAttribute != null && currentAttribute.size() == 1
				&& changedAttr.size() == 1) {
			// Replace single-vale attribute.
			modificationList.add(new ModificationItem(
					DirContext.REPLACE_ATTRIBUTE, changedAttr));
		}
		else if (changedAttr.size() == 0 && currentAttribute != null) {
			// Attribute has been removed.
			modificationList.add(new ModificationItem(
					DirContext.REMOVE_ATTRIBUTE, changedAttr));
		}
		else if ((currentAttribute == null || currentAttribute.size() == 0)
				&& changedAttr.size() > 0) {
			// Attribute has been added.
			modificationList.add(new ModificationItem(DirContext.ADD_ATTRIBUTE,
					changedAttr));
		}
		else if (changedAttr.size() > 0 && changedAttr.isOrdered()) {
			// This is a multivalue attribute and it is ordered - the original
			// value should be replaced with the new values so that the ordering
			// is preserved.
			modificationList.add(new ModificationItem(
					DirContext.REPLACE_ATTRIBUTE, changedAttr));
		}
		else if (changedAttr.size() > 0) {
			// Change of multivalue Attribute. Collect additions and removals
			// individually.
			List myModifications = new LinkedList();
			collectModifications(currentAttribute, changedAttr, myModifications);

			if (myModifications.isEmpty()) {
				// This means that the attributes are not equal, but the
				// actual values are the same - thus the order must have
				// changed. This should result in a REPLACE_ATTRIBUTE operation.
				myModifications.add(new ModificationItem(
						DirContext.REPLACE_ATTRIBUTE, changedAttr));
			}

			modificationList.addAll(myModifications);
		}
	}

	private void collectModifications(Attribute originalAttr,
			Attribute changedAttr, List modificationList)
			throws NamingException {

		Attribute originalClone = (Attribute) originalAttr.clone();
		Attribute addedValuesAttribute = new BasicAttribute(originalAttr
				.getID());

		for (int i = 0; i < changedAttr.size(); i++) {
			Object attributeValue = changedAttr.get(i);
			if (!originalClone.remove(attributeValue)) {
				addedValuesAttribute.add(attributeValue);
			}
		}

		// We have now traversed and removed all values from the original that
		// were also present in the new values. The remaining values in the
		// original must be the ones that were removed.
		if (originalClone.size() > 0) {
			modificationList.add(new ModificationItem(
					DirContext.REMOVE_ATTRIBUTE, originalClone));
		}

		if (addedValuesAttribute.size() > 0) {
			modificationList.add(new ModificationItem(DirContext.ADD_ATTRIBUTE,
					addedValuesAttribute));
		}
	}

	/**
	 * returns true if the attribute is empty. It is empty if a == null, size ==
	 * 0 or get() == null or an exception if thrown when accessing the get
	 * method
	 */
	private boolean isEmptyAttribute(Attribute a) {
		try {
			return (a == null || a.size() == 0 || a.get() == null);
		}
		catch (NamingException e) {
			return true;
		}
	}

	/**
	 * Compare the existing attribute <code>name</code> with the values on the
	 * array <code>values</code>. The order of the array must be the same order
	 * as the existing multivalued attribute.
	 * <p>
	 * Also handles the case where the values have been reset to the original
	 * values after a previous change. For example, changing
	 * <code>[a,b,c]</code> to <code>[a,b]</code> and then back to
	 * <code>[a,b,c]</code> again must result in this method returning
	 * <code>true</code> so the first change can be overwritten with the latest
	 * change.
	 * 
	 * @param name Name of the original multi-valued attribute.
	 * @param values Array of values to check if they have been changed.
	 * @return true if there has been a change compared to original attribute,
	 * or a previous update
	 */
	private boolean isChanged(String name, Object[] values, boolean orderMatters) {

		Attribute orig = originalAttrs.get(name);
		Attribute prev = updatedAttrs.get(name);

		// values == null and values.length == 0 is treated the same way
		boolean emptyNewValue = (values == null || values.length == 0);

		// Setting to empty ---------------------
		if (emptyNewValue) {
			// FALSE: if both are null, it is not changed (both don't exist)
			// TRUE: if new value is null and old value exists (should be
			// removed)
			// TODO Also include prev in null check
			// TODO Also check if there is a single null element
			if (orig != null) {
				return true;
			}
			return false;
		}

		// NOT setting to empty -------------------

		// TRUE if existing value is null
		if (orig == null) {
			return true;
		}

		// TRUE if different length compared to original attributes
		if (orig.size() != values.length) {
			return true;
		}

		// TRUE if different length compared to previously updated attributes
		if (prev != null && prev.size() != values.length) {
			return true;
		}

		// Check contents of arrays

		// Order DOES matter, e.g. first names
		try {
			for (int i = 0; i < orig.size(); i++) {
				Object obj = orig.get(i);
				// TRUE if one value is not equal
				if (!(obj instanceof String)) {
					return true;
				}
				if (orderMatters) {
					// check only the string with same index
					if (!values[i].equals(obj)) {
						return true;
					}
				}
				else {
					// check all strings
					if (!ArrayUtils.contains(values, obj)) {
						return true;
					}
				}
			}

		}
		catch (NamingException e) {
			// TRUE if we can't access the value
			return true;
		}

		if (prev != null) {
			// Also check against updatedAttrs, since there might have been
			// a previous update
			try {
				for (int i = 0; i < prev.size(); i++) {
					Object obj = prev.get(i);
					// TRUE if one value is not equal
					if (!(obj instanceof String)) {
						return true;
					}
					if (orderMatters) {
						// check only the string with same index
						if (!values[i].equals(obj)) {
							return true;
						}
					}
					else {
						// check all strings
						if (!ArrayUtils.contains(values, obj)) {
							return true;
						}
					}
				}

			}
			catch (NamingException e) {
				// TRUE if we can't access the value
				return true;
			}
		}
		// FALSE since we have compared all values
		return false;
	}

	/**
	 * Checks if an entry has a specific attribute.
	 * 
	 * This method simply calls exists(String) with the attribute name.
	 * 
	 * @param attr the attribute to check.
	 * @return true if attribute exists in entry.
	 */
	protected final boolean exists(Attribute attr) {
		return exists(attr.getID());
	}

	/**
	 * Checks if the attribute exists in this entry, either it was read or it
	 * has been added and update() has been called.
	 * 
	 * @param attrId id of the attribute to check.
	 * @return true if the attribute exists in the entry.
	 */
	protected final boolean exists(String attrId) {
		return originalAttrs.get(attrId) != null;
	}

	/*
	 * @see
	 * org.springframework.ldap.support.DirContextOperations#getStringAttribute
	 * (java.lang.String)
	 */
	public String getStringAttribute(String name) {
		return (String) getObjectAttribute(name);
	}

	/*
	 * @see org.springframework.ldap.support.DirContextOperations#getObjectAttribute
	 * (java.lang.String)
	 */
	public Object getObjectAttribute(String name) {
		Attribute oneAttr = originalAttrs.get(name);
		if (oneAttr == null || oneAttr.size() == 0) { // LDAP-215
			return null;
		}
		try {
			return oneAttr.get();
		}
		catch (NamingException e) {
			throw LdapUtils.convertLdapException(e);
		}
	}

	// LDAP-215
	/* (non-Javadoc)
	 * @see org.springframework.ldap.core.DirContextOperations#attributeExists(java.lang.String)
	 */
	public boolean attributeExists(String name) {
		Attribute oneAttr = originalAttrs.get(name);
		if (oneAttr == null) {
			return false;
		} else {
			return true;
		}
	}
	
	/*
	 * @see
	 * org.springframework.ldap.support.DirContextOperations#setAttributeValue
	 * (java.lang.String, java.lang.Object)
	 */
	public void setAttributeValue(String name, Object value) {
		// new entry
		if (!updateMode && value != null) {
			originalAttrs.put(name, value);
		}

		// updating entry
		if (updateMode) {
			BasicAttribute attribute = new BasicAttribute(name);
			if (value != null) {
				attribute.add(value);
			}
			updatedAttrs.put(attribute);
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.ldap.core.DirContextOperations#addAttributeValue(
	 * java.lang.String, java.lang.Object)
	 */
	public void addAttributeValue(String name, Object value) {
		addAttributeValue(name, value, DONT_ADD_IF_DUPLICATE_EXISTS);
	}

	public void addAttributeValue(String name, Object value,
			boolean addIfDuplicateExists) {
		if (!updateMode && value != null) {
			Attribute attr = originalAttrs.get(name);
			if (attr == null) {
				originalAttrs.put(name, value);
			}
			else {
				attr.add(value);
			}
		}
		else if (updateMode) {
			Attribute attr = updatedAttrs.get(name);
			if (attr == null) {
				if (originalAttrs.get(name) == null) {
					// No match in the original attributes -
					// add a new Attribute to updatedAttrs
					updatedAttrs.put(name, value);
				}
				else {
					// The attribute exists in the original attributes - clone
					// that and add the new entry to it
					attr = (Attribute) originalAttrs.get(name).clone();
					if (addIfDuplicateExists || !attr.contains(value)) {
						attr.add(value);
					}
					updatedAttrs.put(attr);
				}
			}
			else {
				attr.add(value);
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.ldap.core.DirContextOperations#removeAttributeValue
	 * (java.lang.String, java.lang.Object)
	 */
	public void removeAttributeValue(String name, Object value) {
		if (!updateMode && value != null) {
			Attribute attr = originalAttrs.get(name);
			if (attr != null) {
				attr.remove(value);
				if (attr.size() == 0) {
					originalAttrs.remove(name);
				}
			}
		}
		else if (updateMode) {
			Attribute attr = updatedAttrs.get(name);
			if (attr == null) {
				if (originalAttrs.get(name) != null) {
					attr = (Attribute) originalAttrs.get(name).clone();
					attr.remove(value);
					updatedAttrs.put(attr);
				}
			}
			else {
				attr.remove(value);
			}
		}
	}

	/*
	 * @see
	 * org.springframework.ldap.support.DirContextOperations#setAttributeValues
	 * (java.lang.String, java.lang.Object[])
	 */
	public void setAttributeValues(String name, Object[] values) {
		setAttributeValues(name, values, ORDER_DOESNT_MATTER);
	}

	/*
	 * @see
	 * org.springframework.ldap.support.DirContextOperations#setAttributeValues
	 * (java.lang.String, java.lang.Object[], boolean)
	 */
	public void setAttributeValues(String name, Object[] values,
			boolean orderMatters) {
		Attribute a = new BasicAttribute(name, orderMatters);

		for (int i = 0; values != null && i < values.length; i++) {
			a.add(values[i]);
		}

		// only change the original attribute if not in update mode
		if (!updateMode && values != null && values.length > 0) {
			// don't save empty arrays
			originalAttrs.put(a);
		}

		// possible to set an already existing attribute to an empty array
		if (updateMode && isChanged(name, values, orderMatters)) {
			updatedAttrs.put(a);
		}
	}

	/*
	 * @see org.springframework.ldap.support.DirContextOperations#update()
	 */
	public void update() {
		NamingEnumeration attributesEnumeration = null;

		try {
			attributesEnumeration = updatedAttrs.getAll();

			// find what to update
			while (attributesEnumeration.hasMore()) {
				Attribute a = (Attribute) attributesEnumeration.next();

				// if it does not exist it should be added
				if (isEmptyAttribute(a)) {
					originalAttrs.remove(a.getID());
				}
				else {
					// Otherwise it should be set.
					originalAttrs.put(a);
				}
			}
		}
		catch (NamingException e) {
			throw LdapUtils.convertLdapException(e);
		}
		finally {
			closeNamingEnumeration(attributesEnumeration);
		}

		// Reset the attributes to be updated
		updatedAttrs = new BasicAttributes(true);
	}

	/*
	 * @see
	 * org.springframework.ldap.core.DirContextOperations#getStringAttributes
	 * (java.lang.String)
	 */
	public String[] getStringAttributes(String name) {
		try {
			return (String[]) collectAttributeValuesAsList(name).toArray(
					new String[0]);
		}
		catch (NoSuchAttributeException e) {
			// The attribute does not exist - contract says to return null.
			return null;
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.ldap.core.DirContextOperations#getObjectAttributes
	 * (java.lang.String)
	 */
	public Object[] getObjectAttributes(String name) {
		try {
			return collectAttributeValuesAsList(name).toArray(new Object[0]);
		}
		catch (NoSuchAttributeException e) {
			// The attribute does not exist - contract says to return null.
			return null;
		}
	}

	private List collectAttributeValuesAsList(String name) {
		List list = new LinkedList();
		LdapUtils.collectAttributeValues(originalAttrs, name, list);
		return list;
	}

	/*
	 * @seeorg.springframework.ldap.support.DirContextOperations#
	 * getAttributeSortedStringSet(java.lang.String)
	 */
	public SortedSet getAttributeSortedStringSet(String name) {
		try {
			TreeSet attrSet = new TreeSet();
			LdapUtils.collectAttributeValues(originalAttrs, name, attrSet);
			return attrSet;
		}
		catch (NoSuchAttributeException e) {
			// The attribute does not exist - contract says to return null.
			return null;
		}
	}

	/**
	 * Set the supplied attribute.
	 * 
	 * @param attribute the attribute to set.
	 */
	public void setAttribute(Attribute attribute) {
		if (!updateMode) {
			originalAttrs.put(attribute);
		}
		else {
			updatedAttrs.put(attribute);
		}
	}

	/**
	 * Get all attributes.
	 * 
	 * @return all attributes.
	 */
	public Attributes getAttributes() {
		return originalAttrs;
	}

	/**
	 * @see javax.naming.directory.DirContext#getAttributes(Name)
	 */
	public Attributes getAttributes(Name name) throws NamingException {
		return getAttributes(name.toString());
	}

	/**
	 * @see javax.naming.directory.DirContext#getAttributes(String)
	 */
	public Attributes getAttributes(String name) throws NamingException {
		if (StringUtils.hasLength(name)) {
			throw new NameNotFoundException();
		}
		return (Attributes) originalAttrs.clone();
	}

	/**
	 * @see javax.naming.directory.DirContext#getAttributes(Name, String[])
	 */
	public Attributes getAttributes(Name name, String[] attrIds)
			throws NamingException {
		return getAttributes(name.toString(), attrIds);
	}

	/**
	 * @see javax.naming.directory.DirContext#getAttributes(String, String[])
	 */
	public Attributes getAttributes(String name, String[] attrIds)
			throws NamingException {
		if (StringUtils.hasLength(name)) {
			throw new NameNotFoundException();
		}

		Attributes a = new BasicAttributes(true);
		Attribute target;
		for (int i = 0; i < attrIds.length; i++) {
			target = originalAttrs.get(attrIds[i]);
			if (target != null) {
				a.put(target);
			}
		}

		return a;
	}

	/**
	 * @see javax.naming.directory.DirContext#modifyAttributes(javax.naming.Name,
	 * int, javax.naming.directory.Attributes)
	 */
	public void modifyAttributes(Name name, int modOp, Attributes attrs)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#modifyAttributes(String, int,
	 * Attributes)
	 */
	public void modifyAttributes(String name, int modOp, Attributes attrs)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#modifyAttributes(Name,
	 * ModificationItem[])
	 */
	public void modifyAttributes(Name name, ModificationItem[] mods)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#modifyAttributes(String,
	 * ModificationItem[])
	 */
	public void modifyAttributes(String name, ModificationItem[] mods)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#bind(Name, Object, Attributes)
	 */
	public void bind(Name name, Object obj, Attributes attrs)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#bind(String, Object, Attributes)
	 */
	public void bind(String name, Object obj, Attributes attrs)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#rebind(Name, Object, Attributes)
	 */
	public void rebind(Name name, Object obj, Attributes attrs)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#rebind(String, Object, Attributes)
	 */
	public void rebind(String name, Object obj, Attributes attrs)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#createSubcontext(Name, Attributes)
	 */
	public DirContext createSubcontext(Name name, Attributes attrs)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#createSubcontext(String,
	 * Attributes)
	 */
	public DirContext createSubcontext(String name, Attributes attrs)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#getSchema(Name)
	 */
	public DirContext getSchema(Name name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#getSchema(String)
	 */
	public DirContext getSchema(String name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#getSchemaClassDefinition(Name)
	 */
	public DirContext getSchemaClassDefinition(Name name)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#getSchemaClassDefinition(String)
	 */
	public DirContext getSchemaClassDefinition(String name)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#search(Name, Attributes, String[])
	 */
	public NamingEnumeration search(Name name, Attributes matchingAttributes,
			String[] attributesToReturn) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#search(String, Attributes,
	 * String[])
	 */
	public NamingEnumeration search(String name, Attributes matchingAttributes,
			String[] attributesToReturn) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#search(Name, Attributes)
	 */
	public NamingEnumeration search(Name name, Attributes matchingAttributes)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#search(String, Attributes)
	 */
	public NamingEnumeration search(String name, Attributes matchingAttributes)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#search(Name, String,
	 * SearchControls)
	 */
	public NamingEnumeration search(Name name, String filter,
			SearchControls cons) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#search(String, String,
	 * SearchControls)
	 */
	public NamingEnumeration search(String name, String filter,
			SearchControls cons) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#search(Name, String, Object[],
	 * SearchControls)
	 */
	public NamingEnumeration search(Name name, String filterExpr,
			Object[] filterArgs, SearchControls cons) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.directory.DirContext#search(String, String, Object[],
	 * SearchControls)
	 */
	public NamingEnumeration search(String name, String filterExpr,
			Object[] filterArgs, SearchControls cons) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#lookup(Name)
	 */
	public Object lookup(Name name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#lookup(String)
	 */
	public Object lookup(String name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#bind(Name, Object)
	 */
	public void bind(Name name, Object obj) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#bind(String, Object)
	 */
	public void bind(String name, Object obj) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#rebind(Name, Object)
	 */
	public void rebind(Name name, Object obj) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#rebind(String, Object)
	 */
	public void rebind(String name, Object obj) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#unbind(Name)
	 */
	public void unbind(Name name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#unbind(String)
	 */
	public void unbind(String name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#rename(Name, Name)
	 */
	public void rename(Name oldName, Name newName) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#rename(String, String)
	 */
	public void rename(String oldName, String newName) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#list(Name)
	 */
	public NamingEnumeration list(Name name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#list(String)
	 */
	public NamingEnumeration list(String name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#listBindings(Name)
	 */
	public NamingEnumeration listBindings(Name name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#listBindings(String)
	 */
	public NamingEnumeration listBindings(String name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#destroySubcontext(Name)
	 */
	public void destroySubcontext(Name name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#destroySubcontext(String)
	 */
	public void destroySubcontext(String name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#createSubcontext(Name)
	 */
	public Context createSubcontext(Name name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#createSubcontext(String)
	 */
	public Context createSubcontext(String name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#lookupLink(Name)
	 */
	public Object lookupLink(Name name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#lookupLink(String)
	 */
	public Object lookupLink(String name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#getNameParser(Name)
	 */
	public NameParser getNameParser(Name name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#getNameParser(String)
	 */
	public NameParser getNameParser(String name) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#composeName(Name, Name)
	 */
	public Name composeName(Name name, Name prefix) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#composeName(String, String)
	 */
	public String composeName(String name, String prefix)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#addToEnvironment(String, Object)
	 */
	public Object addToEnvironment(String propName, Object propVal)
			throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#removeFromEnvironment(String)
	 */
	public Object removeFromEnvironment(String propName) throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#getEnvironment()
	 */
	public Hashtable getEnvironment() throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#close()
	 */
	public void close() throws NamingException {
		throw new UnsupportedOperationException("Not implemented.");
	}

	/**
	 * @see javax.naming.Context#getNameInNamespace()
	 */
	public String getNameInNamespace() {
		DistinguishedName result = new DistinguishedName(dn);
		result.prepend(base);
		return result.toString();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.ldap.support.DirContextOperations#getDn()
	 */
	public Name getDn() {
		return new DistinguishedName(dn);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.springframework.ldap.support.DirContextOperations#setDn(javax.naming
	 * .Name)
	 */
	public final void setDn(Name dn) {
		if (!updateMode) {
			this.dn = new DistinguishedName(dn.toString());
		}
		else {
			throw new IllegalStateException(
					"Not possible to call setDn() on a DirContextAdapter in update mode");
		}

	}

	/**
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	public boolean equals(Object obj) {
		// A subclass with identical values should NOT be considered equal.
		// EqualsBuilder in commons-lang cannot handle subclasses correctly.
		if (obj == null || obj.getClass() != this.getClass()) {
			return false;
		}
		return EqualsBuilder.reflectionEquals(this, obj);
	}

	/**
	 * @see Object#hashCode()
	 */
	public int hashCode() {
		return HashCodeBuilder.reflectionHashCode(this);
	}

	/**
	 * @see java.lang.Object#toString()
	 */
	public String toString() {
		StringBuffer buf = new StringBuffer();
		buf.append(getClass().getName());
		buf.append(":");
		if (dn != null) {
			buf.append(" dn=" + dn);
		}
		buf.append(" {");

		try {
			for (NamingEnumeration i = originalAttrs.getAll(); i.hasMore();) {
				Attribute attribute = (Attribute) i.next();
				if (attribute.size() == 1) {
					buf.append(attribute.getID());
					buf.append('=');
					buf.append(attribute.get());
				}
				else {
					for (int j = 0; j < attribute.size(); j++) {
						if (j > 0) {
							buf.append(", ");
						}
						buf.append(attribute.getID());
						buf.append('[');
						buf.append(j);
						buf.append("]=");
						buf.append(attribute.get(j));
					}
				}

				if (i.hasMore()) {
					buf.append(", ");
				}
			}
		}
		catch (NamingException e) {
			log.warn("Error in toString()");
		}
		buf.append('}');

		return buf.toString();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.ldap.core.DirContextOperations#getReferralUrl()
	 */
	public String getReferralUrl() {
		return referralUrl;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.springframework.ldap.core.DirContextOperations#isReferral()
	 */
	public boolean isReferral() {
		return StringUtils.hasLength(referralUrl);
	}

}
