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


import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.directory.api.i18n.I18n;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
import org.apache.directory.api.ldap.model.entry.DefaultEntry;
import org.apache.directory.api.ldap.model.entry.DefaultModification;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Modification;
import org.apache.directory.api.ldap.model.entry.ModificationOperation;
import org.apache.directory.api.ldap.model.entry.Value;
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.message.Control;
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.name.Rdn;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.util.Strings;


/**
 * A entry to be populated by an ldif parser.
 * 
 * We will have different kind of entries : 
 * <ul>
 * <li>added entries</li>
 * <li>deleted entries</li>
 * <li>modified entries</li>
 * <li>Rdn modified entries</li>
 * <li>Dn modified entries</li>
 * </ul>
 * 
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class LdifEntry implements Cloneable, Externalizable, Iterable<Attribute>
{
    /** Used in toArray() */
    public static final Modification[] EMPTY_MODS = new Modification[0];

    /** the change type */
    private ChangeType changeType;

    /** the modification item list */
    private List<Modification> modificationList;

    /** The map containing all the modifications */
    private Map<String, Modification> modifications;

    /** The new superior */
    private String newSuperior;

    /** The new rdn */
    private String newRdn;

    /** The delete old rdn flag */
    private boolean deleteOldRdn;

    /** the entry */
    private Entry entry;

    /** the DN */
    private Dn entryDn;

    /** The controls */
    private Map<String, LdifControl> controls;

    /** The lengthBeforeParsing of the entry at the time of parsing. This includes
     *  the lengthBeforeParsing of the comments present in entry at the time of parsing
     *  so this lengthBeforeParsing may not always match with the lengthBeforeParsing of the entry
     *  data present in memory.
     */
    private int lengthBeforeParsing = 0;

    /** the position of the entry in the file or given input string*/
    private long offset = 0;


    /**
     * Creates a new LdifEntry object.
     */
    public LdifEntry()
    {
        // Default LDIF content
        changeType = ChangeType.None;
        modificationList = new LinkedList<>();
        modifications = new HashMap<>();
        entry = new DefaultEntry( ( Dn ) null );
        entryDn = null;
        controls = null;
    }


    /**
     * Creates a new schema aware LdifEntry object.
     * 
     * @param schemaManager The SchemaManager
     */
    public LdifEntry( SchemaManager schemaManager )
    {
        // Default LDIF content
        changeType = ChangeType.None;
        modificationList = new LinkedList<>();
        modifications = new HashMap<>();
        entry = new DefaultEntry( schemaManager, ( Dn ) null );
        entryDn = null;
        controls = null;
    }


    /**
     * Creates a new LdifEntry object, storing an Entry
     * 
     * @param entry The entry to encapsulate
     */
    public LdifEntry( Entry entry )
    {
        // Default LDIF content
        changeType = ChangeType.None;
        modificationList = new LinkedList<>();
        modifications = new HashMap<>();
        this.entry = entry;
        entryDn = entry.getDn();
        controls = null;
    }


    /**
     * Creates a LdifEntry using a list of strings representing the Ldif element
     * 
     * @param dn The LdifEntry DN
     * @param avas The Ldif to convert to an LdifEntry
     * @throws LdapInvalidAttributeValueException If either the AttributeType or the associated value
     * is incorrect
     * @throws LdapLdifException If we get any other exception
     */
    public LdifEntry( Dn dn, Object... avas ) throws LdapInvalidAttributeValueException, LdapLdifException
    {
        // First, convert the arguments to a full LDIF
        StringBuilder sb = new StringBuilder();
        int pos = 0;
        boolean valueExpected = false;
        String dnStr = null;

        if ( dn == null )
        {
            dnStr = "";
        }
        else
        {
            dnStr = dn.getName();
        }

        if ( LdifUtils.isLDIFSafe( dnStr ) )
        {
            sb.append( "dn: " ).append( dnStr ).append( '\n' );
        }
        else
        {
            sb.append( "dn:: " ).append( Base64.getEncoder().encode( Strings.getBytesUtf8( dnStr ) ) ).append( '\n' );
        }

        for ( Object ava : avas )
        {
            if ( !valueExpected )
            {
                if ( !( ava 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 ) ava;
                sb.append( attribute );

                if ( attribute.indexOf( ':' ) != -1 )
                {
                    sb.append( '\n' );
                }
                else
                {
                    valueExpected = true;
                }
            }
            else
            {
                if ( ava instanceof String )
                {
                    sb.append( ": " ).append( ( String ) ava ).append( '\n' );
                }
                else if ( ava instanceof byte[] )
                {
                    sb.append( ":: " );
                    sb.append( new String( Base64.getEncoder().encode( ( byte[] ) ava ), 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 ) );
        }

        // Now, parse the Ldif and convert it to a LdifEntry
        List<LdifEntry> ldifEntries = null;
        
        try ( LdifReader reader = new LdifReader() )
        {
            ldifEntries = reader.parseLdif( sb.toString() );
        }
        catch ( IOException e )
        {
            e.printStackTrace();
        }

        if ( ( ldifEntries != null ) && ( ldifEntries.size() == 1 ) )
        {
            LdifEntry ldifEntry = ldifEntries.get( 0 );

            changeType = ldifEntry.getChangeType();
            controls = ldifEntry.getControls();
            entryDn = ldifEntry.getDn();

            switch ( ldifEntry.getChangeType() )
            {
                case Add:
                    // Fallback
                case None:
                    entry = ldifEntry.getEntry();
                    break;

                case Delete:
                    break;

                case ModDn:
                case ModRdn:
                    newRdn = ldifEntry.getNewRdn();
                    newSuperior = ldifEntry.getNewSuperior();
                    deleteOldRdn = ldifEntry.isDeleteOldRdn();
                    break;

                case Modify:
                    modificationList = ldifEntry.getModifications();
                    modifications = new HashMap<>();

                    for ( Modification modification : modificationList )
                    {
                        modifications.put( modification.getAttribute().getId(), modification );
                    }

                    break;

                default:
                    throw new IllegalArgumentException( I18n.err( I18n.ERR_13431_UNEXPECTED_CHANGETYPE, changeType ) );
            }
        }
    }


    /**
     * Creates a LdifEntry using a list of strings representing the Ldif element
     * 
     * @param dn The LdifEntry DN
     * @param strings The Ldif attributes and values to convert to an LdifEntry
     * @throws LdapInvalidDnException If the Dn is invalid
     * @throws LdapInvalidAttributeValueException If either the AttributeType or the associated value
     * is incorrect
     * @throws LdapLdifException If we get any other exception
     */
    public LdifEntry( String dn, Object... strings )
        throws LdapInvalidAttributeValueException, LdapLdifException, LdapInvalidDnException
    {
        this( new Dn( dn ), strings );
    }


    /**
     * Set the Distinguished Name
     * 
     * @param dn The Distinguished Name
     */
    public void setDn( Dn dn )
    {
        entryDn = dn;
        entry.setDn( dn );
    }


    /**
     * Set the Distinguished Name
     * 
     * @param dn The Distinguished Name
     * @throws LdapInvalidDnException If the Dn is invalid
     */
    public void setDn( String dn ) throws LdapInvalidDnException
    {
        entryDn = new Dn( dn );
        entry.setDn( entryDn );
    }


    /**
     * Set the modification type
     * 
     * @param changeType The change type
     * 
     */
    public void setChangeType( ChangeType changeType )
    {
        this.changeType = changeType;
    }


    /**
     * Set the change type
     * 
     * @param changeType The change type
     */
    public void setChangeType( String changeType )
    {
        if ( "add".equals( changeType ) )
        {
            this.changeType = ChangeType.Add;
        }
        else if ( "modify".equals( changeType ) )
        {
            this.changeType = ChangeType.Modify;
        }
        else if ( "moddn".equals( changeType ) )
        {
            this.changeType = ChangeType.ModDn;
        }
        else if ( "modrdn".equals( changeType ) )
        {
            this.changeType = ChangeType.ModRdn;
        }
        else if ( "delete".equals( changeType ) )
        {
            this.changeType = ChangeType.Delete;
        }
    }


    /**
     * Add a modification item (used by modify operations)
     * 
     * @param modification The modification to be added
     */
    public void addModification( Modification modification )
    {
        if ( changeType == ChangeType.Modify )
        {
            modificationList.add( modification );
            modifications.put( modification.getAttribute().getId(), modification );
        }
    }


    /**
     * Add a modification item (used by modify operations)
     * 
     * @param modOp The operation. One of : 
     * <ul>
     * <li>ModificationOperation.ADD_ATTRIBUTE</li>
     * <li>ModificationOperation.REMOVE_ATTRIBUTE</li>
     * <li>ModificationOperation.REPLACE_ATTRIBUTE</li>
     * <li>ModificationOperation.INCREMENT_ATTRIBUTE</li>
     * </ul>
     * 
     * @param attr The attribute to be added
     */
    public void addModification( ModificationOperation modOp, Attribute attr )
    {
        if ( changeType == ChangeType.Modify )
        {
            Modification item = new DefaultModification( modOp, attr );
            modificationList.add( item );
            modifications.put( attr.getId(), item );
        }
    }


    /**
     * Add a modification with no value
     * 
     * @param modOp The modification operation value. One of : 
     * <ul>
     * <li>ModificationOperation.ADD_ATTRIBUTE</li>
     * <li>ModificationOperation.REMOVE_ATTRIBUTE</li>
     * <li>ModificationOperation.REPLACE_ATTRIBUTE</li>
     * <li>ModificationOperation.INCREMENT_ATTRIBUTE</li>
     * </ul>
     * 
     * @param id The attribute's ID
     */
    public void addModification( ModificationOperation modOp, String id )
    {
        if ( changeType == ChangeType.Modify )
        {
            Attribute attr = new DefaultAttribute( id );

            Modification item = new DefaultModification( modOp, attr );
            modificationList.add( item );
            modifications.put( id, item );
        }
    }


    /**
     * Add a modification
     * 
     * @param modOp The modification operation value. One of : 
     * <ul>
     * <li>ModificationOperation.ADD_ATTRIBUTE</li>
     * <li>ModificationOperation.REMOVE_ATTRIBUTE</li>
     * <li>ModificationOperation.REPLACE_ATTRIBUTE</li>
     * <li>ModificationOperation.INCREMENT_ATTRIBUTE</li>
     * </ul>
     * 
     * @param id The attribute's ID
     * @param value The attribute's value
     */
    public void addModification( ModificationOperation modOp, String id, Object value )
    {
        if ( changeType == ChangeType.Modify )
        {
            Attribute attr;

            if ( value == null )
            {
                value = new Value( ( String ) null );
                attr = new DefaultAttribute( id, ( Value ) value );
            }
            else
            {
                attr = ( Attribute ) value;
            }

            Modification item = new DefaultModification( modOp, attr );
            modificationList.add( item );
            modifications.put( id, item );
        }
    }


    /**
     * Add an attribute to the entry
     * 
     * @param attr The attribute to be added
     * @throws org.apache.directory.api.ldap.model.exception.LdapException if something went wrong
     */
    public void addAttribute( Attribute attr ) throws LdapException
    {
        entry.put( attr );
    }


    /**
     * Add an attribute to the entry
     * 
     * @param id The attribute ID
     * 
     * @param values The attribute values
     * @throws LdapException if something went wrong
     */
    public void addAttribute( String id, Object... values ) throws LdapException
    {
        Attribute attribute = entry.get( id );
        Boolean isHR = null;

        if ( attribute != null )
        {
            isHR = attribute.isHumanReadable();
        }

        if ( values != null )
        {
            for ( Object value : values )
            {
                if ( value instanceof String )
                {
                    if ( isHR != null )
                    {
                        if ( isHR )
                        {
                            entry.add( id, ( String ) value );
                        }
                        else
                        {
                            entry.add( id, Strings.getBytesUtf8( ( String ) value ) );
                        }
                    }
                    else
                    {
                        entry.add( id, ( String ) value );
                    }
                }
                else
                {
                    if ( isHR != null )
                    {
                        if ( isHR )
                        {
                            entry.add( id, Strings.utf8ToString( ( byte[] ) value ) );
                        }
                        else
                        {
                            entry.add( id, ( byte[] ) value );
                        }
                    }
                    else
                    {
                        entry.add( id, ( byte[] ) value );
                    }
                }
            }
        }
        else
        {
            entry.add( id, ( Value ) null );
        }
    }


    /**
     * Remove a list of Attributes from the LdifEntry
     *
     * @param ids The Attributes to remove
     */
    public void removeAttribute( String... ids )
    {
        if ( entry.containsAttribute( ids ) )
        {
            entry.removeAttributes( ids );
        }
    }


    /**
     * Add an attribute value to an existing attribute
     * 
     * @param id The attribute ID
     * 
     * @param value The attribute value
     * @throws org.apache.directory.api.ldap.model.exception.LdapException if something went wrong
     */
    public void putAttribute( String id, Object value ) throws LdapException
    {
        if ( value instanceof String )
        {
            entry.add( id, ( String ) value );
        }
        else
        {
            entry.add( id, ( byte[] ) value );
        }
    }


    /**
     * Get the change type
     * 
     * @return The change type. One of : 
     * <ul>
     * <li>ADD</li>
     * <li>MODIFY</li>
     * <li>MODDN</li>
     * <li>MODRDN</li>
     * <li>DELETE</li>
     * <li>NONE</li>
     * </ul>
     */
    public ChangeType getChangeType()
    {
        return changeType;
    }


    /**
     * @return The list of modification items
     */
    public List<Modification> getModifications()
    {
        return modificationList;
    }


    /**
     * Gets the modification items as an array.
     *
     * @return modification items as an array.
     */
    public Modification[] getModificationArray()
    {
        return modificationList.toArray( EMPTY_MODS );
    }


    /**
     * @return The entry Distinguished name
     */
    public Dn getDn()
    {
        return entryDn;
    }


    /**
     * @return The number of entry modifications
     */
    public int size()
    {
        return modificationList.size();
    }


    /**
     * Returns a attribute given it's id
     * 
     * @param attributeId The attribute Id
     * @return The attribute if it exists
     */
    public Attribute get( String attributeId )
    {
        if ( "dn".equalsIgnoreCase( attributeId ) )
        {
            return new DefaultAttribute( "dn", entry.getDn().getName() );
        }

        return entry.get( attributeId );
    }


    /**
     * Get the entry's entry
     * 
     * @return the stored Entry
     */
    public Entry getEntry()
    {
        if ( isEntry() )
        {
            return entry;
        }
        else
        {
            return null;
        }
    }


    /**
     * @return True, if the old Rdn should be deleted.
     */
    public boolean isDeleteOldRdn()
    {
        return deleteOldRdn;
    }


    /**
     * Set the deleteOldRdn flag
     * 
     * @param deleteOldRdn True if the old Rdn should be deleted
     */
    public void setDeleteOldRdn( boolean deleteOldRdn )
    {
        this.deleteOldRdn = deleteOldRdn;
    }


    /**
     * @return The new Rdn
     */
    public String getNewRdn()
    {
        return newRdn;
    }


    /**
     * Set the new Rdn
     * 
     * @param newRdn The new Rdn
     */
    public void setNewRdn( String newRdn )
    {
        this.newRdn = newRdn;
    }


    /**
     * @return The new superior
     */
    public String getNewSuperior()
    {
        return newSuperior;
    }


    /**
     * Set the new superior
     * 
     * @param newSuperior The new Superior
     */
    public void setNewSuperior( String newSuperior )
    {
        this.newSuperior = newSuperior;
    }


    /**
     * @return True if this is a content ldif
     */
    public boolean isLdifContent()
    {
        return changeType == ChangeType.None;
    }


    /**
     * @return True if there is this is a change ldif
     */
    public boolean isLdifChange()
    {
        return changeType != ChangeType.None;
    }


    /**
     * @return True if the entry is an ADD entry
     */
    public boolean isChangeAdd()
    {
        return changeType == ChangeType.Add;
    }


    /**
     * @return True if the entry is a DELETE entry
     */
    public boolean isChangeDelete()
    {
        return changeType == ChangeType.Delete;
    }


    /**
     * @return True if the entry is a MODDN entry
     */
    public boolean isChangeModDn()
    {
        return changeType == ChangeType.ModDn;
    }


    /**
     * @return True if the entry is a MODRDN entry
     */
    public boolean isChangeModRdn()
    {
        return changeType == ChangeType.ModRdn;
    }


    /**
     * @return True if the entry is a MODIFY entry
     */
    public boolean isChangeModify()
    {
        return changeType == ChangeType.Modify;
    }


    /**
     * Tells if the current entry is a added one
     *
     * @return <code>true</code> if the entry is added
     */
    public boolean isEntry()
    {
        return ( changeType == ChangeType.None ) || ( changeType == ChangeType.Add );
    }


    /**
     * @return true if the entry has some controls
     */
    public boolean hasControls()
    {
        return controls != null;
    }


    /**
     * @return The set of controls for this entry
     */
    public Map<String, LdifControl> getControls()
    {
        return controls;
    }


    /**
     * @param oid The control's OID
     * @return The associated control, if any
     */
    public LdifControl getControl( String oid )
    {
        if ( controls != null )
        {
            return controls.get( oid );
        }

        return null;
    }


    /**
     * Add a control to the entry
     * 
     * @param controls The added controls
     */
    public void addControl( Control... controls )
    {
        if ( controls == null )
        {
            throw new IllegalArgumentException( I18n.err( I18n.ERR_13432_NULL_ADDED_CONTROL ) );
        }

        for ( Control control : controls )
        {
            if ( changeType == ChangeType.None )
            {
                changeType = ChangeType.Add;
            }

            if ( this.controls == null )
            {
                this.controls = new ConcurrentHashMap<>();
            }

            if ( control instanceof LdifControl )
            {
                this.controls.put( control.getOid(), ( LdifControl ) control );
            }
            else
            {
                LdifControl ldifControl = new LdifControl( control.getOid() );
                ldifControl.setCritical( control.isCritical() );
                this.controls.put( control.getOid(), new LdifControl( control.getOid() ) );
            }
        }
    }


    /**
     * Clone method
     * @return a clone of the current instance
     * @exception CloneNotSupportedException If there is some problem while cloning the instance
     */
    @Override
    public LdifEntry clone() throws CloneNotSupportedException
    {
        LdifEntry clone = ( LdifEntry ) super.clone();

        if ( modificationList != null )
        {
            for ( Modification modif : modificationList )
            {
                Modification modifClone = new DefaultModification( modif.getOperation(),
                    modif.getAttribute().clone() );
                clone.modificationList.add( modifClone );
            }
        }

        if ( modifications != null )
        {
            for ( Map.Entry<String, Modification> modificationEntry : modifications.entrySet() )
            {
                Modification modif = modificationEntry.getValue();
                Modification modifClone = new DefaultModification( modif.getOperation(),
                    modif.getAttribute().clone() );
                clone.modifications.put( modificationEntry.getKey(), modifClone );
            }

        }

        if ( entry != null )
        {
            clone.entry = entry.clone();
        }

        return clone;
    }


    /** 
     *  Returns the lengthBeforeParsing of the entry at the time of parsing. This includes
     *  the lengthBeforeParsing of the comments present in entry at the time of parsing
     *  so this lengthBeforeParsing may not always match with the lengthBeforeParsing of the entry
     *  data present in memory.
     *  
     *  @return The entry length, comments included 
     */
    public int getLengthBeforeParsing()
    {
        return lengthBeforeParsing;
    }


    /**
     * @param length the lengthBeforeParsing to set
     */
    /*No qualifier*/ void setLengthBeforeParsing( int length )
    {
        this.lengthBeforeParsing = length;
    }


    /**
     * @return the offset
     */
    public long getOffset()
    {
        return offset;
    }


    /**
     * @param offset the offset to set
     */
    /*No qualifier*/ void setOffset( long offset )
    {
        this.offset = offset;
    }


    /**
     * 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 entry.iterator();
    }


    /**
     * @return a String representing the Entry, as a LDIF 
     */
    @Override
    public String toString()
    {
        try
        {
            return LdifUtils.convertToLdif( this );
        }
        catch ( LdapException ne )
        {
            return "";
        }
    }


    /**
     * @see Object#hashCode()
     * 
     * @return the instance's hash code
     */
    @Override
    public int hashCode()
    {
        int result = 37;

        if ( entry != null && entry.getDn() != null )
        {
            result = result * 17 + entry.getDn().hashCode();
        }

        if ( changeType != null )
        {
            result = result * 17 + changeType.hashCode();

            // Check each different cases
            switch ( changeType )
            {
                case None:
                    // Fall through
                case Add:
                    // Checks the attributes
                    if ( entry != null )
                    {
                        result = result * 17 + entry.hashCode();
                    }

                    break;

                case Delete:
                    // Nothing to compute
                    break;

                case Modify:
                    if ( modificationList != null )
                    {
                        result = result * 17 + modificationList.hashCode();

                        for ( Modification modification : modificationList )
                        {
                            result = result * 17 + modification.hashCode();
                        }
                    }

                    break;

                case ModDn:
                case ModRdn:
                    result = result * 17;

                    if ( deleteOldRdn )
                    {
                        result++;
                    }
                    else
                    {
                        result--;
                    }

                    if ( newRdn != null )
                    {
                        result = result * 17 + newRdn.hashCode();
                    }

                    if ( newSuperior != null )
                    {
                        result = result * 17 + newSuperior.hashCode();
                    }

                    break;

                default:
                    // do nothing
                    break;
            }
        }

        if ( controls != null )
        {
            for ( String control : controls.keySet() )
            {
                result = result * 17 + control.hashCode();
            }
        }

        return result;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean equals( Object o )
    {
        // Basic equals checks
        if ( this == o )
        {
            return true;
        }

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

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

        LdifEntry otherEntry = ( LdifEntry ) o;

        // Check the Dn
        Dn thisDn = entryDn;
        Dn dnEntry = otherEntry.getDn();

        if ( !thisDn.equals( dnEntry ) )
        {
            return false;
        }

        // Check the changeType
        if ( changeType != otherEntry.changeType )
        {
            return false;
        }

        // Check each different cases
        switch ( changeType )
        {
            case None:
                // Fall through
            case Add:
                // Checks the attributes
                if ( entry.size() != otherEntry.entry.size() )
                {
                    return false;
                }

                if ( !entry.equals( otherEntry.entry ) )
                {
                    return false;
                }

                break;

            case Delete:
                // Nothing to do, if the DNs are equals
                break;

            case Modify:
                // Check the modificationItems list

                // First, deal with special cases
                if ( modificationList == null )
                {
                    if ( otherEntry.modificationList != null )
                    {
                        return false;
                    }
                    else
                    {
                        break;
                    }
                }

                if ( otherEntry.modificationList == null )
                {
                    return false;
                }

                if ( modificationList.size() != otherEntry.modificationList.size() )
                {
                    return false;
                }

                // Now, compares the contents
                int i = 0;

                for ( Modification modification : modificationList )
                {
                    if ( !modification.equals( otherEntry.modificationList.get( i ) ) )
                    {
                        return false;
                    }

                    i++;
                }

                break;

            case ModDn:
            case ModRdn:
                // Check the deleteOldRdn flag
                if ( deleteOldRdn != otherEntry.deleteOldRdn )
                {
                    return false;
                }

                // Check the newRdn value
                try
                {
                    Rdn thisNewRdn = new Rdn( newRdn );
                    Rdn entryNewRdn = new Rdn( otherEntry.newRdn );

                    if ( !thisNewRdn.equals( entryNewRdn ) )
                    {
                        return false;
                    }
                }
                catch ( LdapInvalidDnException ine )
                {
                    return false;
                }

                // Check the newSuperior value
                try
                {
                    Dn thisNewSuperior = new Dn( newSuperior );
                    Dn entryNewSuperior = new Dn( otherEntry.newSuperior );

                    if ( !thisNewSuperior.equals( entryNewSuperior ) )
                    {
                        return false;
                    }
                }
                catch ( LdapInvalidDnException ine )
                {
                    return false;
                }

                break;

            default:
                // do nothing
                break;
        }

        if ( controls != null )
        {
            Map<String, LdifControl> otherControls = otherEntry.controls;

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

            if ( controls.size() != otherControls.size() )
            {
                return false;
            }

            for ( Map.Entry<String, LdifControl> controlEntry : controls.entrySet() )
            {
                String controlOid = controlEntry.getKey();

                if ( !otherControls.containsKey( controlOid ) )
                {
                    return false;
                }

                Control thisControl = controlEntry.getValue();
                Control otherControl = otherControls.get( controlOid );

                if ( thisControl == null )
                {
                    if ( otherControl != null )
                    {
                        return false;
                    }
                }
                else
                {
                    if ( !thisControl.equals( otherControl ) )
                    {
                        return false;
                    }
                }
            }

            return true;
        }
        else
        {
            return otherEntry.controls == null;
        }
    }


    /**
     * @see Externalizable#readExternal(ObjectInput)
     * 
     * @param in The stream from which the LdifEntry is read
     * @throws IOException If the stream can't be read
     * @throws ClassNotFoundException If the LdifEntry can't be created 
     */
    @Override
    public void readExternal( ObjectInput in ) throws IOException, ClassNotFoundException
    {
        // Read the changeType
        int type = in.readInt();
        changeType = ChangeType.getChangeType( type );

        // Read the modification
        switch ( changeType )
        {
            case Add:
            case None:
                // Read the entry
                entry.readExternal( in );
                entryDn = entry.getDn();

                break;

            case Delete:
                // Read the Dn
                entryDn = new Dn();
                entryDn.readExternal( in );

                break;

            case ModDn:
                // Fallback
            case ModRdn:
                // Read the Dn
                entryDn = new Dn();
                entryDn.readExternal( in );

                deleteOldRdn = in.readBoolean();

                if ( in.readBoolean() )
                {
                    newRdn = in.readUTF();
                }

                if ( in.readBoolean() )
                {
                    newSuperior = in.readUTF();
                }

                break;

            case Modify:
                // Read the Dn
                entryDn = new Dn();
                entryDn.readExternal( in );

                // Read the modifications
                int nbModifs = in.readInt();

                for ( int i = 0; i < nbModifs; i++ )
                {
                    Modification modification = new DefaultModification();
                    modification.readExternal( in );

                    addModification( modification );
                }

                break;

            default:
                throw new IllegalArgumentException( I18n.err( I18n.ERR_13431_UNEXPECTED_CHANGETYPE, changeType ) );
        }

        int nbControls = in.readInt();

        // We have at least a control
        if ( nbControls > 0 )
        {
            controls = new ConcurrentHashMap<>( nbControls );

            for ( int i = 0; i < nbControls; i++ )
            {
                LdifControl control = new LdifControl();

                control.readExternal( in );

                controls.put( control.getOid(), control );
            }
        }
    }


    /**
     * @see Externalizable#readExternal(ObjectInput)
     * @param out The stream in which the ChangeLogEvent will be serialized.
     * @throws IOException If the serialization fail
     */
    @Override
    public void writeExternal( ObjectOutput out ) throws IOException
    {
        // Write the changeType
        out.writeInt( changeType.getChangeType() );

        // Write the data
        switch ( changeType )
        {
            case Add:
            case None:
                entry.writeExternal( out );
                break;

            // Fallback
            case Delete:
                // we write the Dn
                entryDn.writeExternal( out );
                break;

            case ModDn:
                // Fallback
            case ModRdn:
                // Write the Dn
                entryDn.writeExternal( out );

                out.writeBoolean( deleteOldRdn );

                if ( newRdn == null )
                {
                    out.writeBoolean( false );
                }
                else
                {
                    out.writeBoolean( true );
                    out.writeUTF( newRdn );
                }

                if ( newSuperior != null )
                {
                    out.writeBoolean( true );
                    out.writeUTF( newSuperior );
                }
                else
                {
                    out.writeBoolean( false );
                }
                break;

            case Modify:
                // Write the Dn
                entryDn.writeExternal( out );

                // Write the modifications
                out.writeInt( modificationList.size() );

                for ( Modification modification : modificationList )
                {
                    modification.writeExternal( out );
                }

                break;

            default:
                throw new IllegalArgumentException( I18n.err( I18n.ERR_13431_UNEXPECTED_CHANGETYPE, changeType ) );
        }

        // The controls
        if ( controls != null )
        {
            // Write the control
            out.writeInt( controls.size() );

            for ( LdifControl control : controls.values() )
            {
                control.writeExternal( out );
            }
        }
        else
        {
            // No control, write -1
            out.writeInt( -1 );
        }

        // and flush the result
        out.flush();
    }
}
