View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    * 
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   * 
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   * 
19   */
20  package org.apache.directory.api.ldap.model.ldif;
21  
22  
23  import java.util.ArrayList;
24  import java.util.HashSet;
25  import java.util.List;
26  import java.util.Set;
27  
28  import org.apache.directory.api.i18n.I18n;
29  import org.apache.directory.api.ldap.model.entry.Attribute;
30  import org.apache.directory.api.ldap.model.entry.AttributeUtils;
31  import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
32  import org.apache.directory.api.ldap.model.entry.DefaultModification;
33  import org.apache.directory.api.ldap.model.entry.Entry;
34  import org.apache.directory.api.ldap.model.entry.Modification;
35  import org.apache.directory.api.ldap.model.entry.ModificationOperation;
36  import org.apache.directory.api.ldap.model.exception.LdapException;
37  import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
38  import org.apache.directory.api.ldap.model.name.Ava;
39  import org.apache.directory.api.ldap.model.name.Dn;
40  import org.apache.directory.api.ldap.model.name.Rdn;
41  
42  
43  /**
44   * A helper class which provides methods to reverse a LDIF modification operation.
45   *
46   * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
47   */
48  public final class LdifRevertor
49  {
50      /** Flag used when we want to delete the old Rdn */
51      public static final boolean DELETE_OLD_RDN = true;
52  
53      /** Flag used when we want to keep the old Rdn */
54      public static final boolean KEEP_OLD_RDN = false;
55  
56  
57      /**
58       * Private constructor.
59       */
60      private LdifRevertor()
61      {
62      }
63  
64  
65      /**
66       * Compute a reverse LDIF of an AddRequest. It's simply a delete request
67       * of the added entry
68       *
69       * @param dn the dn of the added entry
70       * @return a reverse LDIF
71       */
72      public static LdifEntry reverseAdd( Dn dn )
73      {
74          LdifEntry entry = new LdifEntry();
75          entry.setChangeType( ChangeType.Delete );
76          entry.setDn( dn );
77          return entry;
78      }
79  
80  
81      /**
82       * Compute a reverse LDIF of a DeleteRequest. We have to get the previous
83       * entry in order to restore it.
84       *
85       * @param dn The deleted entry Dn
86       * @param deletedEntry The entry which has been deleted
87       * @return A reverse LDIF
88       * @throws LdapException If something went wrong
89       */
90      public static LdifEntry reverseDel( Dn dn, Entry deletedEntry ) throws LdapException
91      {
92          LdifEntry entry = new LdifEntry();
93  
94          entry.setDn( dn );
95          entry.setChangeType( ChangeType.Add );
96  
97          for ( Attribute attribute : deletedEntry )
98          {
99              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 }