/*
* 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
*
*
* Copyright 2014-2016 UnboundID Corp.
*/
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.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></LI>
* Only the scheme, host and port of the LDAP URL are allowed.
* <LI>binddn=<i>DN</i></LI>
* The LDAP simple bind DN
* <LI>password=<i>password</i></LI>
* The LDAP simple bind password
* <LI>location=<i>location</i></LI>
* The location of the target server. If this matches the source server
* location then the isPreferredForDestination method will return
* true for this destination.
* </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.
*/
public 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.
*
* @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)
{
// 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.
*
* @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)
{
// 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.
*
* @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)
{
// 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.
*
* @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)
{
// 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;
}
}
|