/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * docs/licenses/cddl.txt * or http://www.opensource.org/licenses/cddl1.php. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * docs/licenses/cddl.txt. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Portions Copyright 2014-2024 Ping Identity Corporation */ package com.unboundid.directory.sdk.examples; import com.unboundid.asn1.ASN1OctetString; import com.unboundid.directory.sdk.common.operation.AddRequest; import com.unboundid.directory.sdk.common.operation.DeleteRequest; import com.unboundid.directory.sdk.common.operation.ModifyDNRequest; import com.unboundid.directory.sdk.common.operation.ModifyRequest; import com.unboundid.directory.sdk.common.schema.AttributeType; import com.unboundid.directory.sdk.common.types.LogSeverity; import com.unboundid.directory.sdk.common.types.OperationContext; import com.unboundid.directory.sdk.ds.api.NotificationManager; import com.unboundid.directory.sdk.ds.config.NotificationManagerConfig; import com.unboundid.directory.sdk.ds.types.DirectoryServerContext; import com.unboundid.directory.sdk.ds.types.Notification; import com.unboundid.directory.sdk.ds.types.NotificationChange; import com.unboundid.directory.sdk.ds.types.NotificationDeliveryResult; import com.unboundid.directory.sdk.ds.types.NotificationProperties; import com.unboundid.directory.sdk.proxy.types.Location; import com.unboundid.ldap.sdk.Attribute; import com.unboundid.ldap.sdk.ChangeType; import com.unboundid.ldap.sdk.CompareResult; import com.unboundid.ldap.sdk.DN; import com.unboundid.ldap.sdk.Entry; import com.unboundid.ldap.sdk.LDAPConnection; import com.unboundid.ldap.sdk.LDAPConnectionOptions; import com.unboundid.ldap.sdk.LDAPConnectionPool; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.ldap.sdk.LDAPURL; import com.unboundid.ldap.sdk.Modification; import com.unboundid.ldap.sdk.ResultCode; import com.unboundid.ldap.sdk.unboundidds.UnboundIDChangeLogEntry; import com.unboundid.ldap.sdk.unboundidds.extensions. NotificationDestinationDetails; import com.unboundid.ldap.sdk.unboundidds.extensions. NotificationSubscriptionDetails; import com.unboundid.util.ObjectPair; import com.unboundid.util.StaticUtils; import com.unboundid.util.args.ArgumentParser; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import static java.util.Arrays.asList; /** * This class provides an example of a notification manager that can synchronize * changes out to another directory server. * <BR><BR> * Each notification destination identifies a target directory server. * The connection information for the target directory server is specified * by notification destination details. Each destination * detail value is expected to be a string of the form NAME=VALUE. The * following information can be specified. * <UL> * <LI> * ldapurl=<i>LDAPURL</i><BR> * Only the scheme, host and port of the LDAP URL are allowed. * </LI> * <LI> * binddn=<i>DN</i><BR> * The LDAP simple bind DN * </LI> * <LI> * password=<i>password</i><BR> * The LDAP simple bind password * </LI> * <LI> * location=<i>location</i><BR> * The location of the target server. If this matches the source server * location then the isPreferredForDestination method will return * true for this destination. * </LI> * </UL> * * Each notification subscription specifies one or more LDAP URLs that identify * the base DN and scope of source entries to be synchronized. Each subscription * detail value should be an LDAP URL, where only the base DN, scope and * attributes of the LDAP URL are allowed. For example, * <i>ldap:///ou=people,dc=example,dc=com?uid,mail?sub</i>. * Attributes may be specified in the subscription LDAP URL to cause * additional key attributes to be written to the LDAP changelog and included * in the notification. This is for testing purposes only because this example * does not use these key attribute values when the changes are synchronized * to the target server. */ public class ExampleNotificationManager extends NotificationManager { private static final String DEST_DETAIL_LDAPURL = "ldapurl"; private static final String DEST_DETAIL_BINDDN = "binddn"; private static final String DEST_DETAIL_PASSWORD = "password"; private static final String DEST_DETAIL_LOCATION = "location"; private static final Set<ResultCode> UNLIMITED_RETRY_RESULT_CODES = new LinkedHashSet<ResultCode>(asList( ResultCode.BUSY, ResultCode.UNAVAILABLE, ResultCode.SERVER_DOWN, ResultCode.CONNECT_ERROR)); private static final Set<ResultCode> LIMITED_RETRY_RESULT_CODES = new LinkedHashSet<ResultCode>(asList( ResultCode.OPERATIONS_ERROR, ResultCode.PROTOCOL_ERROR, ResultCode.TIME_LIMIT_EXCEEDED, ResultCode.ADMIN_LIMIT_EXCEEDED, ResultCode.CONSTRAINT_VIOLATION, ResultCode.ALIAS_DEREFERENCING_PROBLEM, ResultCode.UNWILLING_TO_PERFORM, ResultCode.LOOP_DETECT, ResultCode.CANCELED, ResultCode.OTHER, ResultCode.LOCAL_ERROR, ResultCode.ENCODING_ERROR, ResultCode.DECODING_ERROR, ResultCode.TIMEOUT, ResultCode.USER_CANCELED, ResultCode.NO_MEMORY, ResultCode.CLIENT_LOOP, ResultCode.REFERRAL_LIMIT_EXCEEDED, ResultCode.INTERACTIVE_TRANSACTION_ABORTED)); private static final int MAX_LIMITED_RETRY_ATTEMPTS = 3; /** * The human-readable name for this extension. */ private static final String EXTENSION_NAME = "Example Notification Manager"; /** * The handle to the server context for the server in which this extension * is running. */ private DirectoryServerContext serverContext; /** * A map from destination ID to a target LDAP server. */ private volatile Map<String,TargetServer> targetServers = new ConcurrentHashMap<String, TargetServer>(); /** * A map from destination and subscription ID pair to a list of LDAPURLs for * the subscription. */ private volatile Map<ObjectPair<String,String>,List<LDAPURL>> subscriptionURLs = new ConcurrentHashMap<ObjectPair<String, String>, List<LDAPURL>>(); /** * This class represents a target LDAP server. */ class TargetServer { private final LDAPURL ldapURL; private final String bindDN; private final String password; private final String locationName; private AtomicReference<LDAPConnectionPool> connectionPoolRef = new AtomicReference<LDAPConnectionPool>(null); /** * Create a new instance of a target server. * * @param ldapURL The LDAP URL of the target server. * @param bindDN The bind DN to use to bind to the target server. * @param password The password to use to bind to the target server. * @param locationName The location of the target server, or * {@code null} if the location is not defined. */ TargetServer(final LDAPURL ldapURL, final String bindDN, final String password, final String locationName) { this.ldapURL = ldapURL; this.bindDN = bindDN; this.password = password; this.locationName = locationName; } /** * Retrieve the LDAP URL of the target server. * * @return The LDAP URL of the target server. */ public LDAPURL getLdapURL() { return ldapURL; } /** * Retrieve the bind DN to use to bind to the target server. * * @return The bind DN to use to bind to the target server. */ public String getBindDN() { return bindDN; } /** * Retrieve the password to use to bind to the target server. * * @return The password to use to bind to the target server. */ public String getPassword() { return password; } /** * Retrieve the name of the location of the target server, or * {@code null} if the location is not defined. * * @return The name of the location of the target server, or * {@code null} if the location is not defined. */ public String getLocationName() { return locationName; } /** * Retrieve an LDAP connection pool for connections to the target server. * * @return An LDAP connection pool for connections to the target server. * * @throws LDAPException If a connection could not be established. */ public LDAPConnectionPool getConnectionPool() throws LDAPException { LDAPConnectionPool p = connectionPoolRef.get(); if ((p != null) && p.isClosed()) { connectionPoolRef.compareAndSet(p, null); p = null; } if (p == null) { final LDAPConnectionOptions connectionOptions = new LDAPConnectionOptions(); LDAPConnection connection = new LDAPConnection(connectionOptions, getLdapURL().getHost(), getLdapURL().getPort(), getBindDN(), getPassword()); p = new LDAPConnectionPool(connection, 16); if (! connectionPoolRef.compareAndSet(null, p)) { p.close(); p = connectionPoolRef.get(); } } return p; } /** * Performs cleanup when this target server is finished with. */ public void finalizeTargetServer() { final LDAPConnectionPool p = connectionPoolRef.get(); if (p != null) { p.close(); } } } /** * Retrieves a human-readable name for this extension. * * @return A human-readable name for this extension. */ @Override public String getExtensionName() { return EXTENSION_NAME; } /** * Initializes this notification manager. * * @param serverContext A handle to the server context for the server in which * this extension is running. * @param config The general configuration for this notification * manager. * @param parser The argument parser which has been initialized from * the configuration for this notification manager. * * @throws LDAPException If a problem occurs while initializing this * notification manager. */ @Override public void initializeNotificationManager( final DirectoryServerContext serverContext, final NotificationManagerConfig config, final ArgumentParser parser) throws LDAPException { this.serverContext = serverContext; } /** * Performs any cleanup which may be necessary when this notification manager * is to be taken out of service. */ @Override public void finalizeNotificationManager() { for (TargetServer targetServer : targetServers.values()) { targetServer.finalizeTargetServer(); } targetServers = null; subscriptionURLs = null; } /** * Retrieves a human-readable description for this extension. Each element * of the array that is returned will be considered a separate paragraph in * generated documentation. * * @return A human-readable description for this extension, or {@code null} * or an empty array if no description should be available. */ @Override() public String[] getExtensionDescription() { return new String[] { "This notification manager serves an example that may be used " + "to demonstrate the process for creating a third-party " + "notification manager. For each change matching a subscription " + "it will synchronize this change to another directory server." }; } /** * Parse the provided list of destination details. * * @param destinationDetails The notification destination details. * * @return A target LDAP server. */ private TargetServer parseDestinationDetails( final List<ASN1OctetString> destinationDetails ) { LDAPURL ldapURL = null; String bindDN = null; String password = null; String location = null; for (ASN1OctetString s : destinationDetails) { final String detailName; final String detailValue; try { final String components[] = s.stringValue().split(":", 2); detailName = components[0].trim(); detailValue = components[1]; } catch (Exception e) { throw new RuntimeException( String.format("Destination detail '%s' is not of the form " + "NAME=VALUE", s)); } if (detailName.startsWith(DEST_DETAIL_LDAPURL)) { if (ldapURL != null) { throw new RuntimeException("LDAP URL must only be specified once"); } try { ldapURL = new LDAPURL(detailValue); } catch (LDAPException e) { throw new RuntimeException( String.format("'%s' cannot be parsed as an LDAP URL", detailValue)); } if (!ldapURL.getScheme().equals("ldap")) { throw new RuntimeException( "The '" + ldapURL.getScheme() + "' scheme is not supported " + "in a destination LDAP URL"); } if (ldapURL.baseDNProvided() || ldapURL.scopeProvided() || ldapURL.attributesProvided() || ldapURL.filterProvided()) { throw new RuntimeException( "Only the scheme, host and port should be provided in a " + "destination LDAP URL"); } } else if (detailName.startsWith(DEST_DETAIL_BINDDN)) { if (bindDN != null) { throw new RuntimeException("Bind DN must only be specified once"); } bindDN = detailValue; } else if (detailName.startsWith(DEST_DETAIL_PASSWORD)) { if (password != null) { throw new RuntimeException("Bind password must only be " + "specified once"); } password = detailValue; } else if (detailName.startsWith(DEST_DETAIL_LOCATION)) { if (location != null) { throw new RuntimeException("Location must only be specified once"); } location = detailValue; } else { throw new RuntimeException( String.format("Unrecognized destination detail '%s'", s)); } } if (ldapURL == null) { throw new RuntimeException( String.format("Destination details must specify an LDAP URL")); } return new TargetServer(ldapURL, bindDN, password, location); } /** * Initializes or re-initializes the notification destinations for this * notification manager. This will be called once after the notification * manager has been initialized and any time subsequently. * * @param destinations The list of defined notification destinations and * their associated subscriptions. * * @throws LDAPException If a problem occurs while initializing the * notification destinations for this notification * manager. */ @Override public synchronized void initializeNotificationDestinations( final List<NotificationDestinationDetails> destinations) throws LDAPException { final Map<String, TargetServer> newTargetServers = new ConcurrentHashMap<String, TargetServer>(); final Map<ObjectPair<String,String>,List<LDAPURL>> newSubscriptionURLs = new ConcurrentHashMap<ObjectPair<String, String>, List<LDAPURL>>(); for (NotificationDestinationDetails destination : destinations) { final TargetServer targetServer = parseDestinationDetails(destination.getDetails()); newTargetServers.put(destination.getID(), targetServer); for (NotificationSubscriptionDetails subscription : destination.getSubscriptions()) { final List<LDAPURL> ldapurls = new ArrayList<LDAPURL>(subscription.getDetails().size()); for (ASN1OctetString s : subscription.getDetails()) { ldapurls.add(new LDAPURL(s.stringValue())); } final ObjectPair<String, String> subscriptionKey = new ObjectPair<String, String>(destination.getID(), subscription.getID()); newSubscriptionURLs.put(subscriptionKey, ldapurls); } } targetServers = newTargetServers; subscriptionURLs = newSubscriptionURLs; } /** * Determine whether the provided destination details are acceptable. If this * method returns true then it is expected that a call to {@code * setNotificationDestination} with the same details will not fail due to * invalid details. * * @param destinationID The notification destination ID. * @param destinationDetails The notification destination details. * @param unacceptableReasons A list that may be used to hold the reasons that * the provided details are unacceptable. * * @return {@code true} if the provided details are acceptable. */ @Override public boolean areDestinationDetailsAcceptable( final String destinationID, final List<ASN1OctetString> destinationDetails, final List<String> unacceptableReasons) { boolean areAcceptable = true; try { final TargetServer targetServer = parseDestinationDetails(destinationDetails); try { final LDAPConnectionPool pool = targetServer.getConnectionPool(); // Make sure we do not target the server in which this extension is // running. final CompareResult compareResult = pool.compare("", "startupUUID", serverContext.getStartupUUID().toString()); if (compareResult.compareMatched()) { throw new RuntimeException( "The destination directory server is the same server as the " + "one in which this extension is running"); } } catch (LDAPException le) { // The target server is currently unavailable but we'll allow that. } finally { targetServer.finalizeTargetServer(); } } catch (Exception e) { areAcceptable = false; unacceptableReasons.add(e.getMessage()); } return areAcceptable; } /** * Create or update a notification destination. * * @param destinationID The notification destination ID. * @param destinationDetails The notification destination details. * * @throws LDAPException If a problem occurs while creating or updating the * notification destination. */ @Override public synchronized void setNotificationDestination( final String destinationID, final List<ASN1OctetString> destinationDetails) throws LDAPException { final TargetServer targetServer = parseDestinationDetails(destinationDetails); final TargetServer previous = targetServers.put(destinationID, targetServer); if (previous != null) { previous.finalizeTargetServer(); } if (serverContext.debugEnabled()) { if (previous != null) { serverContext.debugVerbose("Updated destination " + destinationID); } else { serverContext.debugVerbose("Added destination " + destinationID); } } } /** * Determine whether the provided destination details are acceptable. If this * method returns true then it is expected that a call to * setNotificationSubscription with the same details will not fail due to * invalid details. * * @param destinationID The notification destination ID. * @param subscriptionID The notification subscription ID. * @param subscriptionDetails The notification subscription details. * @param unacceptableReasons A list that may be used to hold the reasons that * the provided details are unacceptable. * * @return {@code true} if the provided details are acceptable. */ @Override public boolean areSubscriptionDetailsAcceptable( final String destinationID, final String subscriptionID, final List<ASN1OctetString> subscriptionDetails, final List<String> unacceptableReasons) { boolean areAcceptable = true; for (ASN1OctetString s : subscriptionDetails) { LDAPURL ldapURL; try { ldapURL = new LDAPURL(s.stringValue()); } catch (LDAPException e) { areAcceptable = false; unacceptableReasons.add( String.format("Subscription detail '%s' cannot be parsed as " + "an LDAP URL", s.stringValue())); continue; } if (ldapURL.hostProvided() || ldapURL.portProvided() || ldapURL.filterProvided()) { areAcceptable = false; unacceptableReasons.add( "Only base DN, attributes and scope should be provided in a " + "subscription LDAP URL"); } } return areAcceptable; } /** * Create or update a notification subscription. * * @param destinationID The notification destination ID. * @param subscriptionID The notification subscription ID. * @param subscriptionDetails The notification destination details. * * @throws LDAPException If a problem occurs while creating or updating the * notification subscription. */ @Override public synchronized void setNotificationSubscription( final String destinationID, final String subscriptionID, final List<ASN1OctetString> subscriptionDetails) throws LDAPException { final List<LDAPURL> ldapurls = new ArrayList<LDAPURL>(subscriptionDetails.size()); for (ASN1OctetString s : subscriptionDetails) { ldapurls.add(new LDAPURL(s.stringValue())); } final ObjectPair<String, String> subscriptionKey = new ObjectPair<String, String>(destinationID, subscriptionID); final List<LDAPURL> previous = subscriptionURLs.put(subscriptionKey, ldapurls); if (serverContext.debugEnabled()) { if (previous != null) { serverContext.debugVerbose( "Updated subscription " + subscriptionID + " on destination " + destinationID + " with LDAP URLs " + String.valueOf(ldapurls)); } else { serverContext.debugVerbose( "Added subscription " + subscriptionID + " to destination " + destinationID + " with LDAP URLs " + String.valueOf(ldapurls)); } } } /** * Delete a notification destination including any associated * subscriptions. * * @param destinationID The notification destination ID. */ @Override public synchronized void deleteNotificationDestination( final String destinationID) { // Remove all subscriptions associated with the destination. final Iterator<ObjectPair<String,String>> iter = subscriptionURLs.keySet().iterator(); while (iter.hasNext()) { final ObjectPair<String,String> subscriptionKey = iter.next(); if (subscriptionKey.getFirst().equals(destinationID)) { iter.remove(); } } final TargetServer targetServer = targetServers.remove(destinationID); if (targetServer != null) { targetServer.finalizeTargetServer(); } if (serverContext.debugEnabled()) { serverContext.debugVerbose("Deleted destination " + destinationID); } } /** * Delete a notification subscription. * * @param destinationID The notification destination ID. * @param subscriptionID The notification subscription ID. */ @Override public synchronized void deleteNotificationSubscription( final String destinationID, final String subscriptionID) { final ObjectPair<String,String> subscriptionKey = new ObjectPair<String, String>(destinationID, subscriptionID); subscriptionURLs.remove(subscriptionKey); if (serverContext.debugEnabled()) { serverContext.debugVerbose( "Deleted subscription " + subscriptionID + " from destination " + destinationID); } } /** * Determine whether the server running this extension is preferred for the * given notification destination. This method returns {@code true} if * the source server's location is the same as that of the destination, and * {@code false} if the locations are different, or either of the source or * target server's locations are not defined. * * @param destinationID The notification destination ID. * * @return {@code true} if this server is preferred for the given * notification destination. */ @Override public boolean isPreferredForDestination(final String destinationID) { final TargetServer targetServer = targetServers.get(destinationID); final Location location = serverContext.getLocation(); return (targetServer != null && targetServer.getLocationName() != null && location != null && targetServer.getLocationName().equals(location.getName())); } /** * Determine if any notifications are required for the provided add * request and return notification properties for each notification * destination that requires a notification. * * @param addRequest The add request that is being processed. * * @param addedEntry The entry that was added. * * @param opContext The operation context for the current operation. * * @return A list of notification properties with an element for each * notification destination that requires a notification. An * empty or {@code null} list indicates that the operation does not * require any notifications. */ @Override public List<NotificationProperties> getAddNotificationProperties( final AddRequest addRequest, final com.unboundid.directory.sdk.common.types.Entry addedEntry, final OperationContext opContext) { // Return immediately if there are no subscriptions to check. if (subscriptionURLs.isEmpty()) { return null; } // Parse the updated entry's DN. final DN entryDN; try { entryDN = new DN(addRequest.getEntry().getDN()); } catch (LDAPException e) { // Should never happen because the DN should always be valid. return null; } return getNotificationProperties(entryDN); } /** * Determine if any notifications are required for the provided delete * request and return notification properties for each notification * destination that requires a notification. * * @param deleteRequest The delete request that is being processed. * * @param deletedEntry The entry against which the delete operation was * processed, if available, and {@code null} otherwise. * * @param opContext The operation context for the current operation. * * @return A list of notification properties with an element for each * notification destination that requires a notification. An * empty or {@code null} list indicates that the operation does not * require any notifications. */ @Override public List<NotificationProperties> getDeleteNotificationProperties( final DeleteRequest deleteRequest, final com.unboundid.directory.sdk.common.types.Entry deletedEntry, final OperationContext opContext) { // Return immediately if there are no subscriptions to check. if (subscriptionURLs.isEmpty()) { return null; } // Parse the updated entry's DN. final DN entryDN; try { entryDN = new DN(deleteRequest.getDN()); } catch (LDAPException e) { // Should never happen because the DN should always be valid. return null; } return getNotificationProperties(entryDN); } /** * Determine if any notifications are required for the provided modify * request and return notification properties for each notification * destination that requires a notification. * * @param modifyRequest The modify request that is being processed. * * @param oldEntry The entry as it appeared before the modify operation, * or {@code null} if it is not available. * * @param newEntry The entry as it appeared after the modify operation, * or {@code null} if it is not available. * * @param opContext The operation context for the current operation. * * @return A list of notification properties with an element for each * notification destination that requires a notification. An * empty or {@code null} list indicates that the operation does not * require any notifications. */ @Override public List<NotificationProperties> getModifyNotificationProperties( final ModifyRequest modifyRequest, final com.unboundid.directory.sdk.common.types.Entry oldEntry, final com.unboundid.directory.sdk.common.types.Entry newEntry, final OperationContext opContext) { // Return immediately if there are no subscriptions to check. if (subscriptionURLs.isEmpty()) { return null; } // Parse the updated entry's DN. final DN entryDN; try { entryDN = new DN(modifyRequest.getDN()); } catch (LDAPException e) { // Should never happen because the DN should always be valid. return null; } return getNotificationProperties(entryDN); } /** * Determine if any notifications are required for the provided modify DN * request and return notification properties for each notification * destination that requires a notification. * * @param modifyDNRequest The modify DN request that is being processed. * * @param oldEntry The entry as it appeared before the modify DN * operation, or {@code null} if it is not available. * * @param newEntry The entry as it appeared after the modify DN * operation, or {@code null} if it is not available. * * @param opContext The operation context for the current operation. * * @return A list of notification properties with an element for each * notification destination that requires a notification. An * empty or {@code null} list indicates that the operation does not * require any notifications. */ @Override public List<NotificationProperties> getModifyDNNotificationProperties( final ModifyDNRequest modifyDNRequest, final com.unboundid.directory.sdk.common.types.Entry oldEntry, final com.unboundid.directory.sdk.common.types.Entry newEntry, final OperationContext opContext) { // Return immediately if there are no subscriptions to check. if (subscriptionURLs.isEmpty()) { return null; } // Parse the updated entry's DN (before the update). final DN entryDN; try { entryDN = new DN(modifyDNRequest.getDN()); } catch (LDAPException e) { // Should never happen because the DN should always be valid. return null; } return getNotificationProperties(entryDN); } /** * Attempt delivery of a notification. In this example, we attempt to apply * the changes in the notification to another LDAP server. * * @param notification The notification to be delivered. * * @return A delivery result indicating whether delivery was successful, * and whether delivery should be retried if this attempt was * unsuccessful. */ @Override public NotificationDeliveryResult attemptDelivery( final Notification notification) { final TargetServer targetServer = targetServers.get(notification.getDestinationID()); if (targetServer == null) { serverContext.logMessage(LogSeverity.SEVERE_ERROR, "Delivery attempt failed, no destination with ID " + notification.getDestinationID()); return NotificationDeliveryResult.FAILURE; } try { final LDAPConnectionPool pool = targetServer.getConnectionPool(); for (NotificationChange c : notification.getNotificationChanges()) { final UnboundIDChangeLogEntry changeLogEntry = c.getChangeLogEntry(); if (changeLogEntry.getChangeType().equals(ChangeType.DELETE)) { pool.delete(changeLogEntry.getTargetDN()); } else if (changeLogEntry.getChangeType().equals(ChangeType.ADD)) { final Entry partialEntryAfterChange = changeLogEntry.constructPartialEntryAfterChange(false). duplicate(); // Remove attributes that are NO-USER-MODIFICATION. final List<Attribute> toRemove = new ArrayList<Attribute>(); for (Attribute a : partialEntryAfterChange.getAttributes()) { final AttributeType attributeType = serverContext.getSchema().getAttributeType(a.getName(), false); if (attributeType != null && attributeType.isNoUserModification()) { toRemove.add(a); } } for (final Attribute a : toRemove) { partialEntryAfterChange.removeAttribute(a.getName()); } pool.add(partialEntryAfterChange); } else if (changeLogEntry.getChangeType().equals(ChangeType.MODIFY_DN)) { pool.modifyDN(changeLogEntry.getTargetDN(), changeLogEntry.getNewRDN(), changeLogEntry.deleteOldRDN(), changeLogEntry.getNewSuperior()); } else if (changeLogEntry.getChangeType().equals(ChangeType.MODIFY)) { // Remove modifications to attributes that are NO-USER-MODIFICATION. final List<Modification> modifications = new ArrayList<Modification>(changeLogEntry.getModifications()); final Iterator<Modification> iterator = modifications.iterator(); while (iterator.hasNext()) { final Modification m = iterator.next(); final AttributeType attributeType = serverContext.getSchema().getAttributeType( m.getAttributeName(), false); if (attributeType != null && attributeType.isNoUserModification()) { iterator.remove(); } } if (modifications.isEmpty()) { serverContext.logMessage(LogSeverity.SEVERE_WARNING, "Skipping MODIFY for notification changelog entry " + "containing only modifications to NO-USER-MODIFICATION " + "attributes: " + changeLogEntry); } else { pool.modify(changeLogEntry.getTargetDN(), modifications); } } } } catch (LDAPException e) { serverContext.logMessage(LogSeverity.SEVERE_ERROR, "Delivery attempt failed: " + StaticUtils.getExceptionMessage(e)); if (UNLIMITED_RETRY_RESULT_CODES.contains(e.getResultCode())) { return NotificationDeliveryResult.RETRY; } else if (LIMITED_RETRY_RESULT_CODES.contains(e.getResultCode())) { if (notification.getNumPreviousDeliveryAttempts() + 1 < MAX_LIMITED_RETRY_ATTEMPTS) { return NotificationDeliveryResult.RETRY; } else { return NotificationDeliveryResult.FAILURE; } } else { return NotificationDeliveryResult.FAILURE; } } return NotificationDeliveryResult.SUCCESS; } /** * Determine if any notifications are required for the provided entry DN * and return notification properties for each notification destination * that requires a notification. * * @param entryDN The entry DN of the entry that is the target of an update. * * @return A list of notification properties with an element for each * notification destination that requires a notification. An * empty or {@code null} list indicates that the operation does not * require any notifications. */ private List<NotificationProperties> getNotificationProperties( final DN entryDN) { if (serverContext.debugEnabled()) { serverContext.debugVerbose( "Checking " + subscriptionURLs.size() + " subscription(s) in " + targetServers.size() + " destination(s) for a match on entry " + entryDN.toString()); } // Check all the subscriptions for a match. final List<NotificationProperties> propertiesList = new ArrayList<NotificationProperties>(); for (Map.Entry<ObjectPair<String,String>,List<LDAPURL>> mapEntry : subscriptionURLs.entrySet()) { final ObjectPair<String,String> subscriptionKey = mapEntry.getKey(); final List<LDAPURL> ldapurls = mapEntry.getValue(); for (LDAPURL ldapurl : ldapurls) { // Check if the modified entry is within the base DN and scope of // the LDAP URL. boolean inScope = false; try { inScope = entryDN.matchesBaseAndScope(ldapurl.getBaseDN(), ldapurl.getScope()); } catch (LDAPException e) { // This can only happen if the scope is not supported. } if (!inScope) { if (serverContext.debugEnabled()) { serverContext.debugVerbose( "Entry '" + entryDN.toString() + "' did not match subscription LDAPURL " + ldapurl.toString()); } continue; } if (serverContext.debugEnabled()) { serverContext.debugVerbose( "Entry '" + entryDN.toString() + "' matched subscription LDAPURL " + ldapurl.toString()); } final HashMap<String, String> propertiesMap = new HashMap<String, String>(); propertiesMap.put("subscription", subscriptionKey.getSecond()); final HashSet<AttributeType> keyAttributes = new HashSet<AttributeType>(ldapurl.getAttributes().length); for (String keyAttr : ldapurl.getAttributes()) { final AttributeType attributeType = serverContext.getSchema().getAttributeType(keyAttr, false); if (attributeType != null) { keyAttributes.add(attributeType); } } propertiesList.add(new NotificationProperties( subscriptionKey.getFirst(), propertiesMap, keyAttributes)); } } return propertiesList; } }