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