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 *    http://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, previous );
225                    reverseModifications.add( 0, reverseModification );
226                    break;
227
228                default:
229                    // Do nothing
230                    break;
231
232            }
233
234            AttributeUtils.applyModification( clonedEntry, modification );
235
236        }
237
238        // Special case if we don't have any reverse modifications
239        if ( reverseModifications.isEmpty() )
240        {
241            throw new IllegalArgumentException( I18n.err( I18n.ERR_12073, forwardModifications ) );
242        }
243
244        // Now, push the reversed list into the entry
245        for ( Modification modification : reverseModifications )
246        {
247            entry.addModification( modification );
248        }
249
250        // Return the reverted entry
251        return entry;
252    }
253
254
255    /**
256     * Compute a reverse LDIF for a forward change which if in LDIF format
257     * would represent a Move operation. Hence there is no newRdn in the
258     * picture here.
259     *
260     * @param newSuperiorDn the new parent dn to be (must not be null)
261     * @param modifiedDn the dn of the entry being moved (must not be null)
262     * @return a reverse LDIF
263     * @throws LdapException if something went wrong
264     */
265    public static LdifEntry reverseMove( Dn newSuperiorDn, Dn modifiedDn ) throws LdapException
266    {
267        LdifEntry entry = new LdifEntry();
268        Dn currentParent;
269        Rdn currentRdn;
270        Dn newDn;
271
272        if ( newSuperiorDn == null )
273        {
274            throw new IllegalArgumentException( I18n.err( I18n.ERR_12074 ) );
275        }
276
277        if ( modifiedDn == null )
278        {
279            throw new IllegalArgumentException( I18n.err( I18n.ERR_12075 ) );
280        }
281
282        if ( modifiedDn.size() == 0 )
283        {
284            throw new IllegalArgumentException( I18n.err( I18n.ERR_12076 ) );
285        }
286
287        currentParent = modifiedDn;
288        currentRdn = currentParent.getRdn();
289        currentParent = currentParent.getParent();
290
291        newDn = newSuperiorDn;
292        newDn = newDn.add( modifiedDn.getRdn() );
293
294        entry.setChangeType( ChangeType.ModDn );
295        entry.setDn( newDn );
296        entry.setNewRdn( currentRdn.getName() );
297        entry.setNewSuperior( currentParent.getName() );
298        entry.setDeleteOldRdn( false );
299        return entry;
300    }
301
302
303    /**
304     * A small helper class to compute the simple revert.
305     */
306    private static LdifEntry revertEntry( Entry entry, Dn newDn, Dn newSuperior, Rdn oldRdn, Rdn newRdn )
307        throws LdapInvalidDnException
308    {
309        LdifEntry reverted = new LdifEntry();
310
311        // We have a composite old Rdn, something like A=a+B=b
312        // It does not matter if the RDNs overlap
313        reverted.setChangeType( ChangeType.ModRdn );
314
315        if ( newSuperior != null )
316        {
317            Dn restoredDn = newSuperior.add( newRdn );
318            reverted.setDn( restoredDn );
319        }
320        else
321        {
322            reverted.setDn( newDn );
323        }
324
325        reverted.setNewRdn( oldRdn.getName() );
326
327        // Is the newRdn's value present in the entry ?
328        // ( case 3, 4 and 5)
329        // If keepOldRdn = true, we cover case 4 and 5
330        boolean keepOldRdn = entry.contains( newRdn.getNormType(), newRdn.getNormValue() );
331
332        reverted.setDeleteOldRdn( !keepOldRdn );
333
334        if ( newSuperior != null )
335        {
336            Dn oldSuperior = entry.getDn();
337
338            oldSuperior = oldSuperior.getParent();
339            reverted.setNewSuperior( oldSuperior.getName() );
340        }
341
342        return reverted;
343    }
344
345
346    /**
347     * A helper method to generate the modified attribute after a rename.
348     */
349    private static LdifEntry generateModify( Dn parentDn, Entry entry, Rdn oldRdn, Rdn newRdn )
350    {
351        LdifEntry restored = new LdifEntry();
352        restored.setChangeType( ChangeType.Modify );
353
354        // We have to use the parent Dn, the entry has already
355        // been renamed
356        restored.setDn( parentDn );
357
358        for ( Ava ava : newRdn )
359        {
360            // No need to add something which has already been added
361            // in the previous modification
362            if ( !entry.contains( ava.getNormType(), ava.getValue().getString() )
363                && !( ava.getNormType().equals( oldRdn.getNormType() ) && ava.getValue().getString().equals(
364                    oldRdn.getNormValue() ) ) )
365            {
366                // Create the modification, which is an Remove
367                Modification modification = new DefaultModification( ModificationOperation.REMOVE_ATTRIBUTE,
368                    new DefaultAttribute( ava.getType(), ava.getValue().getString() ) );
369
370                restored.addModification( modification );
371            }
372        }
373
374        return restored;
375    }
376
377
378    /**
379     * A helper method which generates a reverted entry
380     */
381    private static LdifEntry generateReverted( Dn newSuperior, Rdn newRdn, Dn newDn, Rdn oldRdn, boolean deleteOldRdn )
382        throws LdapInvalidDnException
383    {
384        LdifEntry reverted = new LdifEntry();
385        reverted.setChangeType( ChangeType.ModRdn );
386
387        if ( newSuperior != null )
388        {
389            Dn restoredDn = newSuperior.add( newRdn );
390            reverted.setDn( restoredDn );
391        }
392        else
393        {
394            reverted.setDn( newDn );
395        }
396
397        reverted.setNewRdn( oldRdn.getName() );
398
399        if ( newSuperior != null )
400        {
401            Dn oldSuperior = newDn;
402
403            oldSuperior = oldSuperior.getParent();
404            reverted.setNewSuperior( oldSuperior.getName() );
405        }
406
407        // Delete the newRDN values
408        reverted.setDeleteOldRdn( deleteOldRdn );
409
410        return reverted;
411    }
412
413
414    /**
415     * Revert a Dn to it's previous version by removing the first Rdn and adding the given Rdn.
416     * It's a rename operation. The biggest issue is that we have many corner cases, depending
417     * on the RDNs we are manipulating, and on the content of the initial entry.
418     * 
419     * @param entry The initial Entry
420     * @param newRdn The new Rdn
421     * @param deleteOldRdn A flag which tells to delete the old Rdn AVAs
422     * @return A list of LDIF reverted entries
423     * @throws LdapInvalidDnException If the name reverting failed
424     */
425    public static List<LdifEntry> reverseRename( Entry entry, Rdn newRdn, boolean deleteOldRdn )
426        throws LdapInvalidDnException
427    {
428        return reverseMoveAndRename( entry, null, newRdn, deleteOldRdn );
429    }
430
431
432    /**
433     * Revert a Dn to it's previous version by removing the first Rdn and adding the given Rdn.
434     * It's a rename operation. The biggest issue is that we have many corner cases, depending
435     * on the RDNs we are manipulating, and on the content of the initial entry.
436     * 
437     * @param entry The initial Entry
438     * @param newSuperior The new superior Dn (can be null if it's just a rename)
439     * @param newRdn The new Rdn
440     * @param deleteOldRdn A flag which tells to delete the old Rdn AVAs
441     * @return A list of LDIF reverted entries
442     * @throws LdapInvalidDnException If the name reverting failed
443     */
444    public static List<LdifEntry> reverseMoveAndRename( Entry entry, Dn newSuperior, Rdn newRdn, boolean deleteOldRdn )
445        throws LdapInvalidDnException
446    {
447        Dn parentDn = entry.getDn();
448        Dn newDn;
449
450        if ( newRdn == null )
451        {
452            throw new IllegalArgumentException( I18n.err( I18n.ERR_12077 ) );
453        }
454
455        if ( parentDn == null )
456        {
457            throw new IllegalArgumentException( I18n.err( I18n.ERR_12078 ) );
458        }
459
460        if ( parentDn.size() == 0 )
461        {
462            throw new IllegalArgumentException( I18n.err( I18n.ERR_12079 ) );
463        }
464
465        parentDn = entry.getDn();
466        Rdn oldRdn = parentDn.getRdn();
467
468        newDn = parentDn;
469        newDn = newDn.getParent();
470        newDn = newDn.add( newRdn );
471
472        List<LdifEntry> entries = new ArrayList<>( 1 );
473        LdifEntry reverted;
474
475        // Start with the cases here
476        if ( newRdn.size() == 1 )
477        {
478            // We have a simple new Rdn, something like A=a
479            reverted = revertEntry( entry, newDn, newSuperior, oldRdn, newRdn );
480
481            entries.add( reverted );
482        }
483        else
484        {
485            // We have a composite new Rdn, something like A=a+B=b
486            if ( oldRdn.size() == 1 )
487            {
488                // The old Rdn is simple
489                boolean existInEntry = false;
490
491                // Does it overlap ?
492                // Is the new Rdn AVAs contained into the entry?
493                for ( Ava atav : newRdn )
494                {
495                    if ( !atav.equals( oldRdn.getAva() )
496                        && ( entry.contains( atav.getNormType(), atav.getValue().getString() ) ) )
497                    {
498                        existInEntry = true;
499                    }
500                }
501
502                // The new Rdn includes the old one
503                if ( existInEntry )
504                {
505                    // Some of the new Rdn AVAs existed in the entry
506                    // We have to restore them, but we also have to remove
507                    // the new values
508                    reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, KEEP_OLD_RDN );
509
510                    entries.add( reverted );
511
512                    // Now, restore the initial values
513                    LdifEntry restored = generateModify( parentDn, entry, oldRdn, newRdn );
514
515                    entries.add( restored );
516                }
517                else
518                {
519                    // This is the simplest case, we don't have to restore
520                    // some existing values (case 8.1 and 9.1)
521                    reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, DELETE_OLD_RDN );
522
523                    entries.add( reverted );
524                }
525            }
526            else
527            {
528                // We have a composite new Rdn, something like A=a+B=b
529                // Does the Rdn overlap ?
530                boolean overlapping = false;
531                boolean existInEntry = false;
532
533                Set<Ava> oldAtavs = new HashSet<>();
534
535                // We first build a set with all the oldRDN ATAVs
536                for ( Ava atav : oldRdn )
537                {
538                    oldAtavs.add( atav );
539                }
540
541                // Now we loop on the newRDN ATAVs to evaluate if the Rdns are overlaping
542                // and if the newRdn ATAVs are present in the entry
543                for ( Ava atav : newRdn )
544                {
545                    if ( oldAtavs.contains( atav ) )
546                    {
547                        overlapping = true;
548                    }
549                    else if ( entry.contains( atav.getNormType(), atav.getValue().getString() ) )
550                    {
551                        existInEntry = true;
552                    }
553                }
554
555                if ( overlapping )
556                {
557                    // They overlap
558                    if ( existInEntry )
559                    {
560                        // In this case, we have to reestablish the removed ATAVs
561                        // (Cases 12.2 and 13.2)
562                        reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, KEEP_OLD_RDN );
563
564                        entries.add( reverted );
565                    }
566                    else
567                    {
568                        // We can simply remove all the new Rdn atavs, as the
569                        // overlapping values will be re-created.
570                        // (Cases 12.1 and 13.1)
571                        reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, DELETE_OLD_RDN );
572
573                        entries.add( reverted );
574                    }
575                }
576                else
577                {
578                    // No overlapping
579                    if ( existInEntry )
580                    {
581                        // In this case, we have to reestablish the removed ATAVs
582                        // (Cases 10.2 and 11.2)
583                        reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, KEEP_OLD_RDN );
584
585                        entries.add( reverted );
586
587                        LdifEntry restored = generateModify( parentDn, entry, oldRdn, newRdn );
588
589                        entries.add( restored );
590                    }
591                    else
592                    {
593                        // We are safe ! We can delete all the new Rdn ATAVs
594                        // (Cases 10.1 and 11.1)
595                        reverted = generateReverted( newSuperior, newRdn, newDn, oldRdn, DELETE_OLD_RDN );
596
597                        entries.add( reverted );
598                    }
599                }
600            }
601        }
602
603        return entries;
604    }
605}