001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 * 
010 *    https://www.apache.org/licenses/LICENSE-2.0
011 * 
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 * 
019 */
020package org.apache.directory.api.ldap.model.ldif;
021
022
023import java.util.ArrayList;
024import java.util.HashSet;
025import java.util.List;
026import java.util.Set;
027
028import org.apache.directory.api.i18n.I18n;
029import org.apache.directory.api.ldap.model.entry.Attribute;
030import org.apache.directory.api.ldap.model.entry.AttributeUtils;
031import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
032import org.apache.directory.api.ldap.model.entry.DefaultModification;
033import org.apache.directory.api.ldap.model.entry.Entry;
034import org.apache.directory.api.ldap.model.entry.Modification;
035import org.apache.directory.api.ldap.model.entry.ModificationOperation;
036import org.apache.directory.api.ldap.model.exception.LdapException;
037import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
038import org.apache.directory.api.ldap.model.name.Ava;
039import org.apache.directory.api.ldap.model.name.Dn;
040import org.apache.directory.api.ldap.model.name.Rdn;
041
042
043/**
044 * A helper class which provides methods to reverse a LDIF modification operation.
045 *
046 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
047 */
048public final class LdifRevertor
049{
050    /** Flag used when we want to delete the old Rdn */
051    public static final boolean DELETE_OLD_RDN = true;
052
053    /** Flag used when we want to keep the old Rdn */
054    public static final boolean KEEP_OLD_RDN = false;
055
056
057    /**
058     * Private constructor.
059     */
060    private LdifRevertor()
061    {
062    }
063
064
065    /**
066     * Compute a reverse LDIF of an AddRequest. It's simply a delete request
067     * of the added entry
068     *
069     * @param dn the dn of the added entry
070     * @return a reverse LDIF
071     */
072    public static LdifEntry reverseAdd( Dn dn )
073    {
074        LdifEntry entry = new LdifEntry();
075        entry.setChangeType( ChangeType.Delete );
076        entry.setDn( dn );
077        return entry;
078    }
079
080
081    /**
082     * Compute a reverse LDIF of a DeleteRequest. We have to get the previous
083     * entry in order to restore it.
084     *
085     * @param dn The deleted entry Dn
086     * @param deletedEntry The entry which has been deleted
087     * @return A reverse LDIF
088     * @throws LdapException If something went wrong
089     */
090    public static LdifEntry reverseDel( Dn dn, Entry deletedEntry ) throws LdapException
091    {
092        LdifEntry entry = new LdifEntry();
093
094        entry.setDn( dn );
095        entry.setChangeType( ChangeType.Add );
096
097        for ( Attribute attribute : deletedEntry )
098        {
099            entry.addAttribute( attribute );
100        }
101
102        return entry;
103    }
104
105
106    /**
107     *
108     * Compute the reversed LDIF for a modify request. We will deal with the
109     * three kind of modifications :
110     * <ul>
111     * <li>add</li>
112     * <li>remove</li>
113     * <li>replace</li>
114     * </ul>
115     * 
116     * As the modifications should be issued in a reversed order ( ie, for
117     * the initials modifications {A, B, C}, the reversed modifications will
118     * be ordered like {C, B, A}), we will change the modifications order.
119     *
120     * @param dn the dn of the modified entry
121     * @param forwardModifications the modification items for the forward change
122     * @param modifiedEntry The modified entry. Necessary for the destructive modifications
123     * @return A reversed LDIF
124     * @throws LdapException If something went wrong
125     */
126    public static LdifEntry reverseModify( Dn dn, List<Modification> forwardModifications, Entry modifiedEntry )
127        throws LdapException
128    {
129        // First, protect the original entry by cloning it : we will modify it
130        Entry clonedEntry = modifiedEntry.clone();
131
132        LdifEntry entry = new LdifEntry();
133        entry.setChangeType( ChangeType.Modify );
134
135        entry.setDn( dn );
136
137        // As the reversed modifications should be pushed in reversed order,
138        // we create a list to temporarily store the modifications.
139        List<Modification> reverseModifications = new ArrayList<>();
140
141        // Loop through all the modifications. For each modification, we will
142        // have to apply it to the modified entry in order to be able to generate
143        // the reversed modification
144        for ( Modification modification : forwardModifications )
145        {
146            switch ( modification.getOperation() )
147            {
148                case ADD_ATTRIBUTE:
149                    Attribute mod = modification.getAttribute();
150
151                    Attribute previous = clonedEntry.get( mod.getId() );
152
153                    if ( mod.equals( previous ) )
154                    {
155                        continue;
156                    }
157
158                    Modification reverseModification = new DefaultModification( ModificationOperation.REMOVE_ATTRIBUTE,
159                        mod );
160                    reverseModifications.add( 0, reverseModification );
161                    break;
162
163                case REMOVE_ATTRIBUTE:
164                    mod = modification.getAttribute();
165
166                    previous = clonedEntry.get( mod.getId() );
167
168                    if ( previous == null )
169                    {
170                        // Nothing to do if the previous attribute didn't exist
171                        continue;
172                    }
173
174                    if ( mod.get() == null )
175                    {
176                        reverseModification = new DefaultModification( ModificationOperation.ADD_ATTRIBUTE, previous );
177                        reverseModifications.add( 0, reverseModification );
178                        break;
179                    }
180
181                    reverseModification = new DefaultModification( ModificationOperation.ADD_ATTRIBUTE, mod );
182                    reverseModifications.add( 0, reverseModification );
183                    break;
184
185                case REPLACE_ATTRIBUTE:
186                    mod = modification.getAttribute();
187
188                    previous = clonedEntry.get( mod.getId() );
189
190                    /*
191                     * The server accepts without complaint replace
192                     * modifications to non-existing attributes in the
193                     * entry.  When this occurs nothing really happens
194                     * but this method freaks out.  To prevent that we
195                     * make such no-op modifications produce the same
196                     * modification for the reverse direction which should
197                     * do nothing as well.
198                     */
199                    if ( ( mod.get() == null ) && ( previous == null ) )
200                    {
201                        reverseModification = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
202                            new DefaultAttribute( mod.getId() ) );
203                        reverseModifications.add( 0, reverseModification );
204                        continue;
205                    }
206
207                    if ( mod.get() == null )
208                    {
209                        reverseModification = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
210                            previous );
211                        reverseModifications.add( 0, reverseModification );
212                        continue;
213                    }
214
215                    if ( previous == null )
216                    {
217                        Attribute emptyAttribute = new DefaultAttribute( mod.getId() );
218                        reverseModification = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
219                            emptyAttribute );
220                        reverseModifications.add( 0, reverseModification );
221                        continue;
222                    }
223
224                    reverseModification = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, 
225                        previous );
226                    reverseModifications.add( 0, reverseModification );
227                    break;
228                    
229                case INCREMENT_ATTRIBUTE:
230                    mod = modification.getAttribute();
231                    previous = clonedEntry.get( mod.getId() );
232
233                    reverseModification = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
234                        previous );
235                    reverseModifications.add( 0, reverseModification );
236                    break;
237
238                default:
239                    // Do nothing
240                    break;
241
242            }
243
244            AttributeUtils.applyModification( clonedEntry, modification );
245
246        }
247
248        // Special case if we don't have any reverse modifications
249        if ( reverseModifications.isEmpty() )
250        {
251            throw new IllegalArgumentException( I18n.err( I18n.ERR_13465_CANT_DEDUCE_REVERSE_FOR_MOD, forwardModifications ) );
252        }
253
254        // Now, push the reversed list into the entry
255        for ( Modification modification : reverseModifications )
256        {
257            entry.addModification( modification );
258        }
259
260        // Return the reverted entry
261        return entry;
262    }
263
264
265    /**
266     * Compute a reverse LDIF for a forward change which if in LDIF format
267     * would represent a Move operation. Hence there is no newRdn in the
268     * picture here.
269     *
270     * @param newSuperiorDn the new parent dn to be (must not be null)
271     * @param modifiedDn the dn of the entry being moved (must not be null)
272     * @return a reverse LDIF
273     * @throws LdapException if something went wrong
274     */
275    public static LdifEntry reverseMove( Dn newSuperiorDn, Dn modifiedDn ) throws LdapException
276    {
277        LdifEntry entry = new LdifEntry();
278        Dn currentParent;
279        Rdn currentRdn;
280        Dn newDn;
281
282        if ( newSuperiorDn == null )
283        {
284            throw new IllegalArgumentException( I18n.err( I18n.ERR_13466_NEW_SUPERIOR_DN_NULL ) );
285        }
286
287        if ( modifiedDn == null )
288        {
289            throw new IllegalArgumentException( I18n.err( I18n.ERR_13467_NULL_MODIFIED_DN ) );
290        }
291
292        if ( modifiedDn.size() == 0 )
293        {
294            throw new IllegalArgumentException( I18n.err( I18n.ERR_13468_DONT_MOVE_ROOTDSE ) );
295        }
296
297        currentParent = modifiedDn;
298        currentRdn = currentParent.getRdn();
299        currentParent = currentParent.getParent();
300
301        newDn = newSuperiorDn;
302        newDn = newDn.add( modifiedDn.getRdn() );
303
304        entry.setChangeType( ChangeType.ModDn );
305        entry.setDn( newDn );
306        entry.setNewRdn( currentRdn.getName() );
307        entry.setNewSuperior( currentParent.getName() );
308        entry.setDeleteOldRdn( false );
309        return entry;
310    }
311
312
313    /**
314     * A small helper class to compute the simple revert.
315     * 
316     * @param entry The entry to revert
317     * @param newDn The new Dn
318     * @param newSuperior The new superior, if it has changed (null otherwise)
319     * @param oldRdn The old Rdn
320     * @param newRdn The new RDN if the RDN has changed
321     * @return The reverted entry
322     * @throws LdapInvalidDnException If the Dn is invalid
323     */
324    private static LdifEntry revertEntry( Entry entry, Dn newDn, Dn newSuperior, Rdn oldRdn, Rdn newRdn )
325        throws LdapInvalidDnException
326    {
327        LdifEntry reverted = new LdifEntry();
328
329        // We have a composite old Rdn, something like A=a+B=b
330        // It does not matter if the RDNs overlap
331        reverted.setChangeType( ChangeType.ModRdn );
332
333        if ( newSuperior != null )
334        {
335            Dn restoredDn = newSuperior.add( newRdn );
336            reverted.setDn( restoredDn );
337        }
338        else
339        {
340            reverted.setDn( newDn );
341        }
342
343        reverted.setNewRdn( oldRdn.getName() );
344
345        // Is the newRdn's value present in the entry ?
346        // ( case 3, 4 and 5)
347        // If keepOldRdn = true, we cover case 4 and 5
348        boolean keepOldRdn = entry.contains( newRdn.getNormType(), newRdn.getValue() );
349
350        reverted.setDeleteOldRdn( !keepOldRdn );
351
352        if ( newSuperior != null )
353        {
354            Dn oldSuperior = entry.getDn();
355
356            oldSuperior = oldSuperior.getParent();
357            reverted.setNewSuperior( oldSuperior.getName() );
358        }
359
360        return reverted;
361    }
362
363
364    /**
365     * A helper method to generate the modified attribute after a rename.
366     * 
367     * @param parentDn The parent Dn
368     * @param entry The entry to revert
369     * @param oldRdn The old Rdn
370     * @param newRdn The new Rdn
371     * @return The modified entry
372     */
373    private static LdifEntry generateModify( Dn parentDn, Entry entry, Rdn oldRdn, Rdn newRdn )
374    {
375        LdifEntry restored = new LdifEntry();
376        restored.setChangeType( ChangeType.Modify );
377
378        // We have to use the parent Dn, the entry has already
379        // been renamed
380        restored.setDn( parentDn );
381
382        for ( Ava ava : newRdn )
383        {
384            // No need to add something which has already been added
385            // in the previous modification
386            if ( !entry.contains( ava.getNormType(), ava.getValue() )
387                && !( ava.getNormType().equals( oldRdn.getNormType() ) && ava.getValue().equals(
388                    oldRdn.getValue() ) ) )
389            {
390                // Create the modification, which is an Remove
391                Modification modification = new DefaultModification( ModificationOperation.REMOVE_ATTRIBUTE,
392                    new DefaultAttribute( ava.getType(), ava.getValue() ) );
393
394                restored.addModification( modification );
395            }
396        }
397
398        return restored;
399    }
400
401
402    /**
403     * A helper method which generates a reverted entry for a MODDN operation
404     * 
405     * @param newSuperior The new superior, if it has changed (null otherwise)
406     * @param newRdn The new RDN if the RDN has changed
407     * @param newDn The new Dn
408     * @param oldRdn The old Rdn
409     * @param deleteOldRdn If the old Rdn attributes must be deleted or not
410     * @return The reverted entry
411     * @throws LdapInvalidDnException If the DN is invalid
412     */
413    private static LdifEntry generateReverted( Dn newSuperior, Rdn newRdn, Dn newDn, Rdn oldRdn, boolean deleteOldRdn )
414        throws LdapInvalidDnException
415    {
416        LdifEntry reverted = new LdifEntry();
417        reverted.setChangeType( ChangeType.ModRdn );
418
419        if ( newSuperior != null )
420        {
421            Dn restoredDn = newSuperior.add( newRdn );
422            reverted.setDn( restoredDn );
423        }
424        else
425        {
426            reverted.setDn( newDn );
427        }
428
429        reverted.setNewRdn( oldRdn.getName() );
430
431        if ( newSuperior != null )
432        {
433            Dn oldSuperior = newDn;
434
435            oldSuperior = oldSuperior.getParent();
436            reverted.setNewSuperior( oldSuperior.getName() );
437        }
438
439        // Delete the newRDN values
440        reverted.setDeleteOldRdn( deleteOldRdn );
441
442        return reverted;
443    }
444
445
446    /**
447     * Revert a Dn to it's previous version by removing the first Rdn and adding the given Rdn.
448     * It's a rename operation. The biggest issue is that we have many corner cases, depending
449     * on the RDNs we are manipulating, and on the content of the initial entry.
450     * 
451     * @param entry The initial Entry
452     * @param newRdn The new Rdn
453     * @param deleteOldRdn A flag which tells to delete the old Rdn AVAs
454     * @return A list of LDIF reverted entries
455     * @throws LdapInvalidDnException If the name reverting failed
456     */
457    public static List<LdifEntry> reverseRename( Entry entry, Rdn newRdn, boolean deleteOldRdn )
458        throws LdapInvalidDnException
459    {
460        return reverseMoveAndRename( entry, null, newRdn, deleteOldRdn );
461    }
462
463
464    /**
465     * Revert a Dn to it's previous version by removing the first Rdn and adding the given Rdn.
466     * It's a rename operation. The biggest issue is that we have many corner cases, depending
467     * on the RDNs we are manipulating, and on the content of the initial entry.
468     * 
469     * @param entry The initial Entry
470     * @param newSuperior The new superior Dn (can be null if it's just a rename)
471     * @param newRdn The new Rdn
472     * @param deleteOldRdn A flag which tells to delete the old Rdn AVAs
473     * @return A list of LDIF reverted entries
474     * @throws LdapInvalidDnException If the name reverting failed
475     */
476    public static List<LdifEntry> reverseMoveAndRename( Entry entry, Dn newSuperior, Rdn newRdn, boolean deleteOldRdn )
477        throws LdapInvalidDnException
478    {
479        Dn parentDn = entry.getDn();
480        Dn newDn;
481
482        if ( newRdn == null )
483        {
484            throw new IllegalArgumentException( I18n.err( I18n.ERR_13469_NULL_READ_DN ) );
485        }
486
487        if ( parentDn == null )
488        {
489            throw new IllegalArgumentException( I18n.err( I18n.ERR_13467_NULL_MODIFIED_DN ) );
490        }
491
492        if ( parentDn.size() == 0 )
493        {
494            throw new IllegalArgumentException( I18n.err( I18n.ERR_13470_DONT_RENAME_ROOTDSE ) );
495        }
496
497        parentDn = entry.getDn();
498        Rdn oldRdn = parentDn.getRdn();
499
500        newDn = parentDn;
501        newDn = newDn.getParent();
502        newDn = newDn.add( newRdn );
503
504        List<LdifEntry> entries = new ArrayList<>( 1 );
505        LdifEntry reverted;
506
507        // Start with the cases here
508        if ( newRdn.size() == 1 )
509        {
510            // We have a simple new Rdn, something like A=a
511            reverted = revertEntry( entry, newDn, newSuperior, oldRdn, newRdn );
512
513            entries.add( reverted );
514        }
515        else
516        {
517            // We have a composite new Rdn, something like A=a+B=b
518            if ( oldRdn.size() == 1 )
519            {
520                // The old Rdn is simple
521                boolean existInEntry = false;
522
523                // Does it overlap ?
524                // Is the new Rdn AVAs contained into the entry?
525                for ( Ava atav : newRdn )
526                {
527                    if ( !atav.equals( oldRdn.getAva() )
528                        && ( entry.contains( atav.getNormType(), atav.getValue() ) ) )
529                    {
530                        existInEntry = true;
531                    }
532                }
533
534                // The new Rdn includes the old one
535                if ( existInEntry )
536                {
537                    // Some of the new Rdn AVAs existed in the entry
538                    // We have to restore them, but we also have to remove
539                    // the new values
540                    reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, KEEP_OLD_RDN );
541
542                    entries.add( reverted );
543
544                    // Now, restore the initial values
545                    LdifEntry restored = generateModify( parentDn, entry, oldRdn, newRdn );
546
547                    entries.add( restored );
548                }
549                else
550                {
551                    // This is the simplest case, we don't have to restore
552                    // some existing values (case 8.1 and 9.1)
553                    reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, DELETE_OLD_RDN );
554
555                    entries.add( reverted );
556                }
557            }
558            else
559            {
560                // We have a composite new Rdn, something like A=a+B=b
561                // Does the Rdn overlap ?
562                boolean overlapping = false;
563                boolean existInEntry = false;
564
565                Set<Ava> oldAtavs = new HashSet<>();
566
567                // We first build a set with all the oldRDN ATAVs
568                for ( Ava atav : oldRdn )
569                {
570                    oldAtavs.add( atav );
571                }
572
573                // Now we loop on the newRDN ATAVs to evaluate if the Rdns are overlaping
574                // and if the newRdn ATAVs are present in the entry
575                for ( Ava atav : newRdn )
576                {
577                    if ( oldAtavs.contains( atav ) )
578                    {
579                        overlapping = true;
580                    }
581                    else if ( entry.contains( atav.getNormType(), atav.getValue() ) )
582                    {
583                        existInEntry = true;
584                    }
585                }
586
587                if ( overlapping )
588                {
589                    // They overlap
590                    if ( existInEntry )
591                    {
592                        // In this case, we have to reestablish the removed ATAVs
593                        // (Cases 12.2 and 13.2)
594                        reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, KEEP_OLD_RDN );
595
596                        entries.add( reverted );
597                    }
598                    else
599                    {
600                        // We can simply remove all the new Rdn atavs, as the
601                        // overlapping values will be re-created.
602                        // (Cases 12.1 and 13.1)
603                        reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, DELETE_OLD_RDN );
604
605                        entries.add( reverted );
606                    }
607                }
608                else
609                {
610                    // No overlapping
611                    if ( existInEntry )
612                    {
613                        // In this case, we have to reestablish the removed ATAVs
614                        // (Cases 10.2 and 11.2)
615                        reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, KEEP_OLD_RDN );
616
617                        entries.add( reverted );
618
619                        LdifEntry restored = generateModify( parentDn, entry, oldRdn, newRdn );
620
621                        entries.add( restored );
622                    }
623                    else
624                    {
625                        // We are safe ! We can delete all the new Rdn ATAVs
626                        // (Cases 10.1 and 11.1)
627                        reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, DELETE_OLD_RDN );
628
629                        entries.add( reverted );
630                    }
631                }
632            }
633        }
634
635        return entries;
636    }
637}