/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * */ package org.apache.directory.studio.openldap.config.jobs; import java.text.ParseException; import java.util.ArrayList; import java.util.List; import org.apache.directory.api.ldap.model.constants.LdapConstants; import org.apache.directory.api.ldap.model.constants.SchemaConstants; import org.apache.directory.api.ldap.model.cursor.CursorException; import org.apache.directory.api.ldap.model.entry.Attribute; import org.apache.directory.api.ldap.model.entry.DefaultModification; import org.apache.directory.api.ldap.model.entry.Entry; import org.apache.directory.api.ldap.model.entry.Modification; import org.apache.directory.api.ldap.model.entry.ModificationOperation; import org.apache.directory.api.ldap.model.exception.LdapException; import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException; import org.apache.directory.api.ldap.model.filter.FilterParser; import org.apache.directory.api.ldap.model.ldif.ChangeType; import org.apache.directory.api.ldap.model.ldif.LdifEntry; import org.apache.directory.api.ldap.model.message.AliasDerefMode; import org.apache.directory.api.ldap.model.message.SearchScope; import org.apache.directory.api.ldap.model.name.Dn; import org.apache.directory.api.ldap.model.schema.AttributeType; import org.apache.directory.api.ldap.model.schema.UsageEnum; import org.apache.directory.server.core.api.filtering.EntryFilteringCursor; import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext; import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext; import org.apache.directory.server.core.api.partition.Partition; /** * An utility class that computes the difference between two Partitions. * * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a> */ public class PartitionsDiffComputer { /** * Compute the difference between two partitions : * <ul> * <li>Added entries</li> * <li>Removed entries</li> * <li>Modified entries * <ul> * <li>Added Attributes</li> * <li>Removed Attributes</li> * <li>Modified Attributes * <ul> * <li>Added Values</li> * <li>Removed Values</li> * </ul> * </li> * </ul> * </li> * </ul> * @param originalPartition The original partition * @param modifiedPartition The modified partition * @return A list of LDIF Additions, Deletions or Modifications * @throws Exception If something went wrong */ public static List<LdifEntry> computeModifications( Partition originalPartition, Partition modifiedPartition ) throws Exception { // '*' for all user attributes, '+' for all operational attributes return computeModifications( originalPartition, modifiedPartition, SchemaConstants.ALL_ATTRIBUTES_ARRAY ); } /** * Compute the difference between two partitions : * <ul> * <li>Added entries</li> * <li>Removed entries</li> * <li>Modified entries * <ul> * <li>Added Attributes</li> * <li>Removed Attributes</li> * <li>Modified Attributes * <ul> * <li>Added Values</li> * <li>Removed Values</li> * </ul> * </li> * </ul> * </li> * </ul> * @param originalPartition The original partition * @param modifiedPartition The modified partition * @param attributeIds The list of attributes we want to compare * @return A list of LDIF Additions, Deletions or Modifications * @throws Exception If something went wrong */ public static List<LdifEntry> computeModifications( Partition originalPartition, Partition modifiedPartition, String[] attributeIds ) throws Exception { return computeModifications( originalPartition, modifiedPartition, originalPartition.getSuffixDn(), attributeIds ); } /** * Compute the actual diff. */ private static List<LdifEntry> computeModifications( Partition originalPartition, Partition modifiedPartition, Dn baseDn, String[] attributeIds ) throws Exception { // Checking partitions checkPartitions( originalPartition, modifiedPartition ); return comparePartitions( originalPartition, modifiedPartition, baseDn, attributeIds ); } /** * Checks the partitions. */ private static void checkPartitions( Partition originalPartition, Partition modifiedPartition ) throws PartitionsDiffException { // Checking the original partition if ( originalPartition == null ) { throw new PartitionsDiffException( "The original partition must not be 'null'." ); } else { if ( !originalPartition.isInitialized() ) { throw new PartitionsDiffException( "The original partition must be intialized." ); } else if ( originalPartition.getSuffixDn() == null ) { throw new PartitionsDiffException( "The original suffix is null." ); } } // Checking the destination partition if ( modifiedPartition == null ) { throw new PartitionsDiffException( "The modified partition must not be 'null'." ); } else { if ( !modifiedPartition.isInitialized() ) { throw new PartitionsDiffException( "The modified partition must be intialized." ); } else if ( modifiedPartition.getSuffixDn() == null ) { throw new PartitionsDiffException( "The modified suffix is null." ); } } } /** * Compare two partitions. * * @param originalPartition The original partition * @param modifiedPartition The modified partition * @param baseDn the Dn from which we will iterate * @param attributeIds the IDs of the attributes we will compare * @return a list containing LDIF entries with all modifications * @throws Exception If something went wrong */ public static List<LdifEntry> comparePartitions( Partition originalPartition, Partition modifiedPartition, Dn baseDn, String[] attributeIds ) throws PartitionsDiffException { // Creating the list containing all the modifications List<LdifEntry> modifications = new ArrayList<LdifEntry>(); try { // Looking up the original base entry Entry originalBaseEntry = originalPartition.lookup( new LookupOperationContext( null, baseDn, attributeIds ) ); if ( originalBaseEntry == null ) { throw new PartitionsDiffException( "Unable to find the base entry in the original partition." ); } // Creating the list containing all the original entries to be processed // and adding it the original base entry. This is done going down the tree. List<Entry> originalEntries = new ArrayList<Entry>(); originalEntries.add( originalBaseEntry ); // Looping until all original entries are being processed. We will read all the children, // adding each of them at the end of the list, consuming the first element of the list // at every iteration. When we have processed all the tree in depth, we should not have // any left entries in the list. // We don't dereference aliases and referrals. while ( originalEntries.size() > 0 ) { // Getting the first original entry from the list Entry originalEntry = originalEntries.remove( 0 ); // Creating a modification entry to hold all modifications LdifEntry ldifEntry = new LdifEntry(); ldifEntry.setDn( originalEntry.getDn() ); // Looking for the equivalent entry in the destination partition Entry modifiedEntry = modifiedPartition.lookup( new LookupOperationContext( null, originalEntry .getDn(), attributeIds ) ); if ( modifiedEntry != null ) { // Setting the changeType to Modify atm ldifEntry.setChangeType( ChangeType.Modify ); // Comparing both entries compareEntries( originalEntry, modifiedEntry, ldifEntry ); } else { // The entry has been deleted from the partition. It has to be deleted. // Note : we *must* delete all of it's children first !!! List<LdifEntry> deletions = deleteEntry( originalPartition, originalEntry.getDn() ); // Append the children modifications.addAll( deletions ); // and add the parent entry ldifEntry.setChangeType( ChangeType.Delete ); // And go on with the remaining entries continue; } // Checking if modifications occurred on the original entry ChangeType modificationEntryChangeType = ldifEntry.getChangeType(); if ( modificationEntryChangeType != ChangeType.None ) { if ( modificationEntryChangeType == ChangeType.Delete || ( modificationEntryChangeType == ChangeType.Modify && ldifEntry .getModifications().size() > 0 ) ) { // Adding the modification entry to the list modifications.add( ldifEntry ); } } // Creating a search operation context to get the children of the current entry SearchOperationContext soc = new SearchOperationContext( null, originalEntry.getDn(), SearchScope.ONELEVEL, FilterParser.parse( originalPartition.getSchemaManager(), LdapConstants.OBJECT_CLASS_STAR ), attributeIds ); soc.setAliasDerefMode( AliasDerefMode.DEREF_ALWAYS ); // Looking for the children of the current entry EntryFilteringCursor cursor = originalPartition.search( soc ); while ( cursor.next() ) { originalEntries.add( cursor.get() ); } } // Now, iterate on the modified partition, to see if some entries have // been added. Entry destinationBaseEntry = modifiedPartition .lookup( new LookupOperationContext( null, baseDn, attributeIds ) ); if ( destinationBaseEntry == null ) { throw new PartitionsDiffException( "Unable to find the base entry in the destination partition." ); } // Creating the list containing all the destination entries to be processed // and adding it the destination base entry List<Entry> modifiedEntries = new ArrayList<Entry>(); modifiedEntries.add( originalBaseEntry ); // Looping until all modified entries are being processed while ( modifiedEntries.size() > 0 ) { // Getting the first modification entry from the list Entry modifiedEntry = modifiedEntries.remove( 0 ); // Looking for the equivalent entry in the destination partition Entry originalEntry = originalPartition.lookup( new LookupOperationContext( null, modifiedEntry .getDn(), attributeIds ) ); // We're only looking for new entries, modified or removed // entries have already been computed if ( originalEntry == null ) { // Creating a modification entry to hold all modifications LdifEntry modificationEntry = new LdifEntry(); modificationEntry.setDn( modifiedEntry.getDn() ); // Setting the changeType to addition modificationEntry.setChangeType( ChangeType.Add ); // Copying attributes for ( Attribute attribute : modifiedEntry ) { modificationEntry.addAttribute( attribute ); } // Adding the modification entry to the list modifications.add( modificationEntry ); } // Creating a search operation context to get the children of the current entry SearchOperationContext soc = new SearchOperationContext( null, modifiedEntry.getDn(), SearchScope.ONELEVEL, FilterParser.parse( originalPartition.getSchemaManager(), LdapConstants.OBJECT_CLASS_STAR ), attributeIds ); soc.setAliasDerefMode( AliasDerefMode.DEREF_ALWAYS ); // Looking for the children of the current entry EntryFilteringCursor cursor = modifiedPartition.search( soc ); while ( cursor.next() ) { modifiedEntries.add( cursor.get() ); } } } catch ( Exception e ) { throw new PartitionsDiffException( e ); } return modifications; } /** * Delete recursively the entries under a parent */ private static List<LdifEntry> deleteEntry( Partition originalPartition, Dn parentDn ) throws LdapException, ParseException, CursorException { List<LdifEntry> deletions = new ArrayList<LdifEntry>(); // Lookup for the children SearchOperationContext soc = new SearchOperationContext( null, parentDn, SearchScope.ONELEVEL, FilterParser.parse( originalPartition.getSchemaManager(), LdapConstants.OBJECT_CLASS_STAR ), SchemaConstants.NO_ATTRIBUTE_ARRAY ); soc.setAliasDerefMode( AliasDerefMode.DEREF_ALWAYS ); // Looking for the children of the current entry EntryFilteringCursor cursor = originalPartition.search( soc ); while ( cursor.next() ) { LdifEntry deletion = new LdifEntry( cursor.get().getDn() ); // Iterate List<LdifEntry> childrenDeletions = deleteEntry( originalPartition, deletion.getDn() ); deletions.addAll( childrenDeletions ); deletions.add( deletion ); } return deletions; } /** * Compares the two given entries. * * @param originalEntry the original entry * @param modifiedEntry the destination entry * @param modificationEntry the modification LDIF entry holding the modifications between both entries * @throws LdapInvalidAttributeValueException */ private static void compareEntries( Entry originalEntry, Entry modifiedEntry, LdifEntry modificationEntry ) throws LdapInvalidAttributeValueException { // We loop on all the attributes of the original entries, to detect the // modified ones and the deleted ones for ( Attribute originalAttribute : originalEntry ) { AttributeType originalAttributeType = originalAttribute.getAttributeType(); // We're only working on 'userApplications' attributes if ( originalAttributeType.getUsage() == UsageEnum.USER_APPLICATIONS ) { Attribute modifiedAttribute = modifiedEntry.get( originalAttributeType ); if ( modifiedAttribute == null ) { // The attribute has been deleted // Creating a modification for the removed AT Modification modification = new DefaultModification( ModificationOperation.REMOVE_ATTRIBUTE, originalAttributeType ); modificationEntry.addModification( modification ); } else { // Comparing both attributes compareAttributes( originalAttribute, modifiedAttribute, modificationEntry ); } } } // Now, check all the modified entry's attributes to see what are the added ones for ( Attribute destinationAttribute : modifiedEntry ) { AttributeType destinationAttributeType = destinationAttribute.getAttributeType(); // We're only working on 'userApplications' attributes if ( destinationAttributeType.getUsage() == UsageEnum.USER_APPLICATIONS ) { // Checking if the current AT is not present in the original entry : if so, // it has been added if ( !originalEntry.containsAttribute( destinationAttributeType ) ) { // Creating a modification for the added AT Modification modification = new DefaultModification( ModificationOperation.ADD_ATTRIBUTE, destinationAttribute ); modificationEntry.addModification( modification ); } } } } /** * Compares two attributes. * * @param originalAttribute the original attribute * @param modifiedAttribute the destination attribute * @param modificationEntry the modification LDIF entry holding the modifications between both attributes */ private static void compareAttributes( Attribute originalAttribute, Attribute modifiedAttribute, LdifEntry modificationEntry ) { // Special case for 'objectClass' attribute, due to a limitation in OpenLDAP // which does not allow us to modify the 'objectClass' attribute if ( !SchemaConstants.OBJECT_CLASS_AT.equalsIgnoreCase( originalAttribute.getAttributeType().getName() ) ) { // Checking if the two attributes are equivalent if ( !originalAttribute.equals( modifiedAttribute ) ) { // Creating a modification for the modified AT values. We do that globally, for all the values // The values should also be ordered if this is required (X-ORDERED) Modification modification = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE, modifiedAttribute ); modificationEntry.addModification( modification ); } } } }