/*
 *   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.ldap.client.api;


import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.io.Writer;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.directory.api.ldap.model.constants.SchemaConstants;
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.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.ldif.ChangeType;
import org.apache.directory.api.ldap.model.ldif.LdifEntry;
import org.apache.directory.api.ldap.model.ldif.LdifReader;
import org.apache.directory.api.ldap.model.ldif.LdifUtils;
import org.apache.directory.api.ldap.model.ldif.anonymizer.Anonymizer;
import org.apache.directory.api.ldap.model.ldif.anonymizer.BinaryAnonymizer;
import org.apache.directory.api.ldap.model.ldif.anonymizer.CaseSensitiveStringAnonymizer;
import org.apache.directory.api.ldap.model.ldif.anonymizer.IntegerAnonymizer;
import org.apache.directory.api.ldap.model.ldif.anonymizer.StringAnonymizer;
import org.apache.directory.api.ldap.model.ldif.anonymizer.TelephoneNumberAnonymizer;
import org.apache.directory.api.ldap.model.name.Ava;
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.AttributeType;
import org.apache.directory.api.ldap.model.schema.LdapSyntax;
import org.apache.directory.api.ldap.model.schema.SchemaManager;
import org.apache.directory.api.ldap.model.schema.syntaxCheckers.DnSyntaxChecker;
import org.apache.directory.api.ldap.model.schema.syntaxCheckers.NameAndOptionalUIDSyntaxChecker;
import org.apache.directory.api.ldap.schema.manager.impl.DefaultSchemaManager;


/**
 * Anonymize the content of a LDIF file.
 * 
 * We will replace the values of the defined attributes with random chars. There are a default
 * list of attributes that are going to be anonymized :
 * <ul>
 * <li>userPassword</li>
 * <li>displayName</li>
 * <li>givenName</li>
 * <li>surName</li>
 * <li>homePhone</li>
 * <li>homePostalAddress</li>
 * <li>jpegPhoto</li>
 * <li>labeledURI</li>
 * <li>mail</li>
 * <li>manager</li>
 * <li>mobile</li>
 * <li>organizationName</li>
 * <li>pager</li>
 * <li>photo</li>
 * <li>secretary</li>
 * <li>uid</li>
 * <li>userCertificate</li>
 * <li>userPKCS12</li>
 * <li>userSMIMECertificate</li>
 * <li>x500UniqueIdentifier</li>
 * <li>carLicense</li>
 * <li>host</li>
 * <li>locality</li>
 * <li>organizationName</li>
 * <li>organizationalUnitName</li>
 * <li>seelAlso</li>
 * <li>homeDirectory</li>
 * <li>uidNumber</li>
 * <li>gidNumber</li>
 * <li>commonName</li>
 * <li>gecos</li>
 * <li>description</li>
 * <li>memberUid</li>
 * </ul>
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class LdifAnonymizer
{
    /** The map that stores the anonymized values associated to the original value */
    private Map<Value, Value> valueMap = new HashMap<>();
    
    /** The set that contains all the values we already have anonymized */
    private Set<Value> valueSet = new HashSet<>();
    
    /** The latest anonymized String value Map */
    private Map<Integer, String> latestStringMap;
    
    /** The latest anonymized byte[] value Map */
    private Map<Integer, byte[]> latestBytesMap;
    
    /** The map of AttributeType'sOID we want to anonymize. They are all associated with anonymizers */
    private Map<String, Anonymizer> attributeAnonymizers = new HashMap<>();
    
    /** The list of existing NamingContexts */
    private Set<Dn> namingContexts = new HashSet<>();

    /** The schemaManager */
    private SchemaManager schemaManager;
    
    /** The PrintStream used to write informations about the processing */
    private PrintStream out = null;

    /**
     * Creates a default instance of LdifAnonymizer. The list of anonymized attribute
     * is set to a default value.
     *
     */
    public LdifAnonymizer()
    {
        try
        {
            schemaManager = new DefaultSchemaManager();
        }
        catch ( Exception e )
        {
            println( "Missing a SchemaManager !" );
            System.exit( -1 );
        }

        init( null, null, null, null );
    }
    
    
    /**
     * Creates a default instance of LdifAnonymizer. The list of anonymized attribute
     * is set to a default value.
     * 
     * @param schemaManager The SchemaManager instance we will use
     */
    public LdifAnonymizer( SchemaManager schemaManager )
    {
        this.schemaManager = schemaManager;

        init( null, null, null, null );
    }
    
    
    /**
     * Set the PrintStream to use to print information about the processing
     * 
     * @param out The PrintStream to use
     */
    public void setOut( PrintStream out )
    {
        this.out = out;
    }
    
    
    /**
     * Print the string into the PrintStream, with a NL at the end
     * 
     * @param str The string to print
     */
    private void println( String str )
    {
        if ( out != null )
        {
            out.println( str );
        }
    }
    
    
    /**
     * Print a nl into the PrintStream
     */
    private void println()
    {
        if ( out != null )
        {
            out.println();
        }
    }
    

    /**
     * Initialize the anonymizer, filling the maps we use.
     * 
     * @param stringLatestValueMap The map of already seen Strings
     * @param binaryLatestValueMap The map of already seen byte[]
     * @param integerLatestValueMap  The map of already seen Integers
     * @param telephoneNumberLatestValueMap   The map of already seen telephone numbers
     */
    private void init( Map<Integer, String> stringLatestValueMap, Map<Integer, byte[]> binaryLatestValueMap, 
        Map<Integer, String> integerLatestValueMap, Map<Integer, String> telephoneNumberLatestValueMap )
    {
        // Load the anonymizers
        attributeAnonymizers.put( SchemaConstants.CAR_LICENSE_AT_OID,
            new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.DOMAIN_COMPONENT_AT_OID,
            new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.CN_AT_OID, new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.DESCRIPTION_AT_OID,
            new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.DISPLAY_NAME_AT_OID,
            new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.GECOS_AT_OID, new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.GID_NUMBER_AT_OID,
            new IntegerAnonymizer( integerLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.GIVENNAME_AT_OID,
            new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.HOME_DIRECTORY_AT_OID,
            new CaseSensitiveStringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.HOME_PHONE_AT_OID,
            new TelephoneNumberAnonymizer() );
        attributeAnonymizers.put( SchemaConstants.HOME_POSTAL_ADDRESS_AT_OID,
            new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.HOST_AT_OID, new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.HOUSE_IDENTIFIER_AT_OID,
            new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.JPEG_PHOTO_AT_OID,
            new BinaryAnonymizer( binaryLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.LABELED_URI_AT_OID,
            new CaseSensitiveStringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.LOCALITY_NAME_AT_OID,
            new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.MAIL_AT_OID, new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.MANAGER_AT_OID, new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.MEMBER_UID_AT_OID,
            new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.MOBILE_AT_OID, new TelephoneNumberAnonymizer() );
        attributeAnonymizers.put( SchemaConstants.ORGANIZATION_NAME_AT_OID,
            new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.ORGANIZATIONAL_UNIT_NAME_AT_OID,
            new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.PAGER_AT_OID, new TelephoneNumberAnonymizer() );
        attributeAnonymizers.put( SchemaConstants.POSTAL_ADDRESS_AT_OID,
            new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.PHOTO_AT_OID, new BinaryAnonymizer( binaryLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.SECRETARY_AT_OID,
            new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers
            .put( SchemaConstants.SEE_ALSO_AT_OID, new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.SN_AT_OID, new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.TELEPHONE_NUMBER_AT_OID,
            new TelephoneNumberAnonymizer( telephoneNumberLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.UID_AT_OID, new StringAnonymizer( stringLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.UID_NUMBER_AT_OID,
            new IntegerAnonymizer( integerLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.USER_CERTIFICATE_AT_OID,
            new BinaryAnonymizer( binaryLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.USER_PASSWORD_AT_OID,
            new BinaryAnonymizer( binaryLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.USER_PKCS12_AT_OID,
            new BinaryAnonymizer( binaryLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.USER_SMIME_CERTIFICATE_AT_OID,
            new BinaryAnonymizer( binaryLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.X500_UNIQUE_IDENTIFIER_AT_OID,
            new BinaryAnonymizer( binaryLatestValueMap ) );
        attributeAnonymizers.put( SchemaConstants.FACSIMILE_TELEPHONE_NUMBER_AT_OID,
            new TelephoneNumberAnonymizer( telephoneNumberLatestValueMap ) );
    }
    
    
    /**
     * Set the latest value map to a defined anonymizer - if it exists -.
     *
     * @param attributeType The AttributeType we are targetting
     * @param latestValueMap The latest value map for this attribute
     */
    public void setAttributeLatestValueMap( AttributeType attributeType, Map<Integer, ?> latestValueMap )
    {
        Anonymizer anonymizer = attributeAnonymizers.get( attributeType.getOid() );
        
        if ( anonymizer != null )
        {
            if ( attributeType.getSyntax().isHumanReadable() )
            {
                anonymizer.setLatestStringMap( latestValueMap );
            }
            else
            {
                anonymizer.setLatestBytesMap( latestValueMap );
            }
        }
    }
    
    
    /**
     * Add an attributeType that has to be anonymized
     *
     * @param attributeType the AttributeType that has to be anonymized
     * @throws LdapException If the attributeType cannot be added
     */
    public void addAnonAttributeType( AttributeType attributeType ) throws LdapException
    {
        schemaManager.add( attributeType );
        LdapSyntax syntax = attributeType.getSyntax();
        
        if ( syntax.isHumanReadable() )
        {
            if ( syntax.getOid().equals( SchemaConstants.INTEGER_SYNTAX ) )
            {
                attributeAnonymizers.put( attributeType.getOid(), new IntegerAnonymizer() );
            }
            else if ( syntax.getOid().equals( SchemaConstants.DIRECTORY_STRING_SYNTAX ) )
            {
                attributeAnonymizers.put( attributeType.getOid(), new StringAnonymizer() );
            }
            else if ( syntax.getOid().equals( SchemaConstants.TELEPHONE_NUMBER_SYNTAX ) )
            {
                attributeAnonymizers.put( attributeType.getOid(), new TelephoneNumberAnonymizer() );
            }
        }
        else
        {
            attributeAnonymizers.put( attributeType.getOid(), new BinaryAnonymizer() );
        }
    }
    
    
    /**
     * Add an attributeType that has to be anonymized, with its associated anonymizer.
     *
     * @param attributeType the AttributeType that has to be anonymized
     * @param anonymizer the instance of anonymizer to use with this AttributeType
     * @throws LdapException If the attributeType cannot be added
     */
    public void addAnonAttributeType( AttributeType attributeType, Anonymizer<?> anonymizer ) throws LdapException
    {
        schemaManager.add( attributeType );
        attributeAnonymizers.put( attributeType.getOid(), anonymizer );
    }
    
    
    /**
     * Remove an attributeType that has to be anonymized
     *
     * @param attributeType the AttributeType that we don't want to be anonymized
     */
    public void removeAnonAttributeType( AttributeType attributeType )
    {
        attributeAnonymizers.remove( attributeType.getOid() );
    }
    
    
    /**
     * Get the list of configured anonymizers
     * 
     * @return The list of configured anonymizers
     */
    public Map<String, Anonymizer> getAttributeAnonymizers()
    {
        return attributeAnonymizers;
    }
    
    /**
     * Add a new NamingContext
     *
     * @param dn The naming context to add
     * @throws LdapInvalidDnException if it's an invalid naming context
     */
    public void addNamingContext( String dn ) throws LdapInvalidDnException
    {
        Dn namingContext = new Dn( schemaManager, dn );
        namingContexts.add( namingContext );
    }

    
    /**
     * Anonymize an AVA
     * 
     * @param ava The AVA to anonymize
     * @return The anonymized AVA
     * @throws LdapInvalidDnException If the Ava is invalid
     * @throws LdapInvalidAttributeValueException If teh Ava content is invalid
     */
    private Ava anonymizeAva( Ava ava ) throws LdapInvalidDnException, LdapInvalidAttributeValueException
    {
        Value value = ava.getValue();
        AttributeType attributeType = ava.getAttributeType();
        Value anonymizedValue = valueMap.get( value );
        Ava anonymizedAva;
        
        if ( anonymizedValue == null )
        {
            Attribute attribute = new DefaultAttribute( attributeType );
            attribute.add( value );
            Anonymizer anonymizer = attributeAnonymizers.get( attribute.getAttributeType().getOid() );

            if ( value.isHumanReadable() )
            {
                if ( anonymizer == null )
                {
                    anonymizedAva = new Ava( schemaManager, ava.getType(), value.getString() );
                }
                else
                {
                    Attribute anonymizedAttribute = anonymizer.anonymize( valueMap, valueSet, attribute );
                    anonymizedAva = new Ava( schemaManager, ava.getType(), anonymizedAttribute.getString() );
                }
            }
            else
            {
                if ( anonymizer == null )
                {
                    anonymizedAva = new Ava( schemaManager, ava.getType(), value.getBytes() );
                }
                else
                {
                    Attribute anonymizedAttribute = anonymizer.anonymize( valueMap, valueSet, attribute );

                    anonymizedAva = new Ava( schemaManager, ava.getType(), anonymizedAttribute.getBytes() );
                }
            }
        }
        else
        {
            if ( value.isHumanReadable() )
            {
                anonymizedAva = new Ava( schemaManager, ava.getType(), anonymizedValue.getString() );
            }
            else
            {
                anonymizedAva = new Ava( schemaManager, ava.getType(), anonymizedValue.getBytes() );
            }
        }

        return anonymizedAva;
    }
    
    
    /**
     * Anonymize the entry's DN
     * 
     * @param entryDn The DN to anonymize
     * @return The anonymized DN
     * @throws LdapException If the anonymization failed
     */
    private Dn anonymizeDn( Dn entryDn ) throws LdapException
    {
        // Search for the naming context
        Dn descendant = entryDn;
        Dn namingContext = null;
        
        for ( Dn nc : namingContexts )
        {
            if ( entryDn.isDescendantOf( nc ) )
            { 
                descendant = entryDn.getDescendantOf( nc );
                namingContext = nc;
                break;
            }
        }
        
        Rdn[] anonymizedRdns = new Rdn[entryDn.size()];
        int rdnPos = entryDn.size() - 1;

        if ( namingContext != null )
        {
            // Copy the naming contex
            for ( Rdn ncRdn : namingContext )
            {
                anonymizedRdns[rdnPos] = ncRdn;
                rdnPos--;
            }
        }
        
        // Iterate on all the RDN
        for ( Rdn rdn : descendant )
        {
            Ava[] anonymizedAvas = new Ava[rdn.size()];
            int pos = 0;
            
            // Iterate on the AVAs
            for ( Ava ava : rdn )
            {
                Ava anonymizedAva = anonymizeAva( ava );
                anonymizedAvas[pos] = anonymizedAva;
                pos++;
            }

            Rdn anonymizedRdn = new Rdn( schemaManager, anonymizedAvas );
            anonymizedRdns[rdnPos] = anonymizedRdn;
            rdnPos--;
        }
        
        return new Dn( schemaManager, anonymizedRdns );
    }


    /**
     * Anonymize a LDIF 
     * 
     * @param ldifFile The ldif file to anonymize
     * @param writer The Writer to use to write the result
     * @throws LdapException If we got some LDAP related exception
     * @throws IOException If we had some issue during some IO operations
     */
    public void anonymizeFile( String ldifFile, Writer writer ) throws LdapException, IOException
    {
        File inputFile = new File( ldifFile );
        
        if ( !inputFile.exists() )
        {
            println( "Cannot open file " + ldifFile );
            return;
        }
        
        try ( LdifReader ldifReader = new LdifReader( inputFile, schemaManager ) )
        {
            int count = 0;
            List<LdifEntry> errors = new ArrayList<>();
            List<String> errorTexts = new ArrayList<>();
    
            try
            {
                for ( LdifEntry ldifEntry : ldifReader )
                {
                    count++;
                    
                    try
                    {
                        if ( ldifEntry.isEntry() && !ldifEntry.isChangeAdd() )
                        {
                            // process a full entry. Add changes aren't processed here.
                            Entry newEntry = anonymizeEntry( ldifEntry );
                            
                            writer.write( LdifUtils.convertToLdif( newEntry ) );
                            writer.write( "\n" );
                        }
                        else if ( ldifEntry.isChangeDelete() )
                        {
                            // A Delete operation
                            LdifEntry newLdifEntry = anonymizeChangeDelete( ldifEntry );
    
                            if ( ldifEntry != null )
                            {
                                writer.write( newLdifEntry.toString() );
                                writer.write( "\n" );
                            }
                        }
                        else if ( ldifEntry.isChangeAdd() )
                        {
                            // A Add operation
                            LdifEntry newLdifEntry = anonymizeChangeAdd( ldifEntry );
    
                            if ( ldifEntry != null )
                            {
                                writer.write( newLdifEntry.toString() );
                                writer.write( "\n" );
                            }
                        }
                        else if ( ldifEntry.isChangeModify() )
                        {
                            // A Modify operation
                            LdifEntry newLdifEntry = anonymizeChangeModify( ldifEntry );
    
                            if ( ldifEntry != null )
                            {
                                writer.write( newLdifEntry.toString() );
                                writer.write( "\n" );
                            }
                        }
                        else if ( ldifEntry.isChangeModDn() ||  ldifEntry.isChangeModRdn() )
                        {
                            // A MODDN operation
                            LdifEntry newLdifEntry = anonymizeChangeModDn( ldifEntry );
    
                            if ( ldifEntry != null )
                            {
                                writer.write( newLdifEntry.toString() );
                                writer.write( "\n" );
                            }
                        }
    
                        System.out.print( '.' );
                        
                        if ( count % 100  == 0 )
                        {
                            println();
                        }
                    }
                    catch ( Exception e )
                    {
                        System.out.print( '*' );
    
                        if ( count % 100  == 0 )
                        {
                            println();
                        }
                        
                        errors.add( ldifEntry );
                        errorTexts.add( e.getMessage() );
                    }
                }
    
                println();
                
                if ( !errors.isEmpty() )
                {
                    println( "There are " + errors.size() + " bad entries" );
                    int i = 0;
                    
                    for ( LdifEntry ldifEntry : errors )
                    {
                        println( "---------------------------------------------------" );
                        println( "error : " + errorTexts.get( i ) );
                        println( ldifEntry.getDn().toString() );
                        i++;
                    }
                }
            }
            finally
            {
                println();
    
                if ( !errors.isEmpty() )
                {
                    println( "There are " + errors.size() + " bad entries" );
                }
                    
                println( "Nb entries : " + count ); 
            }
        }
    }
    
    
    /**
     * Anonymize a Modify change
     * 
     * @param ldifEntry The entry to anonymize
     * @return The anonymized entry
     * @throws LdapException If the anonymization failed
     */
    private LdifEntry anonymizeChangeModify( LdifEntry ldifEntry ) throws LdapException
    {
        Dn entryDn = ldifEntry.getDn();
        LdifEntry newLdifEntry = new LdifEntry( schemaManager );
        newLdifEntry.setChangeType( ChangeType.Modify );

        // Process the DN first
        Dn anonymizedDn = anonymizeDn( entryDn );
        
        newLdifEntry.setDn( anonymizedDn );
        
        // Now, process the entry's attributes
        for ( Modification modification : ldifEntry.getModifications() )
        {
            Attribute attribute = modification.getAttribute();
            AttributeType attributeType = schemaManager.getAttributeType( attribute.getId() );
            
            if ( attributeType == null )
            {
                System.out.println( "\nUnknown AttributeType : " + attribute.getId() + " for entry " + entryDn );
                
                return null;
            }
            
            attribute.apply( attributeType );
            
            // Deal with the special case of a DN syntax
            if ( attributeType.getSyntax().getSyntaxChecker() instanceof DnSyntaxChecker )
            {
                Value[] anonymizedValues = new Value[ attribute.size()];
                int pos = 0;
                
                for ( Value dnValue : modification.getAttribute() )
                {
                    Dn dn = new Dn( schemaManager, dnValue.getString() );
                    Dn newdDn = anonymizeDn( dn );
                    anonymizedValues[pos++] = new Value( newdDn.toString() );
                }
                
                Modification anonymizedModification = new DefaultModification( modification.getOperation(), attributeType, anonymizedValues );
                newLdifEntry.addModification( anonymizedModification );
            }
            else
            {
                Anonymizer anonymizer = attributeAnonymizers.get( attributeType.getOid() );

                if ( anonymizer == null )
                {
                    newLdifEntry.addModification( modification );
                }
                else
                {
                    Attribute anonymizedAttribute = anonymizer.anonymize( valueMap, valueSet, attribute );
                    
                    Modification anonymizedModification = new DefaultModification( modification.getOperation(), anonymizedAttribute );
                    newLdifEntry.addModification( anonymizedModification );
                }
            }
        }

        return newLdifEntry;
    }

    
    /**
     * Anonymize a Add change
     * 
     * @param ldifEntry The entry to anonymize
     * @return The anonymized entry
     * @throws LdapException If the anonymization failed
     */
    private LdifEntry anonymizeChangeAdd( LdifEntry ldifEntry ) throws LdapException
    {
        Dn entryDn = ldifEntry.getDn();
        LdifEntry newLdifEntry = new LdifEntry( schemaManager );
        newLdifEntry.setChangeType( ChangeType.Add );

        // Process the DN first
        Dn anonymizedDn = anonymizeDn( entryDn );
        
        newLdifEntry.setDn( anonymizedDn );
        
        // Now, process the entry's attributes
        for ( Attribute attribute : ldifEntry )
        {
            AttributeType attributeType = attribute.getAttributeType();
            Attribute anonymizedAttribute = new DefaultAttribute( attributeType );
            
            // Deal with the special case of a DN syntax
            
            if ( attributeType.getSyntax().getSyntaxChecker() instanceof DnSyntaxChecker )
            {
                for ( Value dnValue : attribute )
                {
                    Dn dn = new Dn( schemaManager, dnValue.getString() );
                    Dn newdDn = anonymizeDn( dn );
                    anonymizedAttribute.add( newdDn.toString() );
                }
                
                newLdifEntry.addAttribute( attribute );
            }
            else
            {
                Anonymizer anonymizer = attributeAnonymizers.get( attribute.getAttributeType().getOid() );

                if ( anonymizer == null )
                {
                    newLdifEntry.addAttribute( attribute );
                }
                else
                {
                    anonymizedAttribute = anonymizer.anonymize( valueMap, valueSet, attribute );
                    
                    if ( anonymizedAttribute != null )
                    {
                        newLdifEntry.addAttribute( anonymizedAttribute );
                    }
                }
            }
        }

        return newLdifEntry;
    }
    
    
    /**
     * Anonymize a Delete change
     * 
     * @param ldifEntry The entry to anonymize
     * @return The anonymized entry
     * @throws LdapException If the anonymization failed
     */
    private LdifEntry anonymizeChangeDelete( LdifEntry ldifEntry ) throws LdapException
    {
        Dn entryDn = ldifEntry.getDn();

        // Process the DN, there is nothing more in the entry
        Dn anonymizedDn = anonymizeDn( entryDn );
        
        ldifEntry.setDn( anonymizedDn );
        
        return ldifEntry;
    }
    
    
    /**
     * Anonymize a Delete change
     * 
     * @param ldifEntry The entry to anonymize
     * @return The anonymized entry
     * @throws LdapException If the anonymization failed
     */
    private LdifEntry anonymizeChangeModDn( LdifEntry ldifEntry ) throws LdapException
    {
        Dn entryDn = ldifEntry.getDn();

        // Process the DN
        Dn anonymizedDn = anonymizeDn( entryDn );
        
        ldifEntry.setDn( anonymizedDn );
        
        // Anonymize the newRdn if any
        String newRdnStr = ldifEntry.getNewRdn();
        
        if ( newRdnStr != null )
        {
            Dn newRdn = new Dn( schemaManager, newRdnStr );
            Dn anonymizedRdn = anonymizeDn( newRdn );
            
            ldifEntry.setNewRdn( anonymizedRdn.toString() );
        }
        
        // Anonymize the neSuperior if any
        String newSuperiorStr = ldifEntry.getNewSuperior();
        
        if ( newSuperiorStr != null )
        {
            Dn newSuperior = new Dn( schemaManager, newSuperiorStr );
            
            Dn anonymizedSuperior = anonymizeDn( newSuperior );
            
            ldifEntry.setNewSuperior( anonymizedSuperior.toString() );
        }

        return ldifEntry;
    }
    
    
    /**
     * Anonymize the full entry
     * 
     * @param ldifEntry The entry to anonymize
     * @return The anonymized entry
     * @throws LdapException If the anonymization failed
     */
    private Entry anonymizeEntry( LdifEntry ldifEntry ) throws LdapException
    {
        Entry entry = ldifEntry.getEntry();
        Entry newEntry = new DefaultEntry( schemaManager );

        // Process the DN first
        Dn entryDn = entry.getDn();
        
        Dn anonymizedDn = anonymizeDn( entryDn );
        
        // Now, process the entry's attributes
        for ( Attribute attribute : entry )
        {
            AttributeType attributeType = attribute.getAttributeType();
            
            // Deal with the special case of DN
            if ( attributeType.getSyntax().getSyntaxChecker() instanceof DnSyntaxChecker )
            {
                for ( Value dnValue : attribute )
                {
                    Dn dn = new Dn( schemaManager, dnValue.getString() );
                    Dn newdDn = anonymizeDn( dn );
                    newEntry.add( attributeType, newdDn.toString() );
                }
            }
            // Deal with the special case of a NameAndOptionalUID
            else if ( attributeType.getSyntax().getSyntaxChecker() instanceof NameAndOptionalUIDSyntaxChecker )
            {
                for ( Value dnValue : attribute )
                {
                    // Get rid of the # part (UID)
                    String valueStr = dnValue.getString();
                    int uidPos = valueStr.indexOf( '#' );
                    String uid = null;
                    
                    if ( uidPos != -1 )
                    {
                        uid = valueStr.substring( uidPos + 1 );
                        valueStr = valueStr.substring( 0, uidPos ); 
                    }
                    
                    Dn dn = new Dn( schemaManager, valueStr );
                    Dn newDn = anonymizeDn( dn );
                    String newDnStr = newDn.toString();
                    
                    if ( uid != null )
                    {
                        newDnStr = newDnStr + '#' + uid;
                    }
                    
                    newEntry.add( attributeType, newDnStr );
                }
            }
            else
            {
                Anonymizer anonymizer = attributeAnonymizers.get( attribute.getAttributeType().getOid() );

                if ( anonymizer == null )
                {
                    newEntry.add( attribute );
                }
                else
                {
                    Attribute anonymizedAttribute = anonymizer.anonymize( valueMap, valueSet, attribute );
                    
                    if ( anonymizedAttribute != null )
                    {
                        newEntry.add( anonymizedAttribute );
                    }
                }
            }
        }

        newEntry.setDn( anonymizedDn );

        return newEntry;
    }


    /**
     * Anonymize a LDIF 
     * 
     * @param ldif The ldif content to anonymize
     * @return an anonymized version of the given ldif
     * @throws LdapException If we got some LDAP related exception
     * @throws IOException If we had some issue during some IO operations
     */
    public String anonymize( String ldif ) throws LdapException, IOException
    {
        LdifReader ldifReader = new LdifReader( schemaManager );

        try
        {
            List<LdifEntry> entries = ldifReader.parseLdif( ldif );
            StringBuilder result = new StringBuilder();

            for ( LdifEntry ldifEntry : entries )
            {
                if ( ldifEntry.isEntry() && !ldifEntry.isChangeAdd() )
                {
                    // process a full entry. Add changes aren't preocessed ghere.
                    Entry newEntry = anonymizeEntry( ldifEntry );
                    
                    result.append( LdifUtils.convertToLdif( newEntry ) );
                    result.append( "\n" );
                }
                else if ( ldifEntry.isChangeDelete() )
                {
                    // A Delete operation
                    LdifEntry newLdifEntry = anonymizeChangeDelete( ldifEntry );

                    if ( newLdifEntry != null )
                    {
                        result.append( newLdifEntry );
                        result.append( "\n" );
                    }
                }
                else if ( ldifEntry.isChangeAdd() )
                {
                    // A Add operation
                    LdifEntry newLdifEntry = anonymizeChangeAdd( ldifEntry );

                    if ( newLdifEntry != null )
                    {
                        result.append( newLdifEntry );
                        result.append( "\n" );
                    }
                }
                else if ( ldifEntry.isChangeModify() )
                {
                    // A Modify operation
                    LdifEntry newLdifEntry = anonymizeChangeModify( ldifEntry );

                    if ( newLdifEntry != null )
                    {
                        result.append( newLdifEntry );
                        result.append( "\n" );
                    }
                }
                else if ( ldifEntry.isChangeModDn() ||  ldifEntry.isChangeModRdn() )
                {
                    // A MODDN operation
                    LdifEntry newLdifEntry = anonymizeChangeModDn( ldifEntry );

                    if ( newLdifEntry != null )
                    {
                        result.append( newLdifEntry );
                        result.append( "\n" );
                    }
                }
            }

            return result.toString();
        }
        catch ( Exception e )
        {
            println( "Error :"  + e.getMessage() );
            return null;
        }
        finally
        {
            ldifReader.close();
        }
    }


    /**
     * Get the map of stored values
     * 
     * @return the valueMap
     */
    public Map<Value, Value> getValueMap()
    {
        return valueMap;
    }


    /**
     * Set the map of stored values
     * 
     * @param valueMap the valueMap to set
     */
    public void setValueMap( Map<Value, Value> valueMap )
    {
        this.valueMap = valueMap;
    }


    /**
     * Get the map containing the latest anonymised Strings
     * 
     * @return the latest String Value Map
     */
    public Map<Integer, String> getLatestStringMap()
    {
        return latestStringMap;
    }


    /**
     * Set the map containing the latest anonymised Strings
     * 
     * @param latestStringMap the latest String Value Map to set
     */
    public void setLatestStringMap( Map<Integer, String> latestStringMap )
    {
        this.latestStringMap = latestStringMap;
    }


    /**
     * Get the map containing the latest anonymised bytes
     * 
     * @return the latest byte[] Value Map
     */
    public Map<Integer, byte[]> getLatestBytesMap()
    {
        return latestBytesMap;
    }


    /**
     * Set the map containing the latest anonymised bytes
     * 
     * @param latestBytesMap the latest byte[] Value Map to set
     */
    public void setLatestBytesMap( Map<Integer, byte[]> latestBytesMap )
    {
        this.latestBytesMap = latestBytesMap;
    }


    /**
     * The entry point, when used as a standalone application.
     *
     * @param args Contains the arguments : the file to convert. The anonymized 
     * LDIF will be printed on stdout
     * @throws IOException If we had an issue opening the file to anonymise ot writing the result
     * @throws LdapException If we had some issue while processing the LDAP data
     */
    public static void main( String[] args ) throws IOException, LdapException
    {
        if ( ( args == null ) || ( args.length < 1 ) )
        {
            System.out.println( "No file to anonymize" );
            return;
        }

        LdifAnonymizer anonymizer = new LdifAnonymizer();

        String ldifString = null;
        
        try ( InputStream fis = Files.newInputStream( Paths.get( args[0] ) ) )
        {
            try ( BufferedReader br = new BufferedReader( new InputStreamReader( fis, Charset.defaultCharset() ) ) )
            {
                StringBuilder sb = new StringBuilder();
                String line = br.readLine();
    
                while ( line != null )
                {
                    sb.append( line );
                    sb.append( System.lineSeparator() );
                    line = br.readLine();
                }

                ldifString = sb.toString();
            }
        }

        String result = anonymizer.anonymize( ldifString );

        System.out.println( result );
    }
}
