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