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<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 }