/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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
 *
 *  https://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.apache.directory.api.ldap.model.entry;


import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.apache.directory.api.i18n.I18n;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
import org.apache.directory.api.ldap.model.ldif.LdapLdifException;
import org.apache.directory.api.ldap.model.ldif.LdifAttributesReader;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.util.Strings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * A default implementation of a ServerEntry which should suite most
 * use cases.<br>
 * <br>
 * This class is final, it should not be extended.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public final class DefaultEntry implements Entry
{
    /** Used for serialization */
    private static final long serialVersionUID = 2L;

    /** The logger for this class */
    private static final Logger LOG = LoggerFactory.getLogger( DefaultEntry.class );

    /** The Dn for this entry */
    private Dn dn;

    /** A map containing all the attributes for this entry */
    private Map<String, Attribute> attributes = new HashMap<>();

    /** A speedup to get the ObjectClass attribute */
    private static AttributeType objectClassAttributeType;

    /** The SchemaManager */
    private transient SchemaManager schemaManager;

    /** The computed hashcode. We don't want to compute it each time the hashcode() method is called */
    private volatile int h;

    /** A mutex to manage synchronization*/
    private static final Object MUTEX = new Object();


    //-------------------------------------------------------------------------
    // Constructors
    //-------------------------------------------------------------------------
    /**
     * Creates a new instance of DefaultEntry.
     * <p>
     * This entry <b>must</b> be initialized before being used !
     */
    public DefaultEntry()
    {
        this( ( SchemaManager ) null );
    }


    /**
     * <p>
     * Creates a new instance of DefaultEntry, schema aware.
     * </p>
     * <p>
     * No attributes will be created.
     * </p>
     *
     * @param schemaManager The reference to the schemaManager
     */
    public DefaultEntry( SchemaManager schemaManager )
    {
        this.schemaManager = schemaManager;
        dn = Dn.EMPTY_DN;

        // Initialize the ObjectClass object
        if ( schemaManager != null )
        {
            initObjectClassAT();
        }
    }


    /**
     * Creates a new instance of DefaultEntry, with a Dn.
     *
     * @param dn The String Dn for this serverEntry. Can be null.
     * @throws LdapInvalidDnException If the Dn is invalid
     */
    public DefaultEntry( String dn ) throws LdapInvalidDnException
    {
        this.dn = new Dn( dn );
    }


    /**
     * Creates a new instance of DefaultEntry, with a Dn.
     *
     * @param dn The Dn for this serverEntry. Can be null.
     */
    public DefaultEntry( Dn dn )
    {
        this.dn = dn;
    }


    /**
     * <p>
     * Creates a new instance of DefaultEntry, schema aware.
     * </p>
     * <p>
     * No attributes will be created.
     * </p>
     *
     * @param schemaManager The reference to the schemaManager
     * @param dn The String Dn for this serverEntry. Can be null.
     * @throws LdapInvalidDnException If the Dn is invalid
     */
    public DefaultEntry( SchemaManager schemaManager, String dn ) throws LdapInvalidDnException
    {
        this.schemaManager = schemaManager;

        if ( Strings.isEmpty( dn ) )
        {
            this.dn = Dn.EMPTY_DN;
        }
        else
        {
            this.dn = new Dn( schemaManager, dn );
        }

        // Initialize the ObjectClass object
        initObjectClassAT();
    }


    /**
     * <p>
     * Creates a new instance of DefaultEntry, schema aware.
     * </p>
     * <p>
     * No attributes will be created.
     * </p>
     *
     * @param schemaManager The reference to the schemaManager
     * @param dn The Dn for this serverEntry. Can be null.
     */
    public DefaultEntry( SchemaManager schemaManager, Dn dn )
    {
        this.schemaManager = schemaManager;

        if ( dn == null )
        {
            this.dn = Dn.EMPTY_DN;
        }
        else
        {
            this.dn = normalizeDn( dn );
        }

        // Initialize the ObjectClass object
        initObjectClassAT();
    }


    /**
     * Creates a new instance of DefaultEntry, with a
     * Dn and a list of IDs.
     *
     * @param dn The Dn for this serverEntry. Can be null.
     * @param elements The list of elements to inject in the entry
     * @throws LdapException If the elements are invalid
     * @throws LdapException If the provided Dn or elements are invalid
     */
    public DefaultEntry( String dn, Object... elements ) throws LdapException
    {
        this( null, dn, elements );
    }


    /**
     * Creates a new instance of DefaultEntry, with a
     * Dn and a list of IDs.
     *
     * @param dn The Dn for this serverEntry. Can be null.
     * @param elements The list of attributes to create.
     * @throws LdapException If the provided Dn or elements are invalid
     */
    public DefaultEntry( Dn dn, Object... elements ) throws LdapException
    {
        this( null, dn, elements );
    }


    /**
     * Creates a new instance of DefaultEntry, with a
     * Dn and a list of IDs.
     *
     * @param schemaManager The SchemaManager
     * @param dn The Dn for this serverEntry. Can be null.
     * @param elements The list of attributes to create.
     * @throws LdapException If the provided Dn or elements are invalid
     */
    public DefaultEntry( SchemaManager schemaManager, String dn, Object... elements ) throws LdapException
    {
        this( schemaManager, new Dn( schemaManager, dn ), elements );
    }


    /**
     * Creates a new instance of DefaultEntry, with a
     * Dn and a list of IDs.
     *
     * @param schemaManager The reference to the schemaManager
     * @param dn The Dn for this serverEntry. Can be null.
     * @param elements The list of attributes to create.
     * @throws LdapException If the provided Dn or Elements are invalid
     */
    public DefaultEntry( SchemaManager schemaManager, Dn dn, Object... elements ) throws LdapException
    {
        DefaultEntry entry = ( DefaultEntry ) createEntry( schemaManager, elements );

        this.dn = dn;
        this.attributes = entry.attributes;
        this.schemaManager = schemaManager;

        if ( schemaManager != null )
        {
            if ( !dn.isSchemaAware() )
            {
                this.dn = new Dn( schemaManager, dn );
            }

            initObjectClassAT();
        }
    }


    /**
     * <p>
     * Creates a new instance of DefaultEntry, copying
     * another entry.
     * </p>
     * <p>
     * No attributes will be created.
     * </p>
     *
     * @param schemaManager The reference to the schemaManager
     * @param entry the entry to copy
     * @throws LdapException If the provided entry is invalid
     */
    public DefaultEntry( SchemaManager schemaManager, Entry entry ) throws LdapException
    {
        this.schemaManager = schemaManager;

        // Initialize the ObjectClass object
        initObjectClassAT();

        // We will clone the existing entry, because it may be normalized
        if ( entry.getDn() != null )
        {
            dn = normalizeDn( entry.getDn() );
        }
        else
        {
            dn = Dn.EMPTY_DN;
        }

        // Init the attributes map
        attributes = new HashMap<>( entry.size() );

        // and copy all the attributes
        for ( Attribute attribute : entry )
        {
            try
            {
                // First get the AttributeType
                AttributeType attributeType = attribute.getAttributeType();

                if ( attributeType == null )
                {
                    attributeType = schemaManager.lookupAttributeTypeRegistry( attribute.getId() );
                }

                // Create a new ServerAttribute.
                Attribute serverAttribute = new DefaultAttribute( attributeType, attribute );

                // And store it
                add( serverAttribute );
            }
            catch ( Exception ne )
            {
                // Just log a warning
                if ( LOG.isWarnEnabled() )
                {
                    LOG.warn( I18n.msg( I18n.MSG_13200_CANT_STORE_ATTRIBUTE, attribute.getId() ) );
                }

                throw ne;
            }
        }
    }


    //-------------------------------------------------------------------------
    // Helper methods
    //-------------------------------------------------------------------------
    private Entry createEntry( SchemaManager schemaManager, Object... elements )
        throws LdapInvalidAttributeValueException, LdapLdifException
    {
        StringBuilder sb = new StringBuilder();
        int pos = 0;
        boolean valueExpected = false;

        for ( Object element : elements )
        {
            if ( !valueExpected )
            {
                if ( !( element instanceof String ) )
                {
                    throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, I18n.err(
                        I18n.ERR_13233_ATTRIBUTE_ID_MUST_BE_A_STRING, pos + 1 ) );
                }

                String attribute = ( String ) element;
                sb.append( attribute );

                if ( attribute.indexOf( ':' ) != -1 )
                {
                    sb.append( '\n' );
                }
                else
                {
                    valueExpected = true;
                }
            }
            else
            {
                if ( element instanceof String )
                {
                    sb.append( ": " ).append( ( String ) element ).append( '\n' );
                }
                else if ( element instanceof byte[] )
                {
                    sb.append( ":: " );
                    sb.append( new String( Base64.getEncoder().encode( ( byte[] ) element ), StandardCharsets.UTF_8 ) );
                    sb.append( '\n' );
                }
                else
                {
                    throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, I18n.err(
                        I18n.ERR_13234_ATTRIBUTE_VAL_STRING_OR_BYTE, pos + 1 ) );
                }

                valueExpected = false;
            }
        }

        if ( valueExpected )
        {
            throw new LdapInvalidAttributeValueException( ResultCodeEnum.INVALID_ATTRIBUTE_SYNTAX, I18n
                .err( I18n.ERR_13250_VALUE_MISSING_AT_THE_END ) );
        }

        try ( LdifAttributesReader reader = new LdifAttributesReader() )
        {
            return reader.parseEntry( schemaManager, sb.toString() );
        }
        catch ( IOException e )
        {
            throw new LdapLdifException( I18n.err( I18n.ERR_13248_CANNOT_READ_ENTRY ), e );
        }
    }


    /**
     * Get the trimmed and lower cased entry ID
     *
     * @param upId The ID
     * @return The retrieved ID
     */
    private String getId( String upId )
    {
        String id = Strings.trim( Strings.toLowerCaseAscii( upId ) );

        // If empty, throw an error
        if ( Strings.isEmpty( id ) )
        {
            String message = I18n.err( I18n.ERR_13216_AT_ID_NULL );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        return id;
    }


    /**
     * Get the UpId if it is null.
     *
     * @param upId The ID
     * @param attributeType The AttributeType to retrieve
     * @return the retrieved ID
     */
    private String getUpId( String upId, AttributeType attributeType )
    {
        String normUpId = Strings.trim( upId );

        if ( attributeType == null )
        {
            if ( Strings.isEmpty( normUpId ) )
            {
                String message = I18n.err( I18n.ERR_13226_CANNOT_ADD_ATTRIBUTE_NO_ID );
                LOG.error( message );
                throw new IllegalArgumentException( message );
            }

            return upId;
        }
        else if ( Strings.isEmpty( normUpId ) )
        {
            String id = attributeType.getName();

            if ( Strings.isEmpty( id ) )
            {
                id = attributeType.getOid();
            }

            return id;
        }
        else
        {
            return upId;
        }
    }


    /**
     * This method is used to initialize the OBJECT_CLASS_AT attributeType.
     *
     * We want to do it only once, so it's a synchronized method. Note that
     * the alternative would be to call the lookup() every time, but this won't
     * be very efficient, as it will get the AT from a map, which is also
     * synchronized, so here, we have a very minimal cost.
     *
     * We can't do it once as a static part in the body of this class, because
     * the access to the registries is mandatory to get back the AttributeType.
     */
    private void initObjectClassAT()
    {
        if ( schemaManager == null )
        {
            return;
        }

        try
        {
            synchronized ( MUTEX )
            {
                if ( objectClassAttributeType == null )
                {
                    objectClassAttributeType = schemaManager
                        .lookupAttributeTypeRegistry( SchemaConstants.OBJECT_CLASS_AT );
                }
            }
        }
        catch ( LdapException ne )
        {
            // do nothing...
        }
    }


    /**
     * Normalizes the given Dn if it was not already normalized
     *
     * @param dn the Dn to be normalized
     * @return The normalized Dn
     */
    private Dn normalizeDn( Dn dn )
    {
        if ( !dn.isSchemaAware() )
        {
            try
            {
                // The dn must be normalized
                return new Dn( schemaManager, dn );
            }
            catch ( LdapException ne )
            {
                if ( LOG.isWarnEnabled() )
                {
                    LOG.warn( I18n.msg( I18n.MSG_13201_DN_CANT_BE_NORMALIZED, dn ) );
                }

                return dn;
            }
        }
        else
        {
            return dn;
        }
    }


    /**
     * A helper method to recompute the hash code
     */
    private void rehash()
    {
        int hTmp = 37;
        h = hTmp * 17 + dn.hashCode();
    }


    /**
     * Add a new EntryAttribute, with its upId. If the upId is null,
     * default to the AttributeType name.
     *
     * Updates the AttributeMap.
     *
     * @param upId The user provided ID for the attribute to create
     * @param attributeType The AttributeType to use
     * @param values The values to add to this attribute
     * @throws LdapInvalidAttributeValueException If one of the value is incorrect
     */
    protected void createAttribute( String upId, AttributeType attributeType, byte[]... values )
        throws LdapInvalidAttributeValueException
    {
        Attribute attribute = new DefaultAttribute( attributeType, values );
        attribute.setUpId( upId, attributeType );
        attributes.put( attributeType.getOid(), attribute );
    }


    /**
     * Add a new EntryAttribute, with its upId. If the upId is null,
     * default to the AttributeType name.
     *
     * Updates the AttributeMap.
     *
     * @param upId The user provided ID for the attribute to create
     * @param attributeType The AttributeType to use
     * @param values The values to add to this attribute
     * @throws LdapInvalidAttributeValueException If one of the value is incorrect
     */
    protected void createAttribute( String upId, AttributeType attributeType, String... values )
        throws LdapInvalidAttributeValueException
    {
        Attribute attribute = new DefaultAttribute( attributeType, values );
        attribute.setUpId( upId, attributeType );
        attributes.put( attributeType.getOid(), attribute );
    }


    /**
     * Add a new EntryAttribute, with its upId. If the upId is null,
     * default to the AttributeType name.
     *
     * Updates the AttributeMap.
     *
     * @param upId The user provided ID for the attribute to create
     * @param attributeType The AttributeType to use
     * @param values The values to add to this attribute
     * @throws LdapInvalidAttributeValueException If one of the value is incorrect
     */
    protected void createAttribute( String upId, AttributeType attributeType, Value... values )
        throws LdapInvalidAttributeValueException
    {
        Attribute attribute = new DefaultAttribute( attributeType, values );
        attribute.setUpId( upId, attributeType );
        attributes.put( attributeType.getOid(), attribute );
    }


    /**
     * Returns the attributeType from an Attribute ID.
     *
     * @param upId The ID we are looking for
     * @return The found attributeType
     * @throws LdapException If the lookup failed
     */
    protected AttributeType getAttributeType( String upId ) throws LdapException
    {
        if ( Strings.isEmpty( Strings.trim( upId ) ) )
        {
            String message = I18n.err( I18n.ERR_13204_NULL_ATTRIBUTE_ID );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        return schemaManager.lookupAttributeTypeRegistry( upId );
    }


    //-------------------------------------------------------------------------
    // Entry methods
    //-------------------------------------------------------------------------
    /**
     * {@inheritDoc}
     */
    @Override
    public Entry add( AttributeType attributeType, byte[]... values ) throws LdapException
    {
        if ( attributeType == null )
        {
            String message = I18n.err( I18n.ERR_13203_ATTRIBUTE_TYPE_NULL_NOT_ALLOWED );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        if ( ( values == null ) || ( values.length == 0 ) )
        {
            String message = I18n.err( I18n.ERR_13232_NO_VALUE_NOT_ALLOWED );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        // ObjectClass with binary values are not allowed
        if ( attributeType.equals( objectClassAttributeType ) )
        {
            String message = I18n.err( I18n.ERR_13227_NON_STRING_VALUE_NOT_ALLOWED );
            LOG.error( message );
            throw new UnsupportedOperationException( message );
        }

        Attribute attribute = attributes.get( attributeType.getOid() );

        if ( attribute != null )
        {
            // This Attribute already exist, we add the values
            // into it
            attribute.add( values );
        }
        else
        {
            // We have to create a new Attribute and set the values.
            // The upId, which is set to null, will be setup by the
            // createAttribute method
            createAttribute( null, attributeType, values );
        }

        return this;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Entry add( AttributeType attributeType, String... values ) throws LdapException
    {
        if ( attributeType == null )
        {
            String message = I18n.err( I18n.ERR_13203_ATTRIBUTE_TYPE_NULL_NOT_ALLOWED );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        Attribute attribute = attributes.get( attributeType.getOid() );

        if ( attribute != null )
        {
            // This Attribute already exist, we add the values
            // into it
            attribute.add( values );
        }
        else
        {
            // We have to create a new Attribute and set the values.
            // The upId, which is set to null, will be setup by the
            // createAttribute method
            createAttribute( null, attributeType, values );
        }

        return this;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Entry add( AttributeType attributeType, Value... values ) throws LdapException
    {
        if ( attributeType == null )
        {
            String message = I18n.err( I18n.ERR_13203_ATTRIBUTE_TYPE_NULL_NOT_ALLOWED );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        Attribute attribute = attributes.get( attributeType.getOid() );

        if ( attribute != null )
        {
            // This Attribute already exist, we add the values
            // into it
            attribute.add( values );
        }
        else
        {
            // We have to create a new Attribute and set the values.
            // The upId, which is set to null, will be setup by the
            // createAttribute method
            createAttribute( null, attributeType, values );
        }

        return this;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Entry add( String upId, AttributeType attributeType, byte[]... values ) throws LdapException
    {
        // ObjectClass with binary values are not allowed
        if ( attributeType.equals( objectClassAttributeType ) )
        {
            String message = I18n.err( I18n.ERR_13227_NON_STRING_VALUE_NOT_ALLOWED );
            LOG.error( message );
            throw new UnsupportedOperationException( message );
        }

        Attribute attribute = attributes.get( attributeType.getOid() );

        String id = getUpId( upId, attributeType );

        if ( attribute != null )
        {
            // This Attribute already exist, we add the values
            // into it
            attribute.add( values );
            attribute.setUpId( id, attributeType );
        }
        else
        {
            // We have to create a new Attribute and set the values
            // and the upId
            createAttribute( id, attributeType, values );
        }

        return this;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Entry add( String upId, AttributeType attributeType, Value... values ) throws LdapException
    {
        if ( attributeType == null )
        {
            String message = I18n.err( I18n.ERR_13203_ATTRIBUTE_TYPE_NULL_NOT_ALLOWED );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        String id = getUpId( upId, attributeType );

        Attribute attribute = attributes.get( attributeType.getOid() );

        if ( attribute != null )
        {
            // This Attribute already exist, we add the values
            // into it
            attribute.add( values );
            attribute.setUpId( id, attributeType );
        }
        else
        {
            createAttribute( id, attributeType, values );
        }

        return this;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Entry add( String upId, AttributeType attributeType, String... values ) throws LdapException
    {
        if ( attributeType == null )
        {
            String message = I18n.err( I18n.ERR_13203_ATTRIBUTE_TYPE_NULL_NOT_ALLOWED );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        String id = getUpId( upId, attributeType );

        Attribute attribute = attributes.get( attributeType.getOid() );

        if ( attribute != null )
        {
            // This Attribute already exist, we add the values
            // into it
            attribute.add( values );
            attribute.setUpId( id, attributeType );
        }
        else
        {
            // We have to create a new Attribute and set the values
            // and the upId
            createAttribute( id, attributeType, values );
        }

        return this;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Entry add( Attribute... attributes ) throws LdapException
    {
        // Loop on all the added attributes
        for ( Attribute attribute : attributes )
        {
            AttributeType attributeType = attribute.getAttributeType();

            if ( attributeType != null )
            {
                String oid = attributeType.getOid();

                if ( this.attributes.containsKey( oid ) )
                {
                    // We already have an attribute with the same AttributeType
                    // Just add the new values into it.
                    Attribute existingAttribute = this.attributes.get( oid );

                    for ( Value value : attribute )
                    {
                        existingAttribute.add( value );
                    }

                    // And update the upId
                    existingAttribute.setUpId( attribute.getUpId() );
                }
                else
                {
                    // The attributeType does not exist, add it
                    this.attributes.put( oid, attribute );
                }
            }
            else
            {
                // If the attribute already exist, we will add the new values.
                if ( contains( attribute ) )
                {
                    Attribute existingAttribute = get( attribute.getId() );

                    // Loop on all the values, and add them to the existing attribute
                    for ( Value value : attribute )
                    {
                        existingAttribute.add( value );
                    }
                }
                else
                {
                    // Stores the attribute into the entry
                    this.attributes.put( attribute.getId(), attribute );
                }
            }
        }

        return this;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Entry add( String upId, byte[]... values ) throws LdapException
    {
        if ( Strings.isEmpty( upId ) )
        {
            String message = I18n.err( I18n.ERR_13204_NULL_ATTRIBUTE_ID );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        // First, transform the upID to a valid ID
        String id = getId( upId );

        if ( schemaManager != null )
        {
            add( upId, schemaManager.lookupAttributeTypeRegistry( id ), values );
        }
        else
        {
            // Now, check to see if we already have such an attribute
            Attribute attribute = attributes.get( id );

            if ( attribute != null )
            {
                // This Attribute already exist, we add the values
                // into it. (If the values already exists, they will
                // not be added, but this is done in the add() method)
                attribute.add( values );
                attribute.setUpId( upId );
            }
            else
            {
                // We have to create a new Attribute and set the values
                // and the upId
                attributes.put( id, new DefaultAttribute( upId, values ) );
            }
        }

        return this;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Entry add( String upId, String... values ) throws LdapException
    {
        if ( Strings.isEmpty( upId ) )
        {
            String message = I18n.err( I18n.ERR_13204_NULL_ATTRIBUTE_ID );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        // First, transform the upID to a valid ID
        String id = getId( upId );

        if ( schemaManager != null )
        {
            add( upId, schemaManager.lookupAttributeTypeRegistry( upId ), values );
        }
        else
        {
            // Now, check to see if we already have such an attribute
            Attribute attribute = attributes.get( id );

            if ( attribute != null )
            {
                // This Attribute already exist, we add the values
                // into it. (If the values already exists, they will
                // not be added, but this is done in the add() method)
                attribute.add( values );
                attribute.setUpId( upId );
            }
            else
            {
                // We have to create a new Attribute and set the values
                // and the upId
                attributes.put( id, new DefaultAttribute( upId, values ) );
            }
        }

        return this;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Entry add( String upId, Value... values ) throws LdapException
    {
        if ( Strings.isEmpty( upId ) )
        {
            String message = I18n.err( I18n.ERR_13204_NULL_ATTRIBUTE_ID );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        // First, transform the upID to a valid ID
        String id = getId( upId );

        if ( schemaManager != null )
        {
            add( upId, schemaManager.lookupAttributeTypeRegistry( upId ), values );
        }
        else
        {
            // Now, check to see if we already have such an attribute
            Attribute attribute = attributes.get( id );

            if ( attribute != null )
            {
                // This Attribute already exist, we add the values
                // into it. (If the values already exists, they will
                // not be added, but this is done in the add() method)
                attribute.add( values );
                attribute.setUpId( upId );
            }
            else
            {
                // We have to create a new Attribute and set the values
                // and the upId
                attributes.put( id, new DefaultAttribute( upId, values ) );
            }
        }

        return this;
    }


    /**
     * Clone an entry. All the element are duplicated, so a modification on
     * the original object won't affect the cloned object, as a modification
     * on the cloned object has no impact on the original object
     */
    @Override
    public Entry clone()
    {
        // First, clone the structure
        DefaultEntry clone = ( DefaultEntry ) shallowClone();

        // now clone all the attributes
        clone.attributes.clear();

        if ( schemaManager != null )
        {
            for ( Attribute attribute : attributes.values() )
            {
                String oid = attribute.getAttributeType().getOid();
                clone.attributes.put( oid, attribute.clone() );
            }
        }
        else
        {
            for ( Attribute attribute : attributes.values() )
            {
                clone.attributes.put( attribute.getId(), attribute.clone() );
            }

        }

        // We are done !
        return clone;

        /*
        // First, clone the structure
        //DefaultEntry clone = ( DefaultEntry ) shallowClone();
        try
        {
            DefaultEntry clone = ( DefaultEntry ) super.clone();
            clone.attributes = new HashMap<>( attributes.size() );

            // now clone all the attributes
            //clone.attributes.clear();

            if ( schemaManager != null )
            {
                for ( Attribute attribute : attributes.values() )
                {
                    String oid = attribute.getAttributeType().getOid();
                    clone.attributes.put( oid, attribute.clone() );
                }
            }
            else
            {
                for ( Attribute attribute : attributes.values() )
                {
                    clone.attributes.put( attribute.getId(), attribute.clone() );
                }
            }

            // We are done !
            return clone;
        }
        catch ( CloneNotSupportedException cnse )
        {
            return this;
        }
        */
    }


    /**
     * Shallow clone an entry. We don't clone the Attributes
     */
    @SuppressWarnings("unchecked")
    @Override
    public Entry shallowClone()
    {
        try
        {
            // First, clone the structure
            DefaultEntry clone = ( DefaultEntry ) super.clone();

            // An Entry has a Dn and many attributes.
            // note that Dn is immutable now
            clone.dn = dn;

            // then clone the ClientAttribute Map.
            clone.attributes = ( Map<String, Attribute> ) ( ( ( HashMap<String, Attribute> ) attributes )
                .clone() );

            // We are done !
            return clone;
        }
        catch ( CloneNotSupportedException cnse )
        {
            return null;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean contains( Attribute... attributes )
    {
        if ( schemaManager == null )
        {
            for ( Attribute attribute : attributes )
            {
                if ( attribute == null )
                {
                    return this.attributes.size() == 0;
                }

                if ( !this.attributes.containsKey( attribute.getId() ) )
                {
                    return false;
                }
            }
        }
        else
        {
            for ( Attribute entryAttribute : attributes )
            {
                if ( entryAttribute == null )
                {
                    return this.attributes.size() == 0;
                }

                AttributeType attributeType = entryAttribute.getAttributeType();

                if ( ( attributeType == null ) || !this.attributes.containsKey( attributeType.getOid() ) )
                {
                    return false;
                }
            }
        }

        return true;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean containsAttribute( String... attributes )
    {
        if ( schemaManager == null )
        {
            for ( String attribute : attributes )
            {
                String id = getId( attribute );

                if ( !this.attributes.containsKey( id ) )
                {
                    return false;
                }
            }

            return true;
        }
        else
        {
            for ( String attribute : attributes )
            {
                try
                {
                    if ( !containsAttribute( schemaManager.lookupAttributeTypeRegistry( attribute ) ) )
                    {
                        return false;
                    }
                }
                catch ( LdapException ne )
                {
                    return false;
                }
            }

            return true;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean containsAttribute( AttributeType attributeType )
    {
        if ( attributeType == null )
        {
            return false;
        }

        return attributes.containsKey( attributeType.getOid() );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean contains( AttributeType attributeType, byte[]... values )
    {
        if ( attributeType == null )
        {
            return false;
        }

        Attribute attribute = attributes.get( attributeType.getOid() );

        if ( attribute != null )
        {
            return attribute.contains( values );
        }
        else
        {
            return false;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean contains( AttributeType attributeType, String... values )
    {
        if ( attributeType == null )
        {
            return false;
        }

        Attribute attribute = attributes.get( attributeType.getOid() );

        if ( attribute != null )
        {
            return attribute.contains( values );
        }
        else
        {
            return false;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean contains( AttributeType attributeType, Value... values )
    {
        if ( attributeType == null )
        {
            return false;
        }

        Attribute attribute = attributes.get( attributeType.getOid() );

        if ( attribute != null )
        {
            return attribute.contains( values );
        }
        else
        {
            return false;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean contains( String upId, byte[]... values )
    {
        if ( Strings.isEmpty( upId ) )
        {
            return false;
        }

        String id = getId( upId );

        if ( schemaManager != null )
        {
            try
            {
                return contains( schemaManager.lookupAttributeTypeRegistry( id ), values );
            }
            catch ( LdapException le )
            {
                return false;
            }
        }

        Attribute attribute = attributes.get( id );

        if ( attribute == null )
        {
            return false;
        }

        return attribute.contains( values );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean contains( String upId, String... values )
    {
        if ( Strings.isEmpty( upId ) )
        {
            return false;
        }

        String id = getId( upId );

        if ( schemaManager != null )
        {
            try
            {
                return contains( schemaManager.lookupAttributeTypeRegistry( id ), values );
            }
            catch ( LdapException le )
            {
                return false;
            }
        }

        Attribute attribute = attributes.get( id );

        if ( attribute == null )
        {
            return false;
        }

        return attribute.contains( values );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean contains( String upId, Value... values )
    {
        if ( Strings.isEmpty( upId ) )
        {
            return false;
        }

        String id = getId( upId );

        if ( schemaManager != null )
        {
            try
            {
                return contains( schemaManager.lookupAttributeTypeRegistry( id ), values );
            }
            catch ( LdapException le )
            {
                return false;
            }
        }

        Attribute attribute = attributes.get( id );

        if ( attribute == null )
        {
            return false;
        }

        return attribute.contains( values );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Attribute get( String alias )
    {
        try
        {
            String id = getId( alias );

            if ( schemaManager != null )
            {
                try
                {
                    AttributeType attributeType = schemaManager.lookupAttributeTypeRegistry( id );

                    return attributes.get( attributeType.getOid() );
                }
                catch ( LdapException ne )
                {
                    String message = ne.getLocalizedMessage();
                    LOG.error( message );
                    return null;
                }
            }
            else
            {
                return attributes.get( id );
            }
        }
        catch ( IllegalArgumentException iea )
        {
            LOG.error( I18n.err( I18n.ERR_13217_FAILED_LOOKUP_AT, alias ) );
            return null;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Attribute get( AttributeType attributeType )
    {
        if ( attributeType != null )
        {
            return attributes.get( attributeType.getOid() );
        }
        else
        {
            return null;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<Attribute> getAttributes()
    {
        return Collections.unmodifiableMap( attributes ).values();
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Attribute put( String upId, byte[]... values )
    {
        if ( Strings.isEmpty( upId ) )
        {
            String message = I18n.err( I18n.ERR_13204_NULL_ATTRIBUTE_ID );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        if ( schemaManager == null )
        {
            // Get the normalized form of the ID
            String id = getId( upId );

            // Create a new attribute
            Attribute clientAttribute = new DefaultAttribute( upId, values );

            // Replace the previous one, and return it back
            return attributes.put( id, clientAttribute );
        }
        else
        {
            try
            {
                return put( upId, getAttributeType( upId ), values );
            }
            catch ( LdapException ne )
            {
                String message = I18n.err( I18n.ERR_13212_ERROR_ADDING_VALUE, upId, ne.getLocalizedMessage() );
                LOG.error( message );
                throw new IllegalArgumentException( message, ne );
            }
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Attribute put( String upId, String... values )
    {
        if ( Strings.isEmpty( upId ) )
        {
            String message = I18n.err( I18n.ERR_13204_NULL_ATTRIBUTE_ID );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        if ( schemaManager == null )
        {
            // Get the normalized form of the ID
            String id = getId( upId );

            // Create a new attribute
            Attribute clientAttribute = new DefaultAttribute( upId, values );

            // Replace the previous one, and return it back
            return attributes.put( id, clientAttribute );
        }
        else
        {
            try
            {
                return put( upId, getAttributeType( upId ), values );
            }
            catch ( LdapException ne )
            {
                String message = I18n.err( I18n.ERR_13212_ERROR_ADDING_VALUE, upId, ne.getLocalizedMessage() );
                LOG.error( message );
                throw new IllegalArgumentException( message, ne );
            }
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Attribute put( String upId, Value... values )
    {
        if ( Strings.isEmpty( upId ) )
        {
            String message = I18n.err( I18n.ERR_13204_NULL_ATTRIBUTE_ID );
            LOG.error( message );
            throw new IllegalArgumentException( message );
        }

        if ( schemaManager == null )
        {
            // Get the normalized form of the ID
            String id = getId( upId );

            // Create a new attribute
            Attribute clientAttribute = new DefaultAttribute( upId, values );

            // Replace the previous one, and return it back
            return attributes.put( id, clientAttribute );
        }
        else
        {
            try
            {
                return put( upId, getAttributeType( upId ), values );
            }
            catch ( LdapException ne )
            {
                String message = I18n.err( I18n.ERR_13212_ERROR_ADDING_VALUE, upId, ne.getLocalizedMessage() );
                LOG.error( message );
                throw new IllegalArgumentException( message, ne );
            }
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public List<Attribute> put( Attribute... attributes ) throws LdapException
    {
        // First, get the existing attributes
        List<Attribute> previous = new ArrayList<>();

        if ( schemaManager == null )
        {
            for ( Attribute attribute : attributes )
            {
                String id = attribute.getId();

                if ( containsAttribute( id ) )
                {
                    // Store the attribute and remove it from the list
                    previous.add( get( id ) );
                    this.attributes.remove( id );
                }

                // add the new one
                this.attributes.put( id, attribute );
            }
        }
        else
        {
            for ( Attribute attribute : attributes )
            {
                if ( attribute == null )
                {
                    String message = I18n.err( I18n.ERR_13228_AT_LIST_NULL_ELEMENTS );
                    LOG.error( message );
                    throw new IllegalArgumentException( message );
                }

                if ( attribute.getAttributeType() == null )
                {
                    AttributeType attributeType = schemaManager.lookupAttributeTypeRegistry( attribute.getId() );
                    attribute.apply( attributeType );
                }

                Attribute removed = this.attributes.put( attribute.getAttributeType().getOid(), attribute );

                if ( removed != null )
                {
                    previous.add( removed );
                }
            }
        }

        // return the previous attributes
        return previous;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Attribute put( AttributeType attributeType, byte[]... values ) throws LdapException
    {
        return put( null, attributeType, values );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Attribute put( AttributeType attributeType, String... values ) throws LdapException
    {
        return put( null, attributeType, values );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Attribute put( AttributeType attributeType, Value... values ) throws LdapException
    {
        return put( null, attributeType, values );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Attribute put( String upId, AttributeType attributeType, byte[]... values ) throws LdapException
    {
        if ( attributeType == null )
        {
            try
            {
                attributeType = getAttributeType( upId );
            }
            catch ( Exception e )
            {
                String message = I18n.err( I18n.ERR_13231_NO_VALID_AT_FOR_THIS_ID );
                LOG.error( message );
                throw new IllegalArgumentException( message, e );
            }
        }
        else
        {
            if ( !Strings.isEmpty( upId ) )
            {
                AttributeType tempAT = getAttributeType( upId );

                if ( !tempAT.equals( attributeType ) )
                {
                    String message = I18n.err( I18n.ERR_13229_ID_INCOMPATIBLE_WITH_AT, upId, attributeType );
                    LOG.error( message );
                    throw new IllegalArgumentException( message );
                }
            }
            else
            {
                upId = getUpId( upId, attributeType );
            }
        }

        if ( attributeType.equals( objectClassAttributeType ) )
        {
            String message = I18n.err( I18n.ERR_13227_NON_STRING_VALUE_NOT_ALLOWED );
            LOG.error( message );
            throw new UnsupportedOperationException( message );
        }

        Attribute attribute = new DefaultAttribute( upId, attributeType, values );

        return attributes.put( attributeType.getOid(), attribute );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Attribute put( String upId, AttributeType attributeType, String... values ) throws LdapException
    {
        if ( attributeType == null )
        {
            try
            {
                attributeType = getAttributeType( upId );
            }
            catch ( Exception e )
            {
                String message = I18n.err( I18n.ERR_13231_NO_VALID_AT_FOR_THIS_ID );
                LOG.error( message );
                throw new IllegalArgumentException( message, e );
            }
        }
        else
        {
            if ( !Strings.isEmpty( upId ) )
            {
                AttributeType tempAT = getAttributeType( upId );

                if ( !tempAT.equals( attributeType ) )
                {
                    String message = I18n.err( I18n.ERR_13229_ID_INCOMPATIBLE_WITH_AT, upId, attributeType );
                    LOG.error( message );
                    throw new IllegalArgumentException( message );
                }
            }
            else
            {
                upId = getUpId( upId, attributeType );
            }
        }

        Attribute attribute = new DefaultAttribute( upId, attributeType, values );

        return attributes.put( attributeType.getOid(), attribute );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Attribute put( String upId, AttributeType attributeType, Value... values ) throws LdapException
    {
        if ( attributeType == null )
        {
            try
            {
                attributeType = getAttributeType( upId );
            }
            catch ( Exception e )
            {
                String message = I18n.err( I18n.ERR_13231_NO_VALID_AT_FOR_THIS_ID );
                LOG.error( message );
                throw new IllegalArgumentException( message, e );
            }
        }
        else
        {
            if ( !Strings.isEmpty( upId ) )
            {
                AttributeType tempAT = getAttributeType( upId );

                if ( !tempAT.equals( attributeType ) )
                {
                    String message = I18n.err( I18n.ERR_13229_ID_INCOMPATIBLE_WITH_AT, upId, attributeType );
                    LOG.error( message );
                    throw new IllegalArgumentException( message );
                }
            }
            else
            {
                upId = getUpId( upId, attributeType );
            }
        }

        Attribute attribute = new DefaultAttribute( upId, attributeType, values );

        return attributes.put( attributeType.getOid(), attribute );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public List<Attribute> remove( Attribute... attributes ) throws LdapException
    {
        List<Attribute> removedAttributes = new ArrayList<>();

        if ( schemaManager == null )
        {
            for ( Attribute attribute : attributes )
            {
                if ( containsAttribute( attribute.getId() ) )
                {
                    this.attributes.remove( attribute.getId() );
                    removedAttributes.add( attribute );
                }
            }
        }
        else
        {
            for ( Attribute attribute : attributes )
            {
                AttributeType attributeType = attribute.getAttributeType();

                if ( attributeType == null )
                {
                    String message = I18n.err( I18n.ERR_13203_ATTRIBUTE_TYPE_NULL_NOT_ALLOWED );
                    LOG.error( message );
                    throw new IllegalArgumentException( message );
                }

                if ( this.attributes.containsKey( attributeType.getOid() ) )
                {
                    this.attributes.remove( attributeType.getOid() );
                    removedAttributes.add( attribute );
                }
            }
        }

        return removedAttributes;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean remove( AttributeType attributeType, byte[]... values ) throws LdapException
    {
        if ( attributeType == null )
        {
            return false;
        }

        try
        {
            Attribute attribute = attributes.get( attributeType.getOid() );

            if ( attribute == null )
            {
                // Can't remove values from a not existing attribute !
                return false;
            }

            int nbOldValues = attribute.size();

            // Remove the values
            attribute.remove( values );

            if ( attribute.size() == 0 )
            {
                // No mare values, remove the attribute
                attributes.remove( attributeType.getOid() );

                return true;
            }

            return nbOldValues != attribute.size();
        }
        catch ( IllegalArgumentException iae )
        {
            LOG.error( I18n.err( I18n.ERR_13205_CANNOT_REMOVE_VAL_MISSING_ATTR, attributeType ) );
            return false;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean remove( AttributeType attributeType, String... values ) throws LdapException
    {
        if ( attributeType == null )
        {
            return false;
        }

        try
        {
            Attribute attribute = attributes.get( attributeType.getOid() );

            if ( attribute == null )
            {
                // Can't remove values from a not existing attribute !
                return false;
            }

            int nbOldValues = attribute.size();

            // Remove the values
            attribute.remove( values );

            if ( attribute.size() == 0 )
            {
                // No mare values, remove the attribute
                attributes.remove( attributeType.getOid() );

                return true;
            }

            return nbOldValues != attribute.size();
        }
        catch ( IllegalArgumentException iae )
        {
            LOG.error( I18n.err( I18n.ERR_13205_CANNOT_REMOVE_VAL_MISSING_ATTR, attributeType ) );
            return false;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean remove( AttributeType attributeType, Value... values ) throws LdapException
    {
        if ( attributeType == null )
        {
            return false;
        }

        try
        {
            Attribute attribute = attributes.get( attributeType.getOid() );

            if ( attribute == null )
            {
                // Can't remove values from a not existing attribute !
                return false;
            }

            int nbOldValues = attribute.size();

            // Remove the values
            attribute.remove( values );

            if ( attribute.size() == 0 )
            {
                // No mare values, remove the attribute
                attributes.remove( attributeType.getOid() );

                return true;
            }

            return nbOldValues != attribute.size();
        }
        catch ( IllegalArgumentException iae )
        {
            LOG.error( I18n.err( I18n.ERR_13205_CANNOT_REMOVE_VAL_MISSING_ATTR, attributeType ) );
            return false;
        }
    }


    /**
     * <p>
     * Removes the attribute with the specified AttributeTypes.
     * </p>
     * <p>
     * The removed attribute are returned by this method.
     * </p>
     * <p>
     * If there is no attribute with the specified AttributeTypes,
     * the return value is <code>null</code>.
     * </p>
     *
     * @param attributes the AttributeTypes to be removed
     */
    @Override
    public void removeAttributes( AttributeType... attributes )
    {
        if ( ( attributes == null ) || ( attributes.length == 0 ) || ( schemaManager == null ) )
        {
            return;
        }

        for ( AttributeType attributeType : attributes )
        {
            if ( attributeType == null )
            {
                continue;
            }

            this.attributes.remove( attributeType.getOid() );
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void removeAttributes( String... attributes )
    {
        if ( attributes.length == 0 )
        {
            return;
        }

        if ( schemaManager == null )
        {
            for ( String attribute : attributes )
            {
                Attribute attr = get( attribute );

                if ( attr != null )
                {
                    this.attributes.remove( attr.getId() );
                }
                else
                {
                    if ( LOG.isWarnEnabled() )
                    {
                        LOG.warn( I18n.err( I18n.ERR_13218_AT_DOES_NOT_EXIST, attribute ) );
                    }
                }
            }
        }
        else
        {
            for ( String attribute : attributes )
            {
                AttributeType attributeType = null;

                try
                {
                    attributeType = schemaManager.lookupAttributeTypeRegistry( attribute );
                }
                catch ( LdapException ne )
                {
                    if ( LOG.isWarnEnabled() )
                    {
                        LOG.warn( I18n.msg( I18n.MSG_13203_MISSING_ATTRIBUTE_IN_ENTRY, attribute ) );
                    }

                    continue;
                }

                this.attributes.remove( attributeType.getOid() );
            }
        }
    }


    /**
     * <p>
     * Removes the specified binary values from an attribute.
     * </p>
     * <p>
     * If at least one value is removed, this method returns <code>true</code>.
     * </p>
     * <p>
     * If there is no more value after having removed the values, the attribute
     * will be removed too.
     * </p>
     * <p>
     * If the attribute does not exist, nothing is done and the method returns
     * <code>false</code>
     * </p>
     *
     * @param upId The attribute ID
     * @param values the values to be removed
     * @return <code>true</code> if at least a value is removed, <code>false</code>
     * if not all the values have been removed or if the attribute does not exist.
     */
    @Override
    public boolean remove( String upId, byte[]... values ) throws LdapException
    {
        if ( Strings.isEmpty( upId ) )
        {
            if ( LOG.isInfoEnabled() )
            {
                LOG.info( I18n.err( I18n.ERR_13204_NULL_ATTRIBUTE_ID ) );
            }

            return false;
        }

        if ( schemaManager == null )
        {
            String id = getId( upId );

            Attribute attribute = get( id );

            if ( attribute == null )
            {
                // Can't remove values from a not existing attribute !
                return false;
            }

            int nbOldValues = attribute.size();

            // Remove the values
            attribute.remove( values );

            if ( attribute.size() == 0 )
            {
                // No mare values, remove the attribute
                attributes.remove( id );

                return true;
            }

            return nbOldValues != attribute.size();
        }
        else
        {
            try
            {
                AttributeType attributeType = getAttributeType( upId );

                return remove( attributeType, values );
            }
            catch ( LdapException ne )
            {
                LOG.error( I18n.err( I18n.ERR_13205_CANNOT_REMOVE_VAL_MISSING_ATTR, upId ) );
                return false;
            }
            catch ( IllegalArgumentException iae )
            {
                LOG.error( I18n.err( I18n.ERR_13206_CANNOT_REMOVE_VAL_BAD_ATTR, upId ) );
                return false;
            }
        }

    }


    /**
     * <p>
     * Removes the specified String values from an attribute.
     * </p>
     * <p>
     * If at least one value is removed, this method returns <code>true</code>.
     * </p>
     * <p>
     * If there is no more value after having removed the values, the attribute
     * will be removed too.
     * </p>
     * <p>
     * If the attribute does not exist, nothing is done and the method returns
     * <code>false</code>
     * </p>
     *
     * @param upId The attribute ID
     * @param values the attributes to be removed
     * @return <code>true</code> if at least a value is removed, <code>false</code>
     * if not all the values have been removed or if the attribute does not exist.
     */
    @Override
    public boolean remove( String upId, String... values ) throws LdapException
    {
        if ( Strings.isEmpty( upId ) )
        {
            if ( LOG.isInfoEnabled() )
            {
                LOG.info( I18n.err( I18n.ERR_13204_NULL_ATTRIBUTE_ID ) );
            }

            return false;
        }

        if ( schemaManager == null )
        {
            String id = getId( upId );

            Attribute attribute = get( id );

            if ( attribute == null )
            {
                // Can't remove values from a not existing attribute !
                return false;
            }

            int nbOldValues = attribute.size();

            // Remove the values
            attribute.remove( values );

            if ( attribute.size() == 0 )
            {
                // No mare values, remove the attribute
                attributes.remove( id );

                return true;
            }

            return nbOldValues != attribute.size();
        }
        else
        {
            try
            {
                AttributeType attributeType = getAttributeType( upId );

                return remove( attributeType, values );
            }
            catch ( LdapException ne )
            {
                LOG.error( I18n.err( I18n.ERR_13205_CANNOT_REMOVE_VAL_MISSING_ATTR, upId ) );
                return false;
            }
            catch ( IllegalArgumentException iae )
            {
                LOG.error( I18n.err( I18n.ERR_13206_CANNOT_REMOVE_VAL_BAD_ATTR, upId ) );
                return false;
            }
        }
    }


    /**
     * <p>
     * Removes the specified values from an attribute.
     * </p>
     * <p>
     * If at least one value is removed, this method returns <code>true</code>.
     * </p>
     * <p>
     * If there is no more value after having removed the values, the attribute
     * will be removed too.
     * </p>
     * <p>
     * If the attribute does not exist, nothing is done and the method returns
     * <code>false</code>
     * </p>
     *
     * @param upId The attribute ID
     * @param values the attributes to be removed
     * @return <code>true</code> if at least a value is removed, <code>false</code>
     * if not all the values have been removed or if the attribute does not exist.
     */
    @Override
    public boolean remove( String upId, Value... values ) throws LdapException
    {
        if ( Strings.isEmpty( upId ) )
        {
            if ( LOG.isInfoEnabled() )
            {
                LOG.info( I18n.err( I18n.ERR_13204_NULL_ATTRIBUTE_ID ) );
            }

            return false;
        }

        if ( schemaManager == null )
        {
            String id = getId( upId );

            Attribute attribute = get( id );

            if ( attribute == null )
            {
                // Can't remove values from a not existing attribute !
                return false;
            }

            int nbOldValues = attribute.size();

            // Remove the values
            attribute.remove( values );

            if ( attribute.size() == 0 )
            {
                // No mare values, remove the attribute
                attributes.remove( id );

                return true;
            }

            return nbOldValues != attribute.size();
        }
        else
        {
            try
            {
                AttributeType attributeType = getAttributeType( upId );

                return remove( attributeType, values );
            }
            catch ( LdapException ne )
            {
                LOG.error( I18n.err( I18n.ERR_13205_CANNOT_REMOVE_VAL_MISSING_ATTR, upId ) );
                return false;
            }
            catch ( IllegalArgumentException iae )
            {
                LOG.error( I18n.err( I18n.ERR_13206_CANNOT_REMOVE_VAL_BAD_ATTR, upId ) );
                return false;
            }
        }
    }


    /**
     * Get this entry's Dn.
     *
     * @return The entry's Dn
     */
    @Override
    public Dn getDn()
    {
        return dn;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void setDn( Dn dn )
    {
        this.dn = dn;

        // Rehash the object
        rehash();
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void setDn( String dn ) throws LdapInvalidDnException
    {
        setDn( new Dn( dn ) );
    }


    /**
     * Remove all the attributes for this entry. The Dn is not reset
     */
    @Override
    public void clear()
    {
        attributes.clear();
    }


    /**
     * Returns an enumeration containing the zero or more attributes in the
     * collection. The behavior of the enumeration is not specified if the
     * attribute collection is changed.
     *
     * @return an enumeration of all contained attributes
     */
    @Override
    public Iterator<Attribute> iterator()
    {
        return attributes.values().iterator();
    }


    /**
     * Returns the number of attributes.
     *
     * @return the number of attributes
     */
    @Override
    public int size()
    {
        return attributes.size();
    }


    /**
     * This is the place where we serialize entries, and all theirs
     * elements.
     * <br>
     * The structure used to store the entry is the following :
     * <ul>
     *   <li>
     *     <b>[Dn]</b> : If it's null, stores an empty Dn
     *   </li>
     *   <li>
     *     <b>[attributes number]</b> : the number of attributes.
     *   </li>
     *   <li>
     *     <b>[attribute]*</b> : each attribute, if we have some
     *   </li>
     * </ul>
     *
     * {@inheritDoc}
     */
    @Override
    public void writeExternal( ObjectOutput out ) throws IOException
    {
        // First, the Dn
        if ( dn == null )
        {
            // Write an empty Dn
            Dn.EMPTY_DN.writeExternal( out );
        }
        else
        {
            // Write the Dn
            dn.writeExternal( out );
        }

        // Then the attributes.
        // Store the attributes' nulber first
        out.writeInt( attributes.size() );

        // Iterate through the keys.
        for ( Attribute attribute : attributes.values() )
        {
            // Store the attribute
            attribute.writeExternal( out );
        }

        out.flush();
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void readExternal( ObjectInput in ) throws IOException, ClassNotFoundException
    {
        // Read the Dn
        dn = new Dn( schemaManager );
        dn.readExternal( in );

        // Read the number of attributes
        int nbAttributes = in.readInt();

        // Read the attributes
        for ( int i = 0; i < nbAttributes; i++ )
        {
            // Read each attribute
            Attribute attribute = new DefaultAttribute();
            attribute.readExternal( in );

            if ( schemaManager != null )
            {
                try
                {
                    AttributeType attributeType = schemaManager.lookupAttributeTypeRegistry( attribute.getId() );
                    attribute.apply( attributeType );

                    attributes.put( attributeType.getOid(), attribute );
                }
                catch ( LdapException le )
                {
                    String message = le.getLocalizedMessage();
                    LOG.error( message );
                    throw new IOException( message, le );
                }
            }
            else
            {
                attributes.put( attribute.getId(), attribute );
            }
        }
    }


    /**
     * Get the hash code of this ClientEntry. The Attributes will be sorted
     * before the comparison can be done.
     *
     * @see java.lang.Object#hashCode()
     * @return the instance's hash code
     */
    @Override
    public int hashCode()
    {
        if ( h == 0 )
        {
            rehash();
        }

        return h;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasObjectClass( String... objectClasses )
    {
        if ( ( objectClasses == null ) || ( objectClasses.length == 0 ) || ( objectClasses[0] == null ) )
        {
            return false;
        }

        for ( String objectClass : objectClasses )
        {
            if ( schemaManager != null )
            {
                if ( !contains( objectClassAttributeType, objectClass ) )
                {
                    return false;
                }
            }
            else
            {
                if ( !contains( "objectclass", objectClass ) )
                {
                    return false;
                }
            }
        }

        return true;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasObjectClass( Attribute... objectClasses )
    {
        if ( ( objectClasses == null ) || ( objectClasses.length == 0 ) || ( objectClasses[0] == null ) )
        {
            return false;
        }

        for ( Attribute objectClass : objectClasses )
        {
            // We have to check that we are checking the ObjectClass attributeType
            if ( !objectClass.getAttributeType().equals( objectClassAttributeType ) )
            {
                return false;
            }

            Attribute attribute = attributes.get( objectClassAttributeType.getOid() );

            if ( attribute == null )
            {
                // The entry does not have an ObjectClass attribute
                return false;
            }

            for ( Value value : objectClass )
            {
                // Loop on all the values, and check if they are present
                if ( !attribute.contains( value.getString() ) )
                {
                    return false;
                }
            }
        }

        return true;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isSchemaAware()
    {
        return schemaManager != null;
    }


    /**
     * @see Object#equals(Object)
     */
    @Override
    public boolean equals( Object o )
    {
        // Short circuit
        if ( this == o )
        {
            return true;
        }

        if ( !( o instanceof Entry ) )
        {
            return false;
        }

        Entry other = ( Entry ) o;

        // Both Dn must be equal
        if ( dn == null )
        {
            if ( other.getDn() != null )
            {
                return false;
            }
        }
        else
        {
            if ( !dn.equals( other.getDn() ) )
            {
                return false;
            }
        }

        // They must have the same number of attributes
        if ( size() != other.size() )
        {
            return false;
        }

        // Each attribute must be equal
        for ( Attribute attribute : other )
        {
            if ( !attribute.equals( this.get( attribute.getId() ) ) )
            {
                return false;
            }
        }

        return true;
    }


    /**
     * @see Object#toString()
     */
    @Override
    public String toString()
    {
        return toString( "" );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public String toString( String tabs )
    {
        StringBuilder sb = new StringBuilder();

        sb.append( tabs ).append( "Entry\n" );
        sb.append( tabs ).append( "    dn" );

        if ( dn.isSchemaAware() )
        {
            sb.append( "[n]" );
        }

        sb.append( ": " );
        sb.append( dn.getName() );
        sb.append( '\n' );

        // First dump the ObjectClass attribute
        if ( schemaManager != null )
        {
            // First dump the ObjectClass attribute
            if ( containsAttribute( objectClassAttributeType.getOid() ) )
            {
                Attribute objectClass = get( objectClassAttributeType );

                sb.append( objectClass.toString( tabs + "    " ) );
            }
        }
        else
        {
            Attribute objectClass = get( "objectclass" );

            if ( objectClass != null )
            {
                sb.append( objectClass.toString( tabs + "    " ) );
            }
        }

        sb.append( '\n' );

        if ( attributes.size() != 0 )
        {
            for ( Attribute attribute : attributes.values() )
            {
                String id = attribute.getId();

                if ( schemaManager != null )
                {
                    AttributeType attributeType = schemaManager.getAttributeType( id );

                    if ( attributeType == null )
                    {
                        sb.append( tabs ).append( "id: " ).append( id );
                    }
                    else if ( !attributeType.equals( objectClassAttributeType ) )
                    {
                        sb.append( attribute.toString( tabs + "    " ) );
                        sb.append( '\n' );
                    }
                }
                else
                {
                    if ( !id.equalsIgnoreCase( SchemaConstants.OBJECT_CLASS_AT )
                        && !id.equals( SchemaConstants.OBJECT_CLASS_AT_OID ) )
                    {
                        sb.append( attribute.toString( tabs + "    " ) );
                        sb.append( '\n' );
                    }
                }
            }
        }

        return sb.toString();
    }
}
