/*
 * Copyright (c) 1999, 2017, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code 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 General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */


package javax.naming.directory;

import java.util.Hashtable;
import java.util.Enumeration;
import java.util.Locale;

import javax.naming.NamingException;
import javax.naming.NamingEnumeration;

/**
  * This class provides a basic implementation
  * of the Attributes interface.
  *<p>
  * BasicAttributes is either case-sensitive or case-insensitive (case-ignore).
  * This property is determined at the time the BasicAttributes constructor
  * is called.
  * In a case-insensitive BasicAttributes, the case of its attribute identifiers
  * is ignored when searching for an attribute, or adding attributes.
  * In a case-sensitive BasicAttributes, the case is significant.
  *<p>
  * When the BasicAttributes class needs to create an Attribute, it
  * uses BasicAttribute. There is no other dependency on BasicAttribute.
  *<p>
  * Note that updates to BasicAttributes (such as adding or removing an attribute)
  * does not affect the corresponding representation in the directory.
  * Updates to the directory can only be effected
  * using operations in the DirContext interface.
  *<p>
  * A BasicAttributes instance is not synchronized against concurrent
  * multithreaded access. Multiple threads trying to access and modify
  * a single BasicAttributes instance should lock the object.
  *
  * @author Rosanna Lee
  * @author Scott Seligman
  *
  * @see DirContext#getAttributes
  * @see DirContext#modifyAttributes
  * @see DirContext#bind
  * @see DirContext#rebind
  * @see DirContext#createSubcontext
  * @see DirContext#search
  * @since 1.3
  */

public class BasicAttributes implements Attributes {
    /**
     * Indicates whether case of attribute ids is ignored.
     * @serial
     */
    private boolean ignoreCase = false;

    // The 'key' in attrs is stored in the 'right case'.
    // If ignoreCase is true, key is aways lowercase.
    // If ignoreCase is false, key is stored as supplied by put().
    // %%% Not declared "private" due to bug 4064984.
    transient Hashtable<String,Attribute> attrs = new Hashtable<>(11);

    /**
      * Constructs a new instance of Attributes.
      * The character case of attribute identifiers
      * is significant when subsequently retrieving or adding attributes.
      */
    public BasicAttributes() {
    }

    /**
      * Constructs a new instance of Attributes.
      * If <code>ignoreCase</code> is true, the character case of attribute
      * identifiers is ignored; otherwise the case is significant.
      * @param ignoreCase true means this attribute set will ignore
      *                   the case of its attribute identifiers
      *                   when retrieving or adding attributes;
      *                   false means case is respected.
      */
    public BasicAttributes(boolean ignoreCase) {
        this.ignoreCase = ignoreCase;
    }

    /**
      * Constructs a new instance of Attributes with one attribute.
      * The attribute specified by attrID and val are added to the newly
      * created attribute.
      * The character case of attribute identifiers
      * is significant when subsequently retrieving or adding attributes.
      * @param attrID   non-null The id of the attribute to add.
      * @param val The value of the attribute to add. If null, a null
      *        value is added to the attribute.
      */
    public BasicAttributes(String attrID, Object val) {
        this();
        this.put(new BasicAttribute(attrID, val));
    }

    /**
      * Constructs a new instance of Attributes with one attribute.
      * The attribute specified by attrID and val are added to the newly
      * created attribute.
      * If <code>ignoreCase</code> is true, the character case of attribute
      * identifiers is ignored; otherwise the case is significant.
      * @param attrID   non-null The id of the attribute to add.
      *           If this attribute set ignores the character
      *           case of its attribute ids, the case of attrID
      *           is ignored.
      * @param val The value of the attribute to add. If null, a null
      *        value is added to the attribute.
      * @param ignoreCase true means this attribute set will ignore
      *                   the case of its attribute identifiers
      *                   when retrieving or adding attributes;
      *                   false means case is respected.
      */
    public BasicAttributes(String attrID, Object val, boolean ignoreCase) {
        this(ignoreCase);
        this.put(new BasicAttribute(attrID, val));
    }

    @SuppressWarnings("unchecked")
    public Object clone() {
        BasicAttributes attrset;
        try {
            attrset = (BasicAttributes)super.clone();
        } catch (CloneNotSupportedException e) {
            attrset = new BasicAttributes(ignoreCase);
        }
        attrset.attrs = (Hashtable<String,Attribute>)attrs.clone();
        return attrset;
    }

    public boolean isCaseIgnored() {
        return ignoreCase;
    }

    public int size() {
        return attrs.size();
    }

    public Attribute get(String attrID) {
        Attribute attr = attrs.get(
                ignoreCase ? attrID.toLowerCase(Locale.ENGLISH) : attrID);
        return (attr);
    }

    public NamingEnumeration<Attribute> getAll() {
        return new AttrEnumImpl();
    }

    public NamingEnumeration<String> getIDs() {
        return new IDEnumImpl();
    }

    public Attribute put(String attrID, Object val) {
        return this.put(new BasicAttribute(attrID, val));
    }

    public Attribute put(Attribute attr) {
        String id = attr.getID();
        if (ignoreCase) {
            id = id.toLowerCase(Locale.ENGLISH);
        }
        return attrs.put(id, attr);
    }

    public Attribute remove(String attrID) {
        String id = (ignoreCase ? attrID.toLowerCase(Locale.ENGLISH) : attrID);
        return attrs.remove(id);
    }

    /**
     * Generates the string representation of this attribute set.
     * The string consists of each attribute identifier and the contents
     * of each attribute. The contents of this string is useful
     * for debugging and is not meant to be interpreted programmatically.
     *
     * @return A non-null string listing the contents of this attribute set.
     */
    public String toString() {
        if (attrs.size() == 0) {
            return("No attributes");
        } else {
            return attrs.toString();
        }
    }

    /**
     * Determines whether this <tt>BasicAttributes</tt> is equal to another
     * <tt>Attributes</tt>
     * Two <tt>Attributes</tt> are equal if they are both instances of
     * <tt>Attributes</tt>,
     * treat the case of attribute IDs the same way, and contain the
     * same attributes. Each <tt>Attribute</tt> in this <tt>BasicAttributes</tt>
     * is checked for equality using <tt>Object.equals()</tt>, which may have
     * be overridden by implementations of <tt>Attribute</tt>).
     * If a subclass overrides <tt>equals()</tt>,
     * it should override <tt>hashCode()</tt>
     * as well so that two <tt>Attributes</tt> instances that are equal
     * have the same hash code.
     * @param obj the possibly null object to compare against.
     *
     * @return true If obj is equal to this BasicAttributes.
     * @see #hashCode
     */
    public boolean equals(Object obj) {
        if ((obj != null) && (obj instanceof Attributes)) {
            Attributes target = (Attributes)obj;

            // Check case first
            if (ignoreCase != target.isCaseIgnored()) {
                return false;
            }

            if (size() == target.size()) {
                Attribute their, mine;
                try {
                    NamingEnumeration<?> theirs = target.getAll();
                    while (theirs.hasMore()) {
                        their = (Attribute)theirs.next();
                        mine = get(their.getID());
                        if (!their.equals(mine)) {
                            return false;
                        }
                    }
                } catch (NamingException e) {
                    return false;
                }
                return true;
            }
        }
        return false;
    }

    /**
     * Calculates the hash code of this BasicAttributes.
     *<p>
     * The hash code is computed by adding the hash code of
     * the attributes of this object. If this BasicAttributes
     * ignores case of its attribute IDs, one is added to the hash code.
     * If a subclass overrides <tt>hashCode()</tt>,
     * it should override <tt>equals()</tt>
     * as well so that two <tt>Attributes</tt> instances that are equal
     * have the same hash code.
     *
     * @return an int representing the hash code of this BasicAttributes instance.
     * @see #equals
     */
    public int hashCode() {
        int hash = (ignoreCase ? 1 : 0);
        try {
            NamingEnumeration<?> all = getAll();
            while (all.hasMore()) {
                hash += all.next().hashCode();
            }
        } catch (NamingException e) {}
        return hash;
    }

    /**
     * Overridden to avoid exposing implementation details.
     * @serialData Default field (ignoreCase flag -- a boolean), followed by
     * the number of attributes in the set
     * (an int), and then the individual Attribute objects.
     */
    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException {
        s.defaultWriteObject(); // write out the ignoreCase flag
        s.writeInt(attrs.size());
        Enumeration<Attribute> attrEnum = attrs.elements();
        while (attrEnum.hasMoreElements()) {
            s.writeObject(attrEnum.nextElement());
        }
    }

    /**
     * Overridden to avoid exposing implementation details.
     */
    private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();  // read in the ignoreCase flag
        int n = s.readInt();    // number of attributes
        attrs = (n >= 1)
                ? new Hashtable<>(1 + (int) (Math.min(768, n) / .75f))
                : new Hashtable<>(2); // can't have initial size of 0 (grrr...)
        while (--n >= 0) {
            put((Attribute)s.readObject());
        }
    }


class AttrEnumImpl implements NamingEnumeration<Attribute> {

    Enumeration<Attribute> elements;

    public AttrEnumImpl() {
        this.elements = attrs.elements();
    }

    public boolean hasMoreElements() {
        return elements.hasMoreElements();
    }

    public Attribute nextElement() {
        return elements.nextElement();
    }

    public boolean hasMore() throws NamingException {
        return hasMoreElements();
    }

    public Attribute next() throws NamingException {
        return nextElement();
    }

    public void close() throws NamingException {
        elements = null;
    }
}

class IDEnumImpl implements NamingEnumeration<String> {

    Enumeration<Attribute> elements;

    public IDEnumImpl() {
        // Walking through the elements, rather than the keys, gives
        // us attribute IDs that have not been converted to lowercase.
        this.elements = attrs.elements();
    }

    public boolean hasMoreElements() {
        return elements.hasMoreElements();
    }

    public String nextElement() {
        Attribute attr = elements.nextElement();
        return attr.getID();
    }

    public boolean hasMore() throws NamingException {
        return hasMoreElements();
    }

    public String next() throws NamingException {
        return nextElement();
    }

    public void close() throws NamingException {
        elements = null;
    }
}

    /**
     * Use serialVersionUID from JNDI 1.1.1 for interoperability.
     */
    private static final long serialVersionUID = 4980164073184639448L;
}
