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