/*
* 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 2017-2020 Ping Identity Corporation
*/
package com.unboundid.directory.sdk.examples;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.unboundid.directory.sdk.common.schema.AttributeType;
import com.unboundid.directory.sdk.common.operation.AddRequest;
import com.unboundid.directory.sdk.common.operation.AddResult;
import com.unboundid.directory.sdk.common.operation.ModifyRequest;
import com.unboundid.directory.sdk.common.operation.ModifyResult;
import com.unboundid.directory.sdk.common.operation.UpdatableAddRequest;
import com.unboundid.directory.sdk.common.operation.UpdatableAddResult;
import com.unboundid.directory.sdk.common.operation.UpdatableModifyRequest;
import com.unboundid.directory.sdk.common.operation.UpdatableModifyResult;
import com.unboundid.directory.sdk.common.types.ActiveOperationContext;
import com.unboundid.directory.sdk.common.types.AlertSeverity;
import com.unboundid.directory.sdk.common.types.CompletedOperationContext;
import com.unboundid.directory.sdk.common.types.Entry;
import com.unboundid.directory.sdk.common.types.InternalConnection;
import com.unboundid.directory.sdk.common.types.OperationContext;
import com.unboundid.directory.sdk.ds.api.Plugin;
import com.unboundid.directory.sdk.ds.config.PluginConfig;
import com.unboundid.directory.sdk.ds.types.DirectoryServerContext;
import com.unboundid.directory.sdk.ds.types.PostResponsePluginResult;
import com.unboundid.directory.sdk.ds.types.PreOperationPluginResult;
import com.unboundid.directory.sdk.ds.types.PreParsePluginResult;
import com.unboundid.ldap.sdk.Attribute;
import com.unboundid.ldap.sdk.DereferencePolicy;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.Filter;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPSearchException;
import com.unboundid.ldap.sdk.Modification;
import com.unboundid.ldap.sdk.ModificationType;
import com.unboundid.ldap.sdk.RDN;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
import com.unboundid.ldap.sdk.SearchScope;
import com.unboundid.ldap.sdk.unboundidds.jsonfilter.ANDJSONObjectFilter;
import com.unboundid.ldap.sdk.unboundidds.jsonfilter.EqualsAnyJSONObjectFilter;
import com.unboundid.ldap.sdk.unboundidds.jsonfilter.EqualsJSONObjectFilter;
import com.unboundid.ldap.sdk.unboundidds.jsonfilter.JSONObjectFilter;
import com.unboundid.ldap.sdk.unboundidds.jsonfilter.NegateJSONObjectFilter;
import com.unboundid.util.StaticUtils;
import com.unboundid.util.args.ArgumentException;
import com.unboundid.util.args.ArgumentParser;
import com.unboundid.util.args.BooleanValueArgument;
import com.unboundid.util.args.DNArgument;
import com.unboundid.util.args.FilterArgument;
import com.unboundid.util.args.StringArgument;
import com.unboundid.util.json.JSONArray;
import com.unboundid.util.json.JSONObject;
import com.unboundid.util.json.JSONValue;
/**
* The JSON Field Uniqueness Plugin can be used to enforce uniqueness
* constraints for values of a specified JSON field stored in a given LDAP
* attribute. Uniqueness can be enforced across entries so that the same value
* will not be allowed to appear in two different entries. It can also be
* enforced within the same entry, so that each value of the JSON field (whether
* in separate values of the LDAP attribute or within a JSON array in the same
* LDAP attribute value).
* <BR><BR>
* This plugin can be used in either the Directory Server or the Directory Proxy
* Server, although it is not recommended to be configured in both types of
* servers in the same topology. In deployments in which each Directory Server
* instance contains a complete copy of all the data (regardless of whether
* requests to those instances pass through the Directory Proxy Server), the
* plugin should be configured in all Directory Server instances as a
* pre-operation and post-synchronization plugin for add and modify operations,
* and it should not be configured in any Directory Proxy Server instances. For
* each add and modify request received from an external client, the plugin will
* check to see if that operation includes any changes involving the target JSON
* field within the appropriate LDAP attribute, and will reject any change that
* would introduce a conflict with a value that already exists in another entry,
* or that includes duplicate values within the same entry if that is not
* allowed. It will also examine each add and modify operation that is
* replicated from another server, and if that operation introduced a conflict
* (which may arise if two conflicting requests are processed by different
* servers at the same time), the plugin will generate an alert to notify
* administrators of the conflict so that it can be addressed by manual
* interaction.
* <BR><BR>
* In deployments that use the Directory Proxy Server to separate data across
* multiple servers, whether through entry balancing or by separating different
* portions of the DIT across different sets of servers, the plugin should be
* configured in all Directory Proxy Server instances as a pre-parse and
* post-response plugin for add and modify operations, and it should not be
* configured in any of the Directory Server instances. For each add and modify
* request received by the Directory Proxy Server, the plugin will reject any
* request that attempts to introduce a conflicting value for the target JSON
* field. It will also check for conflicts after each of those operations have
* completed (to ensure that no conflicts were introduced by two conflicting
* requests processed by different servers at the same time), and the plugin
* will generate an alert to notify administrators of each conflict identified
* so that they can be addressed by manual interaction.
*/
public class JSONFieldUniquenessPlugin
extends Plugin
{
/**
* The parent DN for all JSON attribute constraints definitions contained in
* the server configuration.
*/
private static final DN JSON_ATTRIBUTE_CONSTRAINTS_PARENT_DN = new DN(
new RDN("cn", "JSON Attribute Constraints"),
new RDN("cn", "Cluster"),
new RDN("cn", "config"));
/**
* The set of expected plugin types for a plugin that is running in the
* Directory Server.
*/
private final Set<String> EXPECTED_DIRECTORY_PLUGIN_TYPES =
Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
"preOperationAdd".toLowerCase(),
"preOperationModify".toLowerCase(),
"postSynchronizationAdd".toLowerCase(),
"postSynchronizationModify".toLowerCase())));
/**
* The set of expected plugin types for a plugin that is running in the
* Directory Proxy Server.
*/
private final Set<String> EXPECTED_PROXY_PLUGIN_TYPES =
Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(
"preParseAdd".toLowerCase(),
"preParseModify".toLowerCase(),
"postResponseAdd".toLowerCase(),
"postResponseModify".toLowerCase())));
/**
* The name of the required argument used to specify the name or OID of the
* LDAP attribute for which uniqueness is to be maintained.
*/
private static final String ARG_ATTR_NAME = "ldap-attribute-name";
/**
* The name of the required argument used to specify the path to the JSON
* field for which uniqueness is to be maintained.
*/
private static final String ARG_JSON_FIELD_PATH = "json-field-path";
/**
* The name of the optional argument used to indicate whether the plugin
* should ensure that values of the target field must not conflict with values
* for the same field across other entries within the branches indicated by
* the configured base DN(s).
*/
private static final String ARG_REQUIRE_UNIQUENESS_ACROSS_ENTRIES =
"require-uniqueness-across-entries";
/**
* The name of the optional argument used to indicate whether the plugin
* should ensure that multiple values of the target field within the same
* entry will be required to be unique.
*/
private static final String ARG_REQUIRE_UNIQUENESS_WITHIN_AN_ENTRY =
"require-uniqueness-within-an-entry";
/**
* The name of the optional argument used to specify the base DN(s) that will
* be used for searches to find conflicting entries.
*/
private static final String ARG_BASE_DN = "entry-base-dn";
/**
* The name of the optional argument used to provide an LDAP search filter
* that indicates the set of entries for which uniqueness must be enforced.
*/
private static final String ARG_UNIQUE_ENTRY_LDAP_FILTER =
"unique-entry-ldap-filter";
/**
* The name of the optional argument used to provide a JSON object filter that
* a JSON object contained in a value of the specified LDAP attribute will
* be required to match before applying the uniqueness constraint to it.
*/
private static final String ARG_UNIQUE_VALUE_JSON_OBJECT_FILTER =
"unique-value-json-object-filter";
/**
* The name of the attribute in JSON field constraints definitions that
* indicates whether the associated JSON field is indexed.
*/
private static final String ATTR_JSON_FIELD_CONSTRAINTS_INDEX_VALUES =
"ds-cfg-index-values";
/**
* The OID for the JSONObject syntax, which is the required syntax for the
* configured LDAP attribute type.
*/
private static final String OID_JSON_OBJECT_SYNTAX =
"1.3.6.1.4.1.30221.2.3.4";
// The attribute type for which to enforce uniqueness.
private volatile AttributeType ldapAttributeType;
// Indicates whether to require uniqueness across entries.
private volatile boolean requireUniquenessAcrossEntries;
// Indicates whether to require uniqueness across multiple values in the same
// entry.
private volatile boolean requireUniquenessWithinAnEntry;
// The server context to use during processing.
private volatile DirectoryServerContext serverContext;
// An LDAP filter that identifies which entries should be subject to
// uniqueness constraints.
private volatile Filter uniqueEntryLDAPFilter;
// An connection that may be used to perform internal operations within the
// server.
private volatile InternalConnection internalConnection;
// A JSON object filter that identifies which attribute values should be
// subject to uniqueness constraints.
private volatile JSONObjectFilter uniqueValueJSONObjectFilter;
// The base DNs for subtrees in which to enforce uniqueness.
private volatile List<DN> baseDNs;
// The path to the JSON field for which to enforce uniqueness.
private volatile List<String> jsonFieldPath;
// The configuration for this plugin.
private volatile PluginConfig config;
// The string representation of the JSON field path.
private volatile String jsonFieldPathString;
/**
* Creates a new instance of this plugin.
*/
public JSONFieldUniquenessPlugin()
{
ldapAttributeType = null;
jsonFieldPath = null;
jsonFieldPathString = null;
requireUniquenessWithinAnEntry = true;
requireUniquenessAcrossEntries = true;
baseDNs = null;
uniqueEntryLDAPFilter = null;
uniqueValueJSONObjectFilter = null;
serverContext = null;
internalConnection = null;
config = null;
}
/**
* Retrieves the name for this extension.
*
* @return The name for this extension.
*/
@Override()
public String getExtensionName()
{
return "JSON Field Uniqueness Plugin";
}
/**
* Retrieves a description for this extension.
*
* @return A description for this extension.
*/
@Override()
public String[] getExtensionDescription()
{
return new String[]
{
"The JSON Field Uniqueness Plugin can be used to enforce uniqueness " +
"constraints for values of a specified JSON field stored in a " +
"given LDAP attribute. Uniqueness can be enforced across entries " +
"so that the same value will not be allowed to appear in two " +
"different entries. It can also be enforced within the same " +
"entry, so that each value of the JSON field (whether in separate " +
"values of the LDAP attribute or within a JSON array in the same " +
"LDAP attribute value).",
"This plugin can be used in either the Directory Server or the " +
"Directory Proxy Server, although it is not recommended to be " +
"configured in both types of servers in the same topology. In " +
"deployments in which each Directory Server instance contains a " +
"complete copy of all the data (regardless of whether requests to " +
"those instances pass through the Directory Proxy Server), the " +
"plugin should be configured in all Directory Server instances as " +
"a pre-operation and post-synchronization plugin for add and " +
"modify operations, and it should not be configured in any " +
"Directory Proxy Server instances. For each add and modify " +
"request received from an external client, the plugin will check " +
"to see if that operation includes any changes involving the " +
"target JSON field within the appropriate LDAP attribute, and " +
"will reject any change that would introduce a conflict with a " +
"value that already exists in another entry, or that includes " +
"duplicate values within the same entry if that is not allowed. " +
"It will also examine each add and modify operation that is " +
"replicated from another server, and if that operation introduced " +
"a conflict (which may arise if two conflicting requests are " +
"processed by different servers at the same time), the plugin " +
"will generate an alert to notify administrators of the conflict " +
"so that it can be addressed by manual interaction.",
"In deployments that use the Directory Proxy Server to separate data " +
"across multiple servers, whether through entry balancing or by " +
"separating different portions of the DIT across different sets " +
"of servers, the plugin should be configured in all Directory " +
"Proxy Server instances as a pre-parse and post-response plugin " +
"for add and modify operations, and it should not be configured " +
"in any of the Directory Server instances. For each add and " +
"modify request received by the Directory Proxy Server, the " +
"plugin will reject any request that attempts to introduce a " +
"conflicting value for the target JSON field. It will also check " +
"for conflicts after each of those operations have completed (to " +
"ensure that no conflicts were introduced by two conflicting " +
"requests processed by different servers at the same time), and " +
"the plugin will generate an alert to notify administrators of " +
"each conflict identified so that they can be addressed by " +
"manual interaction."
};
}
/**
* Updates the provided argument parser to include the arguments for this
* extension.
*
* @param parser The argument parser to update.
*/
@Override()
public void defineConfigArguments(final ArgumentParser parser)
throws ArgumentException
{
parser.addArgument(new StringArgument(null, ARG_ATTR_NAME, true, 1,
"{attribute}",
"The name or OID of the LDAP attribute type whose values will be " +
"examined for uniqueness conflicts. This attribute type must " +
"be defined with a JSON Object syntax (OID " +
"1.3.6.1.4.1.30221.2.3.4) in the schema for all Directory " +
"Server and Directory Proxy Server instances in the topology. " +
"All Directory Server instances must be configured with a JSON " +
"object constraints definition for this attribute type. This " +
"is a required argument."));
parser.addArgument(new StringArgument(null, ARG_JSON_FIELD_PATH, true, 1,
"{path}",
"The path to the JSON field for which uniqueness will be enforced. " +
"For a top-level field, the path should simply be the name of " +
"that field. For a nested field (a field contained in a JSON " +
"object that is itself the value of a field in an outer JSON " +
"object), the path to the field should be constructed by " +
"concatenating the names of the fields in that path, in order " +
"from outermost to innermost, and separating them with " +
"periods. For example, if a JSON object has a top-level field " +
"named \"contact-info\" whose value is a JSON object that may " +
"contain an \"email-address\" field, then the json-field-path " +
"value for that field would be " +
"\"contact-info.email-address\". If the name of any field " +
"in the path contains one or more periods, those periods " +
"should be escaped by preceding them with a backslash so that " +
"the only unescaped periods are those used as delimiters " +
"between field names. All Directory Server instances must be " +
"configured with a JSON field constraints definition for this " +
"field, and that definition must have the index-values " +
"property set to true. JSON field names will be treated in " +
"a case-sensitive manner. This is a required argument."));
parser.addArgument(new BooleanValueArgument(null,
ARG_REQUIRE_UNIQUENESS_ACROSS_ENTRIES, false, "{true|false}",
"Indicates whether the plugin should check for uniqueness conflicts " +
"across separate entries. If this is true, or if it is " +
"omitted from the configuration, then the plugin will reject " +
"any add or modify request that attempts to set one or more " +
"values for the target JSON field if any of those values is " +
"already in use in any other entry, and it will raise an " +
"administrative alert for each conflict discovered after the " +
"change has already been applied. If this is false, then the " +
"plugin will not attempt to identify uniqueness conflicts in " +
"separate entries. At least one of the " +
"require-uniqueness-across-entries and " +
"require-uniqueness-within-an-entry properties must be set to " +
"true.",
Boolean.TRUE));
parser.addArgument(new BooleanValueArgument(null,
ARG_REQUIRE_UNIQUENESS_WITHIN_AN_ENTRY, false, "{true|false}",
"Indicates whether the plugin should check for uniqueness conflicts " +
"within the same entry. If this is true, or if it is omitted " +
"from the configuration, then the plugin will reject any add " +
"or modify request that attempts to set one or more values for " +
"the target JSON field if any of those values is already in " +
"use in the same entry, whether in separate value for the " +
"LDAP attribute, or in another value in a JSON array. If this " +
"is false, then the plugin will not attempt to identify " +
"uniqueness conflicts within the same entry. At least one of " +
"the require-uniqueness-across-entries and " +
"require-uniqueness-within-an-entry properties must be set to " +
"true.",
Boolean.TRUE));
parser.addArgument(new DNArgument(null, ARG_BASE_DN, false, 0, "{dn}",
"The base DN for a subtree in which the plugin will enforce " +
"uniqueness constraints. This can be provided multiple times " +
"to specify multiple base DNs, and if it is not specified, " +
"then the plugin will use a default set of base DNs that is " +
"the set of public naming contexts for the server. The " +
"plugin will only examine add and modify requests that target " +
"entries at or below one of these base DNs, and if uniqueness " +
"is to be enforced across entries, it will ignore any " +
"conflicts that may be identified outside any of the " +
"configured base DNs."));
parser.addArgument(new FilterArgument(null, ARG_UNIQUE_ENTRY_LDAP_FILTER,
false, 1, "{ldap-filter}",
"An optional LDAP search filter that may be used to identify the " +
"set of entries for which uniqueness should be enforced. If a " +
"filter is configured, then the plugin will ignore any add or " +
"modify requests that target entries that do not match this " +
"filter, and if uniqueness is to be enforced across entries, " +
"it will ignore conflicts that may be identified in entries " +
"that do not match this filter."));
parser.addArgument(new StringArgument(null,
ARG_UNIQUE_VALUE_JSON_OBJECT_FILTER, false, 1, "{json-object-filter}",
"An optional JSON object filter that may be used to identify which " +
"values of a multivalued attribute should be subject to the " +
"uniqueness constraints. If a unique value JSON object " +
"filter is configured, then the plugin will ignore field " +
"values for JSON objects that do not match this filter."));
}
/**
* Initializes this plugin.
*
* @param serverContext A handle to the server context for the server in
* which this extension is running.
* @param config The general configuration for this plugin.
* @param parser The argument parser which has been initialized from
* the configuration for this plugin.
*
* @throws LDAPException If a problem occurs while initializing this plugin.
*/
@Override()
public void initializePlugin(final DirectoryServerContext serverContext,
final PluginConfig config,
final ArgumentParser parser)
throws LDAPException
{
this.serverContext = serverContext;
this.config = config;
// Make sure that we have an internal connection to use for processing
// internal operations.
if (internalConnection == null)
{
internalConnection = serverContext.getInternalRootConnection();
}
// Use the applyConfiguration method to set up the plugin.
final ArrayList<String> adminActionsRequired = new ArrayList<String>(5);
final ArrayList<String> messages = new ArrayList<String>(5);
final ResultCode resultCode = applyConfiguration(config, parser,
adminActionsRequired, messages);
if (resultCode != ResultCode.SUCCESS)
{
throw new LDAPException(resultCode,
"One or more errors occurred while trying to initialize the JSON " +
"field uniqueness plugin using the configuration contained " +
"in '" + config.getConfigObjectDN() + "': " +
StaticUtils.concatenateStrings(messages));
}
}
/**
* Performs any processing which may be necessary before the server starts
* processing for an add request. This will be invoked only for add
* operations requested directly by clients, but not for add operations
* received from another server via replication.
*
* @param operationContext The context for the add operation.
* @param request The add request to be processed. It may be
* altered if desired.
* @param result The result that will be returned to the client if
* the plugin result indicates that processing on
* the operation should be interrupted. It may be
* altered if desired.
*
* @return Information about the result of the plugin processing.
*/
@Override()
public PreParsePluginResult doPreParse(
final ActiveOperationContext operationContext,
final UpdatableAddRequest request,
final UpdatableAddResult result)
{
try
{
ensureUniqueness(request.getEntry().toLDAPSDKEntry(), serverContext,
operationContext, config, internalConnection, ldapAttributeType,
jsonFieldPathString, jsonFieldPath, requireUniquenessWithinAnEntry,
requireUniquenessAcrossEntries, baseDNs, uniqueEntryLDAPFilter,
uniqueValueJSONObjectFilter);
return PreParsePluginResult.SUCCESS;
}
catch (final LDAPException le)
{
serverContext.debugCaught(le);
result.setResultData(le);
return new PreParsePluginResult(false, false, true, true);
}
}
/**
* Performs any processing which may be necessary before the server actually
* attempts to add an entry to the appropriate backend. This will be invoked
* only for add operations requested directly by clients, but not for add
* operations received from another server via replication.
*
* @param operationContext The context for the add operation.
* @param request The add request to be processed.
* @param result The result that will be returned to the client if
* the plugin result indicates that processing on
* the operation should be interrupted. It may be
* altered if desired.
*
* @return Information about the result of the plugin processing.
*/
@Override()
public PreOperationPluginResult doPreOperation(
final ActiveOperationContext operationContext,
final AddRequest request, final UpdatableAddResult result)
{
try
{
ensureUniqueness(request.getEntry().toLDAPSDKEntry(), serverContext,
operationContext, config, internalConnection, ldapAttributeType,
jsonFieldPathString, jsonFieldPath, requireUniquenessWithinAnEntry,
requireUniquenessAcrossEntries, baseDNs, uniqueEntryLDAPFilter,
uniqueValueJSONObjectFilter);
return PreOperationPluginResult.SUCCESS;
}
catch (final LDAPException le)
{
serverContext.debugCaught(le);
result.setResultData(le);
return new PreOperationPluginResult(false, false, true, true);
}
}
/**
* Performs any processing which may be necessary after all other processing
* has been completed for an add operation and the response has been sent to
* the client. This will be invoked only for add operations requested
* directly by clients, but not for add operations received from another
* server via replication.
*
* @param operationContext The context for the add operation.
* @param request The add request that was processed.
* @param result The result that was returned to the client.
*
* @return Information about the result of the plugin processing.
*/
@Override()
public PostResponsePluginResult doPostResponse(
final CompletedOperationContext operationContext,
final AddRequest request, final AddResult result)
{
try
{
ensureUniqueness(request.getEntry().toLDAPSDKEntry(), serverContext,
operationContext, config, internalConnection, ldapAttributeType,
jsonFieldPathString, jsonFieldPath, requireUniquenessWithinAnEntry,
requireUniquenessAcrossEntries, baseDNs, uniqueEntryLDAPFilter,
uniqueValueJSONObjectFilter);
}
catch (final LDAPException le)
{
serverContext.debugCaught(le);
// This means that a replicated operation generated a conflict. We can't
// do anything to prevent this, but we can generate an administrative
// alert to notify administrators of the problem.
serverContext.sendAlert(AlertSeverity.ERROR, le.getMessage());
}
return PostResponsePluginResult.SUCCESS;
}
/**
* Performs any processing which may be necessary after all other processing
* has been completed for an add operation which has been received from
* another server via replication.
*
* @param operationContext The context for the add operation.
* @param request The add request that was processed.
* @param result The result that was returned to the client.
*/
@Override()
public void doPostReplication(
final CompletedOperationContext operationContext,
final AddRequest request, final AddResult result)
{
try
{
ensureUniqueness(request.getEntry().toLDAPSDKEntry(), serverContext,
operationContext, config, internalConnection, ldapAttributeType,
jsonFieldPathString, jsonFieldPath, requireUniquenessWithinAnEntry,
requireUniquenessAcrossEntries, baseDNs, uniqueEntryLDAPFilter,
uniqueValueJSONObjectFilter);
}
catch (final LDAPException le)
{
serverContext.debugCaught(le);
// This means that a replicated operation generated a conflict. We can't
// do anything to prevent this, but we can generate an administrative
// alert to notify administrators of the problem.
serverContext.sendAlert(AlertSeverity.ERROR, le.getMessage());
}
}
/**
* Performs any processing which may be necessary before the server starts
* processing for a modify request. This will be invoked only for modify
* operations requested directly by clients, but not for modify operations
* received from another server via replication.
*
* @param operationContext The context for the modify operation.
* @param request The modify request to be processed. It may be
* altered if desired.
* @param result The result that will be returned to the client if
* the plugin result indicates that processing on
* the operation should be interrupted. It may be
* altered if desired.
*
* @return Information about the result of the plugin processing.
*/
@Override()
public PreParsePluginResult doPreParse(
final ActiveOperationContext operationContext,
final UpdatableModifyRequest request,
final UpdatableModifyResult result)
{
// We aren't given access to the entry (either before or after the changes
// are applied), so we'll need to fetch it from a backend server and apply
// the changes to it so we'll have a representation of the updated entry for
// examination. But we'll only do that if the request could result in a
// conflict, since otherwise retrieving the entry would be wasted effort.
//
// First, we want to make sure that we're dealing with a consistent
// configuration, so get all of the configuration properties first.
final AttributeType attributeType = ldapAttributeType;
final List<String> fieldPath = jsonFieldPath;
final String fieldPathString = jsonFieldPathString;
final boolean uniqueWithinEntry = requireUniquenessWithinAnEntry;
final boolean uniqueAcrossEntries = requireUniquenessAcrossEntries;
final Filter uniqueEntryFilter = uniqueEntryLDAPFilter;
final JSONObjectFilter uniqueValueFilter = uniqueValueJSONObjectFilter;
if (! couldCreateConflict(request, attributeType, baseDNs, fieldPath))
{
return PreParsePluginResult.SUCCESS;
}
// Retrieve the entry from a backend server.
final SearchResultEntry currentEntry;
try
{
currentEntry = internalConnection.getEntry(request.getDN(), "*", "+");
if (currentEntry == null)
{
result.setResultCode(ResultCode.NO_SUCH_OBJECT);
result.setDiagnosticMessage("Could not find entry " + request.getDN());
return new PreParsePluginResult(false, false, true, true);
}
}
catch (final LDAPException le)
{
serverContext.debugCaught(le);
result.setResultData(le);
return new PreParsePluginResult(false, false, true, true);
}
// Update the entry with the modifications from the request.
final com.unboundid.ldap.sdk.Entry updatedEntry;
try
{
updatedEntry = com.unboundid.ldap.sdk.Entry.applyModifications(
currentEntry, false, request.getModifications());
}
catch (final LDAPException le)
{
serverContext.debugCaught(le);
result.setResultData(le);
return new PreParsePluginResult(false, false, true, true);
}
try
{
ensureUniqueness(updatedEntry, serverContext, operationContext, config,
internalConnection, attributeType, fieldPathString, fieldPath,
uniqueWithinEntry, uniqueAcrossEntries, baseDNs, uniqueEntryFilter,
uniqueValueFilter);
return PreParsePluginResult.SUCCESS;
}
catch (final LDAPException le)
{
serverContext.debugCaught(le);
result.setResultData(le);
return new PreParsePluginResult(false, false, true, true);
}
}
/**
* Performs any processing which may be necessary before the server actually
* attempts to update the entry in the backend. This will be invoked only for
* modify operations requested directly by clients, but not for modify
* operations received from another server via replication.
*
* @param operationContext The context for the modify operation.
* @param request The modify request to be processed.
* @param result The result that will be returned to the client if
* the plugin result indicates that processing on
* the operation should be interrupted. It may be
* altered if desired.
* @param oldEntry The entry as it appeared before the modifications
* were applied.
* @param newEntry The updated entry as it will appear after the
* modifications have been applied.
*
* @return Information about the result of the plugin processing.
*/
@Override()
public PreOperationPluginResult doPreOperation(
final ActiveOperationContext operationContext,
final ModifyRequest request, final UpdatableModifyResult result,
final Entry oldEntry, final Entry newEntry)
{
try
{
ensureUniqueness(newEntry.toLDAPSDKEntry(), serverContext,
operationContext, config, internalConnection, ldapAttributeType,
jsonFieldPathString, jsonFieldPath, requireUniquenessWithinAnEntry,
requireUniquenessAcrossEntries, baseDNs, uniqueEntryLDAPFilter,
uniqueValueJSONObjectFilter);
return PreOperationPluginResult.SUCCESS;
}
catch (final LDAPException le)
{
serverContext.debugCaught(le);
result.setResultData(le);
return new PreOperationPluginResult(false, false, true, true);
}
}
/**
* Performs any processing which may be necessary after all other processing
* has been completed for a modify operation and the response has been sent
* to the client. This will be invoked only for modify operations requested
* directly by clients, but not for modify operations received from another
* server via replication.
*
* @param operationContext The context for the modify operation.
* @param request The modify request that was processed.
* @param result The result that was returned to the client.
*
* @return Information about the result of the plugin processing.
*/
@Override()
public PostResponsePluginResult doPostResponse(
final CompletedOperationContext operationContext,
final ModifyRequest request, final ModifyResult result)
{
// We aren't given access to the updated entry, so we'll need to fetch it
// from a backend server. But we'll only do that if the request could
// result in a conflict, since otherwise retrieving the entry would be
// wasted effort.
//
// First, we want to make sure that we're dealing with a consistent
// configuration, so get all of the configuration properties first.
final AttributeType attributeType = ldapAttributeType;
final List<String> fieldPath = jsonFieldPath;
final String fieldPathString = jsonFieldPathString;
final boolean uniqueWithinEntry = requireUniquenessWithinAnEntry;
final boolean uniqueAcrossEntries = requireUniquenessAcrossEntries;
final Filter uniqueEntryFilter = uniqueEntryLDAPFilter;
final JSONObjectFilter uniqueValueFilter = uniqueValueJSONObjectFilter;
if (! couldCreateConflict(request, attributeType, baseDNs, fieldPath))
{
return PostResponsePluginResult.SUCCESS;
}
// Retrieve the entry.
final SearchResultEntry entry;
try
{
entry = internalConnection.getEntry(request.getDN(), "*", "+");
if (entry == null)
{
if (serverContext.debugEnabled())
{
serverContext.debugWarning("The JSON field uniqueness plugin " +
"defined in configuration entry '" + config.getConfigObjectDN() +
"' could not find entry '" + request.getDN() +
"' targeted by proxied modify operation conn=" +
operationContext.getConnectionID() + " op=" +
operationContext.getOperationID() +
". This likely indicates that the entry was deleted shortly " +
"after the modification. At any rate, no uniqueness conflict " +
"processing can be performed for this entry.");
}
return PostResponsePluginResult.SUCCESS;
}
}
catch (final Exception e)
{
if (serverContext.debugEnabled())
{
serverContext.debugCaught(e);
serverContext.debugError("The JSON field uniqueness plugin " +
"defined in configuration entry '" + config.getConfigObjectDN() +
"' encountered an error while trying to retrieve entry '" +
request.getDN() + "' targeted by proxied modify operation " +
"conn=" + operationContext.getConnectionID() + " op=" +
operationContext.getOperationID() +
": " + StaticUtils.getExceptionMessage(e) +
". No uniqueness conflict processing can be performed for this " +
"entry.");
}
return PostResponsePluginResult.SUCCESS;
}
try
{
ensureUniqueness(entry, serverContext, operationContext, config,
internalConnection, attributeType, fieldPathString, fieldPath,
uniqueWithinEntry, uniqueAcrossEntries, baseDNs, uniqueEntryFilter,
uniqueValueFilter);
}
catch (final LDAPException le)
{
serverContext.debugCaught(le);
// This means that a proxied operation generated a conflict. We can't do
// anything to prevent this, but we can generate an administrative alert
// to notify administrators of the problem.
serverContext.sendAlert(AlertSeverity.ERROR, le.getMessage());
}
return PostResponsePluginResult.SUCCESS;
}
/**
* Performs any processing which may be necessary after all other processing
* has been completed for a modify operation which has been received from
* another server via replication.
*
* @param operationContext The context for the modify operation.
* @param request The modify request that was processed.
* @param result The result that was returned to the client.
*/
@Override()
public void doPostReplication(
final CompletedOperationContext operationContext,
final ModifyRequest request, final ModifyResult result)
{
// We aren't given access to the updated entry, so we'll need to fetch it
// from the backend. But we'll only do that if the request could result
// in a conflict, since otherwise retrieving the entry would be wasted
// effort.
//
// First, we want to make sure that we're dealing with a consistent
// configuration, so get all of the configuration properties first.
final AttributeType attributeType = ldapAttributeType;
final List<String> fieldPath = jsonFieldPath;
final String fieldPathString = jsonFieldPathString;
final boolean uniqueWithinEntry = requireUniquenessWithinAnEntry;
final boolean uniqueAcrossEntries = requireUniquenessAcrossEntries;
final Filter uniqueEntryFilter = uniqueEntryLDAPFilter;
final JSONObjectFilter uniqueValueFilter = uniqueValueJSONObjectFilter;
if (! couldCreateConflict(request, attributeType, baseDNs, fieldPath))
{
return;
}
// Retrieve the entry.
final SearchResultEntry entry;
try
{
entry = internalConnection.getEntry(request.getDN(), "*", "+");
if (entry == null)
{
if (serverContext.debugEnabled())
{
serverContext.debugWarning("The JSON field uniqueness plugin " +
"defined in configuration entry '" + config.getConfigObjectDN() +
"' could not find entry '" + request.getDN() +
"' targeted by replicated modify operation conn=" +
operationContext.getConnectionID() + " op=" +
operationContext.getOperationID() +
". This likely indicates that the entry was deleted shortly " +
"after the modification. At any rate, no uniqueness conflict " +
"processing can be performed for this entry.");
}
return;
}
}
catch (final Exception e)
{
if (serverContext.debugEnabled())
{
serverContext.debugCaught(e);
serverContext.debugError("The JSON field uniqueness plugin " +
"defined in configuration entry '" + config.getConfigObjectDN() +
"' encountered an error while trying to retrieve entry '" +
request.getDN() + "' targeted by replicated modify operation " +
"conn=" + operationContext.getConnectionID() + " op=" +
operationContext.getOperationID() +
": " + StaticUtils.getExceptionMessage(e) +
". No uniqueness conflict processing can be performed for this " +
"entry.");
}
return;
}
try
{
ensureUniqueness(entry, serverContext, operationContext, config,
internalConnection, attributeType, fieldPathString, fieldPath,
uniqueWithinEntry, uniqueAcrossEntries, baseDNs, uniqueEntryFilter,
uniqueValueFilter);
}
catch (final LDAPException le)
{
serverContext.debugCaught(le);
// This means that a replicated operation generated a conflict. We can't
// do anything to prevent this, but we can generate an administrative
// alert to notify administrators of the problem.
serverContext.sendAlert(AlertSeverity.ERROR, le.getMessage());
}
}
/**
* Attempts to determine whether the provided modify request could create a
* uniqueness conflict.
*
* @param request The modify request being processed.
* @param attributeType The LDAP attribute type for which uniqueness will be
* enforced.
* @param baseDNList The list of base DNs for which uniqueness will be
* enforced.
* @param fieldPath The path to the JSON field for which uniqueness will
* be enforced.
*
* @return {@code true} if the provided modify request could create a
* uniqueness conflict, or {@code false} if not.
*/
private boolean couldCreateConflict(final ModifyRequest request,
final AttributeType attributeType,
final List<DN> baseDNList,
final List<String> fieldPath)
{
// See if the target entry is within any of the configured base DNs. If
// not, then we don't care about it.
try
{
final DN parsedEntryDN = new DN(request.getDN());
boolean withinBaseDN = false;
for (final DN baseDN : baseDNList)
{
if (parsedEntryDN.isDescendantOf(baseDN, true))
{
withinBaseDN = true;
break;
}
}
if (! withinBaseDN)
{
return false;
}
}
catch (final Exception e)
{
serverContext.debugCaught(e);
}
// Now, iterate through the modifications to see if any of them could
// generate a conflict.
for (final Modification m : request.getModifications())
{
// We only care about modification types of ADD or REPLACE.
if (! ((m.getModificationType() == ModificationType.ADD) ||
(m.getModificationType() == ModificationType.REPLACE)))
{
continue;
}
// We only care about modifications that have values. A REPLACE
// modification might not have any values if it's intended to remove
// the attribute from the entry.
final String[] values = m.getValues();
if ((values == null) || (values.length == 0))
{
continue;
}
// See if the modification targets the right attribute type.
//
// NOTE: It's possible that the modification targeted an attribute with
// options, so we need to check the base name without any options.
final String baseName = Attribute.getBaseName(m.getAttributeName());
if (! attributeType.hasNameOrOID(baseName))
{
continue;
}
// Try to parse each of the values as a JSON object and see if any of
// them contains the target field. That'll be enough to convince us
// that we need to get the entry.
for (final String value : values)
{
try
{
final JSONObject o = new JSONObject(value);
final List<JSONValue> fieldValues = getValues(o, fieldPath);
if (fieldValues.isEmpty())
{
continue;
}
return true;
}
catch (final Exception e)
{
serverContext.debugCaught(e);
}
}
}
// If we've gotten here, then the modification couldn't have introduced a
// conflict.
return false;
}
/**
* Performs all necessary uniqueness processing for the provided entry.
*
* @param entry The entry to examine.
* @param serverContext The server context.
* @param operationContext The operation context.
* @param config The configuration for the plugin.
* @param internalConnection The internal connection to use to perform
* internal searches.
* @param attributeType The target attribute type.
* @param fieldPathString The string representation of the JSON field
* path.
* @param fieldPath The components that comprise the path to the
* target JSON field.
* @param uniqueWithinEntry Indicates whether to enforce uniqueness across
* multiple values within the same entry.
* @param uniqueAcrossEntries Indicates whether to enforce uniqueness across
* entries within the specified set of base DNs.
* @param baseDNs The set of base DNs below which to enforce
* uniqueness.
* @param uniqueEntryFilter An optional filter that entries must match.
* @param uniqueValueFilter An optional filter that values must match.
*
* @throws LDAPException If a conflict is found.
*/
private static void ensureUniqueness(final com.unboundid.ldap.sdk.Entry entry,
final DirectoryServerContext serverContext,
final OperationContext operationContext,
final PluginConfig config,
final InternalConnection internalConnection,
final AttributeType attributeType,
final String fieldPathString,
final List<String> fieldPath,
final boolean uniqueWithinEntry,
final boolean uniqueAcrossEntries,
final List<DN> baseDNs,
final Filter uniqueEntryFilter,
final JSONObjectFilter uniqueValueFilter)
throws LDAPException
{
// See if the entry is below a base DN that we care about.
try
{
final DN parsedEntryDN = entry.getParsedDN();
boolean withinBaseDN = false;
for (final DN baseDN : baseDNs)
{
if (parsedEntryDN.isDescendantOf(baseDN, true))
{
withinBaseDN = true;
break;
}
}
if (! withinBaseDN)
{
if (serverContext.debugEnabled())
{
serverContext.debugInfo("The JSON field uniqueness plugin " +
"defined in configuration entry '" + config.getConfigObjectDN() +
" is skipping uniqueness processing for entry '" +
entry.getDN() + "' for conn=" +
operationContext.getConnectionID() + " op=" +
operationContext.getOperationID() + " because it is not " +
"within any of the configured base DNs " + baseDNs);
}
return;
}
}
catch (final Exception e)
{
if (serverContext.debugEnabled())
{
serverContext.debugCaught(e);
serverContext.debugWarning("The JSON field uniqueness plugin " +
"defined in configuration entry '" + config.getConfigObjectDN() +
"' could not parse DN '" + entry.getDN() +
"' encountered while processing operation conn=" +
operationContext.getConnectionID() + " op=" +
operationContext.getOperationID() + ": " +
StaticUtils.getExceptionMessage(e) + ". Going to assume that " +
"the entry is within a base DN for which uniqueness should be " +
"enforced.");
}
}
// If there is a unique entry filter, then see if the entry matches it.
if (uniqueEntryFilter != null)
{
try
{
if (! uniqueEntryFilter.matchesEntry(entry))
{
if (serverContext.debugEnabled())
{
serverContext.debugInfo("The JSON field uniqueness plugin " +
"defined in configuration entry '" +
config.getConfigObjectDN() + " is skipping uniqueness " +
"processing for entry '" + entry.getDN() + "' for conn=" +
operationContext.getConnectionID() + " op=" +
operationContext.getOperationID() + " because it does not " +
"match " + ARG_UNIQUE_ENTRY_LDAP_FILTER + " value " +
uniqueEntryFilter);
}
return;
}
}
catch (final Exception e)
{
if (serverContext.debugEnabled())
{
serverContext.debugCaught(e);
serverContext.debugWarning("The JSON field uniqueness plugin " +
"defined in configuration entry '" + config.getConfigObjectDN() +
"' encountered an error while evaluating " +
ARG_UNIQUE_ENTRY_LDAP_FILTER + " value " + uniqueEntryFilter +
" against entry '" + entry.getDN() + "' for conn=" +
operationContext.getConnectionID() + " op=" +
operationContext.getOperationID() + ": " +
StaticUtils.getExceptionMessage(e) + ". Going to assume that " +
"the entry matches the filter.");
}
}
}
// Iterate through the attributes in the entry to get a list of all values
// for the target field from the target attribute type.
final ArrayList<JSONValue> fieldValues = new ArrayList<JSONValue>(10);
for (final Attribute a : entry.getAttributes())
{
// See if the attribute has the appropriate attribute type.
//
// NOTE: The attribute could have options, so make sure to check the
// base name.
if (! attributeType.hasNameOrOID(a.getBaseName()))
{
continue;
}
// Iterate through the values of the attribute and try to parse them as
// JSON objects.
for (final String value : a.getValues())
{
final JSONObject o;
try
{
o = new JSONObject(value);
}
catch (final Exception e)
{
if (serverContext.debugEnabled())
{
serverContext.debugCaught(e);
serverContext.debugWarning("The JSON field uniqueness plugin " +
"defined in configuration entry '" +
config.getConfigObjectDN() + "' encountered " + a.getName() +
" value '" + value + "' in entry '" + entry.getDN() +
"' that could not be parsed as a valid JSON object for conn=" +
operationContext.getConnectionID() + " op=" +
operationContext.getOperationID() + ": " +
StaticUtils.getExceptionMessage(e));
}
continue;
}
// If we have a uniqueValueFilter, then see if the provided object
// matches that filter. If not, then skip this value.
if ((uniqueValueFilter != null) &&
(! uniqueValueFilter.matchesJSONObject(o)))
{
continue;
}
// Extract the values of the target field from the object. Iterate
// through them and see if any of them match a value that we've already
// found. At the very least, we only want to do the processing for each
// value once, but we might also want to reject conflicts in the same
// entry.
for (final JSONValue jsonValue : getValues(o, fieldPath))
{
boolean conflictFound = false;
for (final JSONValue v : fieldValues)
{
if (v.equals(jsonValue, false, true, false))
{
conflictFound = true;
break;
}
}
if (conflictFound)
{
if (uniqueWithinEntry)
{
throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
"Entry '" + entry.getDN() + "' has duplicate values for " +
"JSON field " + fieldPathString +
" but the server is configured to require all values " +
"for that field to be unique, even within the same " +
"entry.");
}
}
else
{
fieldValues.add(jsonValue);
}
}
}
}
// If we shouldn't check for conflicts across other entries, or if we didn't
// find any field values, then we're done.
if (fieldValues.isEmpty())
{
if (serverContext.debugEnabled())
{
serverContext.debugInfo("The JSON field uniqueness plugin " +
"defined in configuration entry '" + config.getConfigObjectDN() +
"' did not find any " + attributeType.getNameOrOID() +
" values for field " + fieldPathString + " in entry '" +
entry.getDN() + "' for conn=" +
operationContext.getConnectionID() + " op=" +
operationContext.getOperationID());
}
return;
}
if (! uniqueAcrossEntries)
{
if (serverContext.debugEnabled())
{
serverContext.debugInfo("The JSON field uniqueness plugin " +
"defined in configuration entry '" + config.getConfigObjectDN() +
"' is only configured to look for duplicate " + fieldPathString +
" values in attribute " + attributeType.getNames() +
" within the same entry, and entry '" + entry.getDN() +
"' for conn=" + operationContext.getConnectionID() + " op=" +
operationContext.getOperationID() +
" did not have any such conflicts.");
}
return;
}
if (serverContext.debugEnabled())
{
serverContext.debugInfo("The JSON field uniqueness plugin defined in " +
"configuration entry '" + config.getConfigObjectDN() +
"' found one or more values for JSON field " + fieldPathString +
" in attribute " + attributeType.getNameOrOID() +
" in entry '" + entry.getDN() + " for operation conn=" +
operationContext.getConnectionID() + " op=" +
operationContext.getOperationID() + "': " + fieldValues +
". Going to search for entries with conflicting values.");
}
// We need to look for conflicts across other entries. First, generate the
// equals or equalsAny filter to use to identify entries with conflicting
// values.
JSONObjectFilter jsonObjectFilter;
if (fieldValues.size() == 1)
{
jsonObjectFilter =
new EqualsJSONObjectFilter(fieldPath, fieldValues.get(0));
}
else
{
jsonObjectFilter = new EqualsAnyJSONObjectFilter(fieldPath, fieldValues);
}
// If we have a unique value filter, then we'll need to AND the JSON object
// filter we just created with a NOT of that value filter so that we exclude
// objects that don't match that filter.
if (uniqueValueFilter != null)
{
jsonObjectFilter = new ANDJSONObjectFilter(jsonObjectFilter,
new NegateJSONObjectFilter(uniqueValueFilter));
}
// Get an LDAP filter that will allow us to process the JSON object filter.
Filter ldapFilter =
jsonObjectFilter.toLDAPFilter(attributeType.getNameOrOID());
// If we have a unique entry filter, then we'll need to AND the LDAP filter
// we just created with that unique entry filter so that we only check
// entries that match that filter.
if (uniqueEntryFilter != null)
{
ldapFilter = Filter.createANDFilter(ldapFilter, uniqueEntryFilter);
}
// Iterate through the configured base DNs and search beneath them for
// conflicting entries.
//
// NOTE: Use a size limit of 1 for the search request so that we don't
// waste time finding all the matches once we know that there are at least
// two matching entries. One matching entry may be okay if it's the entry
// that we're currently validating, and we should only report a conflict for
// one match if it doesn't match the DN of the entry we're currently
// validating. But any more than one match (which should trigger a
// SIZE_LIMIT_EXCEEDED result) means that there's definitely a conflict.
final SearchRequest searchRequest = new SearchRequest("", SearchScope.SUB,
DereferencePolicy.NEVER, 1, 0, false, ldapFilter, "1.1");
for (final DN baseDN : baseDNs)
{
searchRequest.setBaseDN(baseDN);
SearchResult searchResult;
try
{
searchResult = internalConnection.search(searchRequest);
}
catch (final LDAPSearchException lse)
{
searchResult = lse.getSearchResult();
}
if (serverContext.debugEnabled())
{
serverContext.debugInfo("The JSON field uniqueness plugin " +
"defined in configuration entry '" + config.getConfigObjectDN() +
"' got search result " + searchResult + " for internal search " +
searchRequest);
}
switch (searchResult.getResultCode().intValue())
{
case ResultCode.SUCCESS_INT_VALUE:
// The search completed successfully, but we need to check the entries
// that were returned to see if it matched any entry other than the
// target entry.
switch (searchResult.getEntryCount())
{
case 0:
break;
case 1:
final DN parsedThisDN = entry.getParsedDN();
final DN parsedFoundDN =
searchResult.getSearchEntries().get(0).getParsedDN();
if (! parsedThisDN.equals(parsedFoundDN))
{
throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
"Entry '" + entry.getDN() + "' has one or more values " +
"for JSON field '" + fieldPathString + "' that " +
"already exist in at least one other entry in " +
"the server, but the server is configured to " +
"require all values for that field to be unique.");
}
break;
default:
throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
"Entry '" + entry.getDN() + "' has one or more values for " +
"JSON field '" + fieldPathString +
"' that already exist in at least one other entry in " +
"the server, but the server is configured to " +
"require all values for that field to be unique.");
}
break;
case ResultCode.NO_SUCH_OBJECT_INT_VALUE:
// This means that the search base didn't exist. This definitely
// means no conflict.
break;
case ResultCode.SIZE_LIMIT_EXCEEDED_INT_VALUE:
// This means that there was definitely a conflict.
throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
"Entry '" + entry.getDN() + "' has one or more values for " +
"JSON field '" + fieldPathString +
"' that already exist in at least one other entry in " +
"the server, but the server is configured to " +
"require all values for that field to be unique.");
default:
// This indicates that some other error occurred that prevented the
// search from succeeding. We'll treat this like a conflict in that
// we'll throw an exception to reject the operation (or alert if it's
// a replicated operation that's already been applied somewhere else)
// but use a different message for the exception.
throw new LDAPException(ResultCode.UNWILLING_TO_PERFORM,
"An error occurred while looking for uniqueness conflicts " +
"for values of the '" + fieldPathString +
"' field in attribute " + attributeType.getNameOrOID() +
" (result code " + searchResult.getResultCode() +
", diagnostic message '" +
searchResult.getDiagnosticMessage() + "'.");
}
}
// If we've gotten here, then there are no conflicts.
if (serverContext.debugEnabled())
{
serverContext.debugInfo("The JSON field uniqueness plugin defined in " +
"configuration entry '" + config.getConfigObjectDN() +
"' did not find any conflicts for values of JSON field " +
fieldPathString + " in attribute " + attributeType.getNameOrOID() +
" in entry '" + entry.getDN() + " for operation conn=" +
operationContext.getConnectionID() + " op=" +
operationContext.getOperationID());
}
}
/**
* Retrieves a list of the values for the specified field from the provided
* object.
*
* @param o The JSON object to examine.
* @param fieldPath The path to the JSON field for which to retrieve the
* values.
*
* @return The set of values that match the provided field name specifier, or
* an empty list if the provided JSON object does not have any fields
* matching the provided specifier.
*/
private static List<JSONValue> getValues(final JSONObject o,
final List<String> fieldPath)
{
final ArrayList<JSONValue> values = new ArrayList<JSONValue>(10);
getValues(o, fieldPath, 0, values);
return values;
}
/**
* Adds all values for the specified field to the provided list.
*
* @param o The JSON object to examine.
* @param fieldPath The path to the JSON field for which to retrieve
* the values.
* @param fieldNameIndex The current index into the field name specifier.
* @param values The list into which matching values should be
* added.
*/
private static void getValues(final JSONObject o,
final List<String> fieldPath,
final int fieldNameIndex,
final List<JSONValue> values)
{
final JSONValue v = o.getField(fieldPath.get(fieldNameIndex));
if (v == null)
{
return;
}
final int nextIndex = fieldNameIndex + 1;
if (nextIndex < fieldPath.size())
{
// This indicates that there are more elements in the field name
// specifier. The value must either be a JSON object that we can look
// further into, or it must be an array containing one or more JSON
// objects.
if (v instanceof JSONObject)
{
getValues((JSONObject) v, fieldPath, nextIndex, values);
}
else if (v instanceof JSONArray)
{
getValuesFromArray((JSONArray) v, fieldPath, nextIndex, values);
}
return;
}
// If we've gotten here, then there is no more of the field specifier, so
// the value we retrieved matches the specifier. Add it to the list of
// values.
values.add(v);
}
/**
* Calls {@code getValues} for any elements of the provided array that are
* JSON objects, recursively descending into any nested arrays.
*
* @param a The array to process.
* @param fieldPath The path to the JSON field for which to retrieve
* the values.
* @param fieldNameIndex The current index into the field name specifier.
* @param values The list into which matching values should be
* added.
*/
private static void getValuesFromArray(final JSONArray a,
final List<String> fieldPath,
final int fieldNameIndex,
final List<JSONValue> values)
{
for (final JSONValue v : a.getValues())
{
if (v instanceof JSONObject)
{
getValues((JSONObject) v, fieldPath, fieldNameIndex, values);
}
else if (v instanceof JSONArray)
{
getValuesFromArray((JSONArray) v, fieldPath, fieldNameIndex, values);
}
}
}
/**
* Indicates whether the configuration represented by the provided argument
* parser is acceptable for use by this extension. The parser will have been
* used to parse any configuration available for this extension, and any
* automatic validation will have been performed. This method may be used to
* perform any more complex validation which cannot be performed automatically
* by the argument parser.
*
* @param config The general configuration for this extension.
* @param parser The argument parser that has been used to
* parse the proposed configuration for this
* extension.
* @param unacceptableReasons A list to which messages may be added to
* provide additional information about why the
* provided configuration is not acceptable.
*
* @return {@code true} if the configuration in the provided argument parser
* appears to be acceptable, or {@code false} if not.
*/
@Override()
public boolean isConfigurationAcceptable(final PluginConfig config,
final ArgumentParser parser,
final List<String> unacceptableReasons)
{
// If the server context is null, then get an instance from the provided
// configuration.
if (serverContext == null)
{
serverContext = config.getServerContext();
}
// Get an internal connection to use for processing internal operations.
if (internalConnection == null)
{
internalConnection = serverContext.getInternalRootConnection();
}
// Make sure that the plugin is configured with the appropriate set of
// plugin types based on the type of product in which it is running.
boolean acceptable = true;
final boolean isDirectoryServer;
final Set<String> pluginTypes = config.getPluginTypes();
if (serverContext.isDirectoryFunctionalityAvailable())
{
isDirectoryServer = true;
if (! pluginTypes.equals(EXPECTED_DIRECTORY_PLUGIN_TYPES))
{
acceptable = false;
unacceptableReasons.add("When the " + getExtensionName() +
" is used in the Directory Server, it must be configured with " +
"the following plugin types: " + EXPECTED_DIRECTORY_PLUGIN_TYPES +
'.');
}
}
else if (serverContext.isDirectoryProxyFunctionalityAvailable())
{
isDirectoryServer = false;
if (! pluginTypes.equals(EXPECTED_PROXY_PLUGIN_TYPES))
{
acceptable = false;
unacceptableReasons.add("When the " + getExtensionName() +
" is used in the Directory Proxy Server, it must be configured " +
"with the following plugin types: " +
EXPECTED_PROXY_PLUGIN_TYPES + '.');
}
}
else
{
isDirectoryServer = false;
acceptable = false;
unacceptableReasons.add("The " + getExtensionName() +
" can only be used in the Directory Server or Directory Proxy " +
"Server.");
}
// Get the configured LDAP attribute type. It must be defined in the server
// schema.
final AttributeType attrType;
final StringArgument attrNameArg = parser.getStringArgument(ARG_ATTR_NAME);
if ((attrNameArg == null) || (! attrNameArg.isPresent()))
{
// The argument parser should ensure that this never happens.
acceptable = false;
unacceptableReasons.add("No value was provided for the required " +
ARG_ATTR_NAME + " argument.");
attrType = null;
}
else
{
attrType = serverContext.getSchema().getAttributeType(
attrNameArg.getValue(), false);
if (attrType == null)
{
acceptable = false;
unacceptableReasons.add("Value '" + attrNameArg.getValue() +
"' for argument " + ARG_ATTR_NAME + " does not represent the " +
"name or OID for any attribute type defined in the server " +
"schema.");
}
}
DN jsonAttributeConstraintsDN = null;
if (attrType != null)
{
// The attribute type must have a JSONObject syntax.
if ((attrType.getSyntax() == null) ||
(! attrType.getSyntax().getOID().equals(OID_JSON_OBJECT_SYNTAX)))
{
acceptable = false;
unacceptableReasons.add("Attribute type " + attrNameArg.getValue() +
" is not defined with a JSONObject syntax (OID " +
OID_JSON_OBJECT_SYNTAX + ").");
}
// If the plugin is being run in the Directory Server, then make sure that
// the server is configured with a JSON attribute constraints definition
// for the specified attribute type. Note that it's possible that the
// definition was created with an alternate name for the attribute type,
// so we'll see if the entry exists with any of the names (or OID) for
// that attribute type.
if (isDirectoryServer)
{
for (final String name : getNamesAndOID(attrType))
{
final DN dn = new DN(
new RDN("ds-cfg-attribute-type", name),
JSON_ATTRIBUTE_CONSTRAINTS_PARENT_DN);
try
{
if (internalConnection.getEntry(dn.toString(), "1.1") != null)
{
jsonAttributeConstraintsDN = dn;
break;
}
}
catch (final Exception e)
{
serverContext.debugCaught(e);
acceptable = false;
unacceptableReasons.add("An error occurred while attempting to " +
"determine whether a JSON attribute constraints definition " +
"exists in entry '" + dn + "': " +
StaticUtils.getExceptionMessage(e));
break;
}
}
if (jsonAttributeConstraintsDN == null)
{
acceptable = false;
unacceptableReasons.add("The server is not configured with a JSON " +
"attribute constraints definition for attribute " +
attrNameArg.getValue() + '.');
}
}
}
// Retrieve and validate the configured JSON field path.
final String fieldPathStr;
final StringArgument fieldPathArg =
parser.getStringArgument(ARG_JSON_FIELD_PATH);
if ((fieldPathArg == null) || (! fieldPathArg.isPresent()))
{
// The argument parser should ensure that this never happens.
acceptable = false;
unacceptableReasons.add("No value was provided for the required " +
ARG_JSON_FIELD_PATH + " argument.");
fieldPathStr = null;
}
else
{
fieldPathStr = fieldPathArg.getValue();
final List<String> fieldPath =
parseFieldPath(fieldPathStr, unacceptableReasons);
if (fieldPath == null)
{
// The unacceptable reasons will have already been updated by the
// parseFieldPath method.
acceptable = false;
}
}
// If the plugin is being run in the Directory Server, then make sure that
// the server is configured with a JSON field constraints definition for the
// target field, and that it has a ds-cfg-index-values attribute with a
// value of true.
if (acceptable && isDirectoryServer)
{
final DN jsonFieldConstraintParentDN = new DN(
new RDN("cn", "JSON Field Constraints"),
jsonAttributeConstraintsDN);
final DN jsonFieldConstraintsDN = new DN(
new RDN("ds-cfg-json-field", fieldPathStr),
jsonFieldConstraintParentDN);
try
{
final SearchResultEntry jsonFieldConstraintsEntry =
internalConnection.getEntry(jsonFieldConstraintsDN.toString(),
ATTR_JSON_FIELD_CONSTRAINTS_INDEX_VALUES);
if (jsonFieldConstraintsEntry == null)
{
acceptable = false;
unacceptableReasons.add("The server is not configured with a JSON " +
"field constraints definition for field " + fieldPathStr +
" for attribute " + attrNameArg.getValue() + '.');
}
else if (! jsonFieldConstraintsEntry.hasAttributeValue(
ATTR_JSON_FIELD_CONSTRAINTS_INDEX_VALUES, "true"))
{
acceptable = false;
unacceptableReasons.add("The JSON field constraints definition in " +
"entry '" + jsonFieldConstraintsDN + "' does not have a " +
ATTR_JSON_FIELD_CONSTRAINTS_INDEX_VALUES + " value of true.");
}
}
catch (final Exception e)
{
serverContext.debugCaught(e);
acceptable = false;
unacceptableReasons.add("An error occurred while attempting to " +
"retrieve the JSON field constraints definition contained in " +
"entry '" + jsonFieldConstraintsDN + "': " +
StaticUtils.getExceptionMessage(e));
}
}
// If a unique value JSON object filter was provided, then make sure that
// it's a valid JSON object filter.
final StringArgument jsonFilterArg =
parser.getStringArgument(ARG_UNIQUE_VALUE_JSON_OBJECT_FILTER);
if ((jsonFilterArg != null) && jsonFilterArg.isPresent())
{
final JSONObjectFilter f = parseJSONObjectFilter(jsonFilterArg.getValue(),
unacceptableReasons, serverContext);
if (f == null)
{
// The unacceptable reasons will have already been updated by the
// parseJSONObjectFilter method.
acceptable = false;
}
}
// At least one of the require-uniqueness-within-an-entry and
// require-uniqueness-across-entries properties must have a value of true.
final BooleanValueArgument uniqueWithEntryArg =
parser.getBooleanValueArgument(ARG_REQUIRE_UNIQUENESS_WITHIN_AN_ENTRY);
if ((uniqueWithEntryArg != null) && uniqueWithEntryArg.isPresent() &&
(uniqueWithEntryArg.getValue() == Boolean.FALSE))
{
final BooleanValueArgument uniqueAcrossEntriesArg =
parser.getBooleanValueArgument(
ARG_REQUIRE_UNIQUENESS_ACROSS_ENTRIES);
if ((uniqueAcrossEntriesArg != null) &&
uniqueAcrossEntriesArg.isPresent() &&
(uniqueAcrossEntriesArg.getValue() == Boolean.FALSE))
{
acceptable = false;
unacceptableReasons.add("The configuration cannot have both " +
ARG_REQUIRE_UNIQUENESS_WITHIN_AN_ENTRY + " and " +
ARG_REQUIRE_UNIQUENESS_ACROSS_ENTRIES +
" arguments set to false.");
}
}
return acceptable;
}
/**
* Attempts to apply the configuration from the provided argument parser to
* this extension.
*
* @param config The general configuration for this extension.
* @param parser The argument parser that has been used to
* parse the new configuration for this
* extension.
* @param adminActionsRequired A list to which messages may be added to
* provide additional information about any
* additional administrative actions that may
* be required to apply some of the
* configuration changes.
* @param messages A list to which messages may be added to
* provide additional information about the
* processing performed by this method.
*
* @return A result code providing information about the result of applying
* the configuration change. A result of {@code SUCCESS} should be
* used to indicate that all processing completed successfully. Any
* other result will indicate that a problem occurred during
* processing.
*/
@Override()
public ResultCode applyConfiguration(final PluginConfig config,
final ArgumentParser parser,
final List<String> adminActionsRequired,
final List<String> messages)
{
// If the server context is null, then get an instance from the provided
// configuration.
if (serverContext == null)
{
serverContext = config.getServerContext();
}
// Get an internal connection to use for processing internal operations.
if (internalConnection == null)
{
internalConnection = serverContext.getInternalRootConnection();
}
// Get the configured LDAP attribute type.
final StringArgument attrNameArg = parser.getStringArgument(ARG_ATTR_NAME);
final AttributeType attrType = serverContext.getSchema().getAttributeType(
attrNameArg.getValue(), false);
boolean valid = true;
if (attrType == null)
{
valid = false;
messages.add("Unable to retrieve attribute type " +
attrNameArg.getValue() + " from the server schema.");
}
// Get the configured JSON field path.
final StringArgument fieldPathArg =
parser.getStringArgument(ARG_JSON_FIELD_PATH);
final String fieldPathString = fieldPathArg.getValue();
final List<String> fieldPath = parseFieldPath(fieldPathString, messages);
if (fieldPath == null)
{
valid = false;
}
// Determine whether to require uniqueness within an entry.
boolean uniqueWithinEntry = true;
final BooleanValueArgument uniqueWithinEntryArg =
parser.getBooleanValueArgument(ARG_REQUIRE_UNIQUENESS_WITHIN_AN_ENTRY);
if (uniqueWithinEntryArg != null)
{
uniqueWithinEntry = uniqueWithinEntryArg.getValue();
}
// Determine whether to require uniqueness across entries.
boolean uniqueAcrossEntries = true;
final BooleanValueArgument uniqueAcrossEntriesArg =
parser.getBooleanValueArgument(ARG_REQUIRE_UNIQUENESS_ACROSS_ENTRIES);
if (uniqueAcrossEntriesArg != null)
{
uniqueAcrossEntries = uniqueAcrossEntriesArg.getValue();
}
// Get the set of base DNs. If no values are provided in the configuration,
// then get the set of naming contexts from the root DSE.
List<DN> baseDNList = null;
final DNArgument baseDNArg = parser.getDNArgument(ARG_BASE_DN);
if ((baseDNArg != null) && baseDNArg.isPresent())
{
baseDNList = baseDNArg.getValues();
}
if ((baseDNList == null) || baseDNList.isEmpty())
{
try
{
final String[] namingContextDNs =
internalConnection.getRootDSE().getNamingContextDNs();
final ArrayList<DN> dnList = new ArrayList<DN>(namingContextDNs.length);
for (final String dn : namingContextDNs)
{
dnList.add(new DN(dn));
}
baseDNList = Collections.unmodifiableList(dnList);
}
catch (final Exception e)
{
serverContext.debugCaught(e);
}
if ((baseDNList == null) || baseDNList.isEmpty())
{
valid = false;
messages.add("No base DNs are configured, and the public naming " +
"context DNs could not be obtained from the server root DSE.");
}
}
// Get the unique entry filter.
Filter entryFilter = null;
final FilterArgument filterArg =
parser.getFilterArgument(ARG_UNIQUE_ENTRY_LDAP_FILTER);
if ((filterArg != null) && filterArg.isPresent())
{
entryFilter = filterArg.getValue();
}
// Get the unique object filter.
JSONObjectFilter objectFilter = null;
final StringArgument objectFilterArg =
parser.getStringArgument(ARG_UNIQUE_VALUE_JSON_OBJECT_FILTER);
if ((objectFilterArg != null) && objectFilterArg.isPresent())
{
objectFilter = parseJSONObjectFilter(objectFilterArg.getValue(), messages,
serverContext);
if (objectFilter == null)
{
valid = false;
}
}
// If the configuration is valid, then apply it.
if (valid)
{
ldapAttributeType = attrType;
jsonFieldPathString = fieldPathString;
jsonFieldPath = fieldPath;
requireUniquenessWithinAnEntry = uniqueWithinEntry;
requireUniquenessAcrossEntries = uniqueAcrossEntries;
baseDNs = baseDNList;
uniqueEntryLDAPFilter = entryFilter;
uniqueValueJSONObjectFilter = objectFilter;
this.config = config;
return ResultCode.SUCCESS;
}
else
{
return ResultCode.OTHER;
}
}
/**
* Retrieves a list containing all of the names and the OID for the provided
* attribute type.
*
* @param attrType The attribute type for which to obtain the names and OID.
*
* @return A list containing all of the names and the OID for the provided
* attribute type.
*/
private static List<String> getNamesAndOID(final AttributeType attrType)
{
final ArrayList<String> namesAndOID = new ArrayList<String>(5);
namesAndOID.addAll(attrType.getNames());
namesAndOID.add(attrType.getOID());
return namesAndOID;
}
/**
* Parses the provided string to extract the path to the target field as a
* list. Levels of hierarchy will be separated by periods. Any periods
* contained in field names must be escaped with backslashes.
*
* @param fieldPathStr The string representation of the field path.
* @param unacceptableReasons A list that should be updated to include any
* reason that the provided string is not a valid
* path.
*
* @return The list containing the path to the field name, or {@code null} if
* the provided field path string does not represent a valid path.
*/
private static List<String> parseFieldPath(final String fieldPathStr,
final List<String> unacceptableReasons)
{
boolean escaped = false;
final ArrayList<String> fieldList = new ArrayList<String>(5);
final StringBuilder buffer = new StringBuilder(fieldPathStr.length());
for (final char c : fieldPathStr.toCharArray())
{
if (escaped)
{
buffer.append(c);
escaped = false;
}
else if (c == '\\')
{
escaped = true;
}
else if (c == '.')
{
if (buffer.length() > 0)
{
fieldList.add(buffer.toString());
buffer.setLength(0);
}
else
{
unacceptableReasons.add("Value '" + fieldPathStr + "' is not a " +
"valid JSON field path because it includes an empty " +
"field name component.");
return null;
}
}
else
{
buffer.append(c);
}
}
if (buffer.length() == 0)
{
unacceptableReasons.add("Value '" + fieldPathStr + "' is not a valid " +
"JSON field path because it includes an empty field name " +
"component.");
return null;
}
else
{
fieldList.add(buffer.toString());
}
return Collections.unmodifiableList(fieldList);
}
/**
* Parses the provided string as a JSON object filter.
*
* @param filterStr The string to be parsed as a JSON object
* filter.
* @param unacceptableReasons A list that should be updated to include any
* reason that the provided string is not a valid
* JSON object filter.
* @param serverContext The server context to use for debugging
* purposes.
*
* @return The parsed JSON object filter, or {@code null} if the provided
* string does not represent a valid JSON object filter.
*/
private static JSONObjectFilter parseJSONObjectFilter(final String filterStr,
final List<String> unacceptableReasons,
final DirectoryServerContext serverContext)
{
final JSONObject filterObject;
try
{
filterObject = new JSONObject(filterStr);
}
catch (final Exception e)
{
serverContext.debugCaught(e);
unacceptableReasons.add(ARG_UNIQUE_VALUE_JSON_OBJECT_FILTER +
" value '" + filterStr + "' is not a valid JSON object: " +
StaticUtils.getExceptionMessage(e));
return null;
}
try
{
return JSONObjectFilter.decode(filterObject);
}
catch (final Exception e)
{
serverContext.debugCaught(e);
unacceptableReasons.add(ARG_UNIQUE_VALUE_JSON_OBJECT_FILTER +
" value '" + filterStr + "' does not represent a valid JSON " +
"object filter: " + StaticUtils.getExceptionMessage(e));
return null;
}
}
/**
* Retrieves a map containing examples of configurations that may be used for
* this extension. The map key should be a list of sample arguments, and the
* corresponding value should be a description of the behavior that will be
* exhibited by the extension when used with that configuration.
*
* @return A map containing examples of configurations that may be used for
* this extension. It may be {@code null} or empty if there should
* not be any example argument sets.
*/
@Override()
public Map<List<String>,String> getExamplesArgumentSets()
{
final List<String> exampleArgs = Arrays.asList(
ARG_ATTR_NAME + "=my-json-attribute",
ARG_JSON_FIELD_PATH + "=my-json-field",
ARG_REQUIRE_UNIQUENESS_WITHIN_AN_ENTRY + "=true",
ARG_REQUIRE_UNIQUENESS_ACROSS_ENTRIES + "=true",
ARG_BASE_DN + "=dc=example,dc=com");
final String exampleDescription = "Configure the " + getExtensionName() +
" to ensure that all values of the 'my-json-field' field in the " +
"'my-json-attribute' attribute are unique across all entries below " +
"'dc=example,dc=com', including across multiple values within the " +
"same entry.";
return Collections.singletonMap(exampleArgs, exampleDescription);
}
}