/*
* 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
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* 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
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. 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 2008-2010 Sun Microsystems, Inc.
* Portions copyright 2011-2012 ForgeRock AS.
* Portions copyright 2011 profiq s.r.o.
*/
package org.opends.server.plugins;
import java.util.Iterator;
import org.opends.server.types.operation.PreOperationAddOperation;
import org.opends.server.types.operation.PreOperationModifyOperation;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.opends.messages.Message;
import org.opends.server.admin.std.server.ReferentialIntegrityPluginCfg;
import org.opends.server.admin.std.server.PluginCfg;
import org.opends.server.admin.std.meta.PluginCfgDefn;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.meta.ReferentialIntegrityPluginCfgDefn
.CheckReferencesScopeCriteria;
import org.opends.server.api.Backend;
import org.opends.server.api.DirectoryThread;
import org.opends.server.api.ServerShutdownListener;
import org.opends.server.api.plugin.*;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DeleteOperation;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ModifyOperation;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.types.*;
import org.opends.server.types.operation.SubordinateModifyDNOperation;
import org.opends.server.types.operation.PostOperationModifyDNOperation;
import org.opends.server.types.operation.PostOperationDeleteOperation;
import static org.opends.messages.PluginMessages.*;
import static org.opends.server.loggers.ErrorLogger.*;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.schema.SchemaConstants.*;
import static org.opends.server.util.StaticUtils.*;
/**
* This class implements a Directory Server post operation plugin that performs
* Referential Integrity processing on successful delete and modify DN
* operations. The plugin uses a set of configuration criteria to determine
* what attribute types to check referential integrity on, and, the set of
* base DNs to search for entries that might need referential integrity
* processing. If none of these base DNs are specified in the configuration,
* then the public naming contexts are used as the base DNs by default.
* <BR><BR>
* The plugin also has an option to process changes in background using
* a thread that wakes up periodically looking for change records in a log
* file.
*/
public class ReferentialIntegrityPlugin
extends DirectoryServerPlugin<ReferentialIntegrityPluginCfg>
implements ConfigurationChangeListener<ReferentialIntegrityPluginCfg>,
ServerShutdownListener
{
/**
* The tracer object for the debug logger.
*/
private static final DebugTracer TRACER = getTracer();
//Current plugin configuration.
private ReferentialIntegrityPluginCfg currentConfiguration;
//List of attribute types that will be checked during referential integrity
//processing.
private LinkedHashSet<AttributeType>
attributeTypes = new LinkedHashSet<AttributeType>();
//List of base DNs that limit the scope of the referential integrity checking.
private Set<DN> baseDNs = new LinkedHashSet<DN>();
//The update interval the background thread uses. If it is 0, then
//the changes are processed in foreground.
private long interval;
//The flag used by the background thread to check if it should exit.
private boolean stopRequested=false;
//The thread name.
private static final String name =
"Referential Integrity Background Update Thread";
//The name of the logfile that the update thread uses to process change
//records. Defaults to "logs/referint", but can be changed in the
//configuration.
private String logFileName;
//The File class that logfile corresponds to.
private File logFile;
//The Thread class that the background thread corresponds to.
private Thread backGroundThread=null;
/**
* Used to save a map in the modifyDN operation attachment map that holds
* the old entry DNs and the new entry DNs related to a modify DN rename to
* new superior operation.
*/
public static final String MODIFYDN_DNS="modifyDNs";
/**
* Used to save a set in the delete operation attachment map that
* holds the subordinate entry DNs related to a delete operation.
*/
public static final String DELETE_DNS="deleteDNs";
//The buffered reader that is used to read the log file by the background
//thread.
private BufferedReader reader;
//The buffered writer that is used to write update records in the log
//when the plugin is in background processing mode.
private BufferedWriter writer;
/* Specifies the mapping between the attribute type (specified in the
* attributeTypes list) and the filter which the plugin should use
* to verify the integrity of the value of the given attribute.
*/
private LinkedHashMap<AttributeType, SearchFilter> attrFiltMap =
new LinkedHashMap<AttributeType, SearchFilter>();
/**
* {@inheritDoc}
*/
@Override
public final void initializePlugin(Set<PluginType> pluginTypes,
ReferentialIntegrityPluginCfg pluginCfg)
throws ConfigException
{
pluginCfg.addReferentialIntegrityChangeListener(this);
LinkedList<Message> unacceptableReasons = new LinkedList<Message>();
if (!isConfigurationAcceptable(pluginCfg, unacceptableReasons))
{
throw new ConfigException(unacceptableReasons.getFirst());
}
applyConfigurationChange(pluginCfg);
// Set up log file. Note: it is not allowed to change once the plugin is
// active.
setUpLogFile(pluginCfg.getLogFile());
interval=pluginCfg.getUpdateInterval();
//Set up background processing if interval > 0.
if(interval > 0)
{
setUpBackGroundProcessing();
}
}
/**
* {@inheritDoc}
*/
@Override
public ConfigChangeResult applyConfigurationChange(
ReferentialIntegrityPluginCfg newConfiguration)
{
ResultCode resultCode = ResultCode.SUCCESS;
boolean adminActionRequired = false;
ArrayList<Message> messages = new ArrayList<Message>();
//Load base DNs from new configuration.
LinkedHashSet<DN> newConfiguredBaseDNs = new LinkedHashSet<DN>();
for(DN baseDN : newConfiguration.getBaseDN())
{
newConfiguredBaseDNs.add(baseDN);
}
//Load attribute types from new configuration.
LinkedHashSet<AttributeType> newAttributeTypes =
new LinkedHashSet<AttributeType>();
for (AttributeType type : newConfiguration.getAttributeType())
{
newAttributeTypes.add(type);
}
// Load the attribute-filter mapping
LinkedHashMap<AttributeType, SearchFilter> newAttrFiltMap =
new LinkedHashMap<AttributeType, SearchFilter>();
for (String attrFilt : newConfiguration.getCheckReferencesFilterCriteria())
{
int sepInd = attrFilt.lastIndexOf(":");
String attr = attrFilt.substring(0, sepInd);
String filtStr = attrFilt.substring(sepInd + 1);
AttributeType attrType =
DirectoryServer.getAttributeType(attr.toLowerCase());
try
{
SearchFilter filter =
SearchFilter.createFilterFromString(filtStr);
newAttrFiltMap.put(attrType, filter);
}
catch (DirectoryException de)
{
/* This should never happen because the filter has already
* been verified.
*/
logError(de.getMessageObject());
}
}
//User is not allowed to change the logfile name, append a message that the
//server needs restarting for change to take effect.
// The first time the plugin is initialised the 'logFileName' is
// not initialised, so in order to verify if it is equal to the new
// log file name, we have to make sure the variable is not null.
String newLogFileName=newConfiguration.getLogFile();
if(logFileName != null && !logFileName.equals(newLogFileName))
{
adminActionRequired=true;
messages.add(
INFO_PLUGIN_REFERENT_LOGFILE_CHANGE_REQUIRES_RESTART.get(logFileName,
newLogFileName));
}
//Switch to the new lists.
baseDNs = newConfiguredBaseDNs;
attributeTypes = newAttributeTypes;
attrFiltMap = newAttrFiltMap;
//If the plugin is enabled and the interval has changed, process that
//change. The change might start or stop the background processing thread.
long newInterval=newConfiguration.getUpdateInterval();
if(newConfiguration.isEnabled() && newInterval != interval)
processIntervalChange(newInterval, messages);
currentConfiguration = newConfiguration;
return new ConfigChangeResult(resultCode, adminActionRequired, messages);
}
/**
* {@inheritDoc}
*/
@Override()
public boolean isConfigurationAcceptable(PluginCfg configuration,
List<Message> unacceptableReasons)
{
boolean isAcceptable = true;
ReferentialIntegrityPluginCfg pluginCfg =
(ReferentialIntegrityPluginCfg) configuration;
for (PluginCfgDefn.PluginType t : pluginCfg.getPluginType())
{
switch (t)
{
case POSTOPERATIONDELETE:
case POSTOPERATIONMODIFYDN:
case SUBORDINATEMODIFYDN:
case SUBORDINATEDELETE:
case PREOPERATIONMODIFY:
case PREOPERATIONADD:
// These are acceptable.
break;
default:
isAcceptable = false;
unacceptableReasons.add(ERR_PLUGIN_REFERENT_INVALID_PLUGIN_TYPE.get(
t.toString()));
}
}
Set<DN> cfgBaseDNs = pluginCfg.getBaseDN();
if ((cfgBaseDNs == null) || cfgBaseDNs.isEmpty())
{
cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
}
// Iterate through all of the defined attribute types and ensure that they
// have acceptable syntaxes and that they are indexed for equality below all
// base DNs.
Set<AttributeType> theAttributeTypes = pluginCfg.getAttributeType();
for (AttributeType type : theAttributeTypes)
{
if (! isAttributeSyntaxValid(type))
{
isAcceptable = false;
unacceptableReasons.add(
ERR_PLUGIN_REFERENT_INVALID_ATTRIBUTE_SYNTAX.get(
type.getNameOrOID(),
type.getSyntax().getSyntaxName()));
}
for (DN baseDN : cfgBaseDNs)
{
Backend b = DirectoryServer.getBackend(baseDN);
if ((b != null) && (!b.isIndexed(type, IndexType.EQUALITY)))
{
isAcceptable = false;
unacceptableReasons.add(ERR_PLUGIN_REFERENT_ATTR_UNINDEXED.get(
pluginCfg.dn().toString(),
type.getNameOrOID(),
b.getBackendID()));
}
}
}
/* Iterate through the attribute-filter mapping and verify that the
* map contains attributes listed in the attribute-type parameter
* and that the filter is valid.
*/
for (String attrFilt : pluginCfg.getCheckReferencesFilterCriteria())
{
int sepInd = attrFilt.lastIndexOf(":");
String attr = attrFilt.substring(0, sepInd).trim();
String filtStr = attrFilt.substring(sepInd + 1).trim();
/* TODO: strip the ;options part? */
/* Get the attribute type for the given attribute. The attribute
* type has to be present in the attributeType list.
*/
AttributeType attrType =
DirectoryServer.getAttributeType(attr.toLowerCase());
if (attrType == null || !theAttributeTypes.contains(attrType))
{
isAcceptable = false;
unacceptableReasons.add(
ERR_PLUGIN_REFERENT_ATTR_NOT_LISTED.get(attr));
}
/* Verify the filter.
*/
try
{
SearchFilter.createFilterFromString(filtStr);
}
catch (DirectoryException de)
{
isAcceptable = false;
unacceptableReasons.add(
ERR_PLUGIN_REFERENT_BAD_FILTER.get(filtStr, de.getMessage()));
}
}
return isAcceptable;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isConfigurationChangeAcceptable(
ReferentialIntegrityPluginCfg configuration,
List<Message> unacceptableReasons)
{
return isConfigurationAcceptable(configuration, unacceptableReasons);
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public PluginResult.PostOperation
doPostOperation(PostOperationModifyDNOperation
modifyDNOperation)
{
// If the operation itself failed, then we don't need to do anything because
// nothing changed.
if (modifyDNOperation.getResultCode() != ResultCode.SUCCESS)
{
return PluginResult.PostOperation.continueOperationProcessing();
}
Map<DN,DN>modDNmap=
(Map<DN, DN>) modifyDNOperation.getAttachment(MODIFYDN_DNS);
if(modDNmap == null)
{
modDNmap=new LinkedHashMap<DN,DN>();
modifyDNOperation.setAttachment(MODIFYDN_DNS, modDNmap);
}
DN oldEntryDN=modifyDNOperation.getOriginalEntry().getDN();
DN newEntryDN=modifyDNOperation.getUpdatedEntry().getDN();
modDNmap.put(oldEntryDN, newEntryDN);
processModifyDN(modDNmap, (interval != 0));
return PluginResult.PostOperation.continueOperationProcessing();
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public PluginResult.PostOperation doPostOperation(
PostOperationDeleteOperation deleteOperation)
{
// If the operation itself failed, then we don't need to do anything because
// nothing changed.
if (deleteOperation.getResultCode() != ResultCode.SUCCESS)
{
return PluginResult.PostOperation.continueOperationProcessing();
}
Set<DN> deleteDNset =
(Set<DN>) deleteOperation.getAttachment(DELETE_DNS);
if(deleteDNset == null)
{
deleteDNset = new HashSet<DN>();
deleteOperation.setAttachment(MODIFYDN_DNS, deleteDNset);
}
deleteDNset.add(deleteOperation.getEntryDN());
processDelete(deleteDNset, (interval != 0));
return PluginResult.PostOperation.continueOperationProcessing();
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public PluginResult.SubordinateModifyDN processSubordinateModifyDN(
SubordinateModifyDNOperation modifyDNOperation, Entry oldEntry,
Entry newEntry, List<Modification> modifications)
{
//This cast gives an unchecked cast warning, suppress it since the cast
//is ok.
Map<DN,DN>modDNmap=
(Map<DN, DN>) modifyDNOperation.getAttachment(MODIFYDN_DNS);
if(modDNmap == null)
{
//First time through, create the map and set it in the operation
//attachment.
modDNmap=new LinkedHashMap<DN,DN>();
modifyDNOperation.setAttachment(MODIFYDN_DNS, modDNmap);
}
modDNmap.put(oldEntry.getDN(), newEntry.getDN());
return PluginResult.SubordinateModifyDN.continueOperationProcessing();
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("unchecked")
@Override
public PluginResult.SubordinateDelete processSubordinateDelete(
DeleteOperation deleteOperation, Entry entry)
{
// This cast gives an unchecked cast warning, suppress it
// since the cast is ok.
Set<DN> deleteDNset =
(Set<DN>) deleteOperation.getAttachment(DELETE_DNS);
if(deleteDNset == null)
{
// First time through, create the set and set it in
// the operation attachment.
deleteDNset = new HashSet<DN>();
deleteOperation.setAttachment(DELETE_DNS, deleteDNset);
}
deleteDNset.add(entry.getDN());
return PluginResult.SubordinateDelete.continueOperationProcessing();
}
/**
* Verify that the specified attribute has either a distinguished name syntax
* or "name and optional UID" syntax.
*
* @param attribute The attribute to check the syntax of.
*
* @return Returns <code>true</code> if the attribute has a valid syntax.
*
*/
private boolean isAttributeSyntaxValid(AttributeType attribute)
{
return (attribute.getSyntaxOID().equals(SYNTAX_DN_OID) ||
attribute.getSyntaxOID().equals(SYNTAX_NAME_AND_OPTIONAL_UID_OID));
}
/**
* Process the specifed new interval value. This processing depends on what
* the current interval value is and new value will be. The values have been
* checked for equality at this point and are not equal.
*
* If the old interval is 0, then the server is in foreground mode and
* the background thread needs to be started using the new interval value.
*
* If the new interval value is 0, the the server is in background mode
* and the the background thread needs to be stopped.
*
* If the user just wants to change the interval value, the background thread
* needs to be interrupted so that it can use the new interval value.
*
* @param newInterval The new interval value to use.
*
* @param msgs An array list of messages that thread stop and start messages
* can be added to.
*
*/
private void processIntervalChange(long newInterval,
ArrayList<Message> msgs) {
if(interval == 0) {
DirectoryServer.registerShutdownListener(this);
interval=newInterval;
msgs.add(INFO_PLUGIN_REFERENT_BACKGROUND_PROCESSING_STARTING.
get(Long.toString(interval)));
setUpBackGroundProcessing();
} else if(newInterval == 0) {
Message message=
INFO_PLUGIN_REFERENT_BACKGROUND_PROCESSING_STOPPING.get();
msgs.add(message);
processServerShutdown(message);
interval=newInterval;
} else {
interval=newInterval;
backGroundThread.interrupt();
msgs.add(
INFO_PLUGIN_REFERENT_BACKGROUND_PROCESSING_UPDATE_INTERVAL_CHANGED.
get(Long.toString(interval),Long.toString(newInterval)));
}
}
/**
* Process a modify DN post operation using the specified map of old and new
* entry DNs. The boolean "log" is used to determine if the map
* is written to the log file for the background thread to pick up. If the
* map is to be processed in foreground, than each base DN or public
* naming context (if the base DN configuration is empty) is processed.
*
* @param modDNMap The map of old entry and new entry DNs from the modify
* DN operation.
*
* @param log Set to <code>true</code> if the map should be written to a log
* file so that the background thread can process the changes at
* a later time.
*
*/
private void processModifyDN(Map<DN, DN> modDNMap, boolean log)
{
if(modDNMap != null)
{
if(log)
{
writeLog(modDNMap);
}
else
{
for(DN baseDN : getBaseDNsToSearch())
{
doBaseDN(baseDN, modDNMap);
}
}
}
}
/**
* Used by both the background thread and the delete post operation to
* process a delete operation on the specified entry DN. The
* boolean "log" is used to determine if the DN is written to the log file
* for the background thread to pick up. This value is set to false if the
* background thread is processing changes. If this method is being called
* by a delete post operation, then setting the "log" value to false will
* cause the DN to be processed in foreground
*
* If the DN is to be processed, than each base DN or public naming
* context (if the base DN configuration is empty) is is checked to see if
* entries under it contain references to the deleted entry DN that need
* to be removed.
*
* @param entryDN The DN of the deleted entry.
*
* @param log Set to <code>true</code> if the DN should be written to a log
* file so that the background thread can process the change at
* a later time.
*
*/
private void processDelete(Set<DN> deleteDNset, boolean log)
{
if(log)
{
writeLog(deleteDNset);
}
else
{
for(DN baseDN : getBaseDNsToSearch())
{
doBaseDN(baseDN, deleteDNset);
}
}
}
/**
* Used by the background thread to process the specified old entry DN and
* new entry DN. Each base DN or public naming context (if the base DN
* configuration is empty) is checked to see if they contain entries with
* references to the old entry DN that need to be changed to the new entry DN.
*
* @param oldEntryDN The entry DN before the modify DN operation.
*
* @param newEntryDN The entry DN after the modify DN operation.
*
*/
private void processModifyDN(DN oldEntryDN, DN newEntryDN)
{
for(DN baseDN : getBaseDNsToSearch())
{
searchBaseDN(baseDN, oldEntryDN, newEntryDN);
}
}
/**
* Return a set of DNs that are used to search for references under. If the
* base DN configuration set is empty, then the public naming contexts
* are used.
*
* @return A set of DNs to use in the reference searches.
*
*/
private Set<DN> getBaseDNsToSearch()
{
if(baseDNs.isEmpty())
{
return DirectoryServer.getPublicNamingContexts().keySet();
}
else
{
return baseDNs;
}
}
/**
* Search a base DN using a filter built from the configured attribute
* types and the specified old entry DN. For each entry that is found from
* the search, delete the old entry DN from the entry. If the new entry
* DN is not null, then add it to the entry.
*
* @param baseDN The DN to base the search at.
*
* @param oldEntryDN The old entry DN that needs to be deleted or replaced.
*
* @param newEntryDN The new entry DN that needs to be added. May be null
* if the original operation was a delete.
*
*/
private void searchBaseDN(DN baseDN, DN oldEntryDN, DN newEntryDN)
{
//Build an equality search with all of the configured attribute types
//and the old entry DN.
HashSet<SearchFilter> componentFilters=new HashSet<SearchFilter>();
for(AttributeType attributeType : attributeTypes)
{
componentFilters.add(SearchFilter.createEqualityFilter(attributeType,
AttributeValues.create(attributeType, oldEntryDN.toString())));
}
InternalClientConnection conn =
InternalClientConnection.getRootConnection();
InternalSearchOperation operation = conn.processSearch(baseDN,
SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0,
false, SearchFilter.createORFilter(componentFilters), null);
switch (operation.getResultCode())
{
case SUCCESS:
break;
case NO_SUCH_OBJECT:
logError(INFO_PLUGIN_REFERENT_SEARCH_NO_SUCH_OBJECT.get(
baseDN.toString()));
return;
default:
Message message1 = ERR_PLUGIN_REFERENT_SEARCH_FAILED.
get(String.valueOf(operation.getErrorMessage()));
logError(message1);
return;
}
for (SearchResultEntry entry : operation.getSearchEntries())
{
deleteAddAttributesEntry(entry, oldEntryDN, newEntryDN);
}
}
/**
* This method is used in foreground processing of a modify DN operation.
* It uses the specified map to perform base DN searching for each map
* entry. The key is the old entry DN and the value is the
* new entry DN.
*
* @param baseDN The DN to base the search at.
*
* @param modifyDNmap The map containing the modify DN old and new entry DNs.
*
*/
private void doBaseDN(DN baseDN, Map<DN,DN> modifyDNmap)
{
for(Map.Entry<DN,DN> mapEntry: modifyDNmap.entrySet())
{
searchBaseDN(baseDN, mapEntry.getKey(), mapEntry.getValue());
}
}
/**
* This method is used in foreground processing of a delete operation.
* It uses the specified set to perform base DN searching for each
* element.
*
* @param baseDN The DN to base the search at.
*
* @param deleteDNset The set containing the delete DNs.
*
*/
private void doBaseDN(DN baseDN, Set<DN> deleteDNset)
{
for(DN deletedEntryDN : deleteDNset)
{
searchBaseDN(baseDN, deletedEntryDN, null);
}
}
/**
* For each attribute type, delete the specified old entry DN and
* optionally add the specified new entry DN if the DN is not null.
* The specified entry is used to see if it contains each attribute type so
* those types that the entry contains can be modified. An internal modify
* is performed to change the entry.
*
* @param e The entry that contains the old references.
*
* @param oldEntryDN The old entry DN to remove references to.
*
* @param newEntryDN The new entry DN to add a reference to, if it is not
* null.
*
*/
private void deleteAddAttributesEntry(Entry e, DN oldEntryDN, DN newEntryDN)
{
LinkedList<Modification> mods = new LinkedList<Modification>();
DN entryDN=e.getDN();
for(AttributeType type : attributeTypes)
{
if(e.hasAttribute(type))
{
AttributeValue value = AttributeValues
.create(type, oldEntryDN.toString());
if (e.hasValue(type, null, value))
{
mods.add(new Modification(ModificationType.DELETE, Attributes
.create(type, value)));
// If the new entry DN exists, create an ADD modification for
// it.
if(newEntryDN != null)
{
mods.add(new Modification(ModificationType.ADD, Attributes
.create(type, newEntryDN.toString())));
}
}
}
}
InternalClientConnection conn =
InternalClientConnection.getRootConnection();
ModifyOperation modifyOperation =
conn.processModify(entryDN, mods);
if(modifyOperation.getResultCode() != ResultCode.SUCCESS)
{
logError(ERR_PLUGIN_REFERENT_MODIFY_FAILED.get(entryDN.toString(),
String.valueOf(modifyOperation.getErrorMessage())));
}
}
/**
* Sets up the log file that the plugin can write update recored to and
* the background thread can use to read update records from. The specifed
* log file name is the name to use for the file. If the file exists from
* a previous run, use it.
*
* @param logFileName The name of the file to use, may be absolute.
*
* @throws ConfigException If a new file cannot be created if needed.
*
*/
private void setUpLogFile(String logFileName)
throws ConfigException
{
this.logFileName=logFileName;
logFile=getFileForPath(logFileName);
try
{
if(!logFile.exists())
{
logFile.createNewFile();
}
}
catch (IOException io)
{
throw new ConfigException(ERR_PLUGIN_REFERENT_CREATE_LOGFILE.get(
io.getMessage()), io);
}
}
/**
* Sets up a buffered writer that the plugin can use to write update records
* with.
*
* @throws IOException If a new file writer cannot be created.
*
*/
private void setupWriter() throws IOException {
writer=new BufferedWriter(new FileWriter(logFile, true));
}
/**
* Sets up a buffered reader that the background thread can use to read
* update records with.
*
* @throws IOException If a new file reader cannot be created.
*
*/
private void setupReader() throws IOException {
reader=new BufferedReader(new FileReader(logFile));
}
/**
* Write the specified map of old entry and new entry DNs to the log
* file. Each entry of the map is a line in the file, the key is the old
* entry normalized DN and the value is the new entry normalized DN.
* The DNs are separated by the tab character. This map is related to a
* modify DN operation.
*
* @param modDNmap The map of old entry and new entry DNs.
*
*/
private void writeLog(Map<DN,DN> modDNmap) {
synchronized(logFile)
{
try
{
setupWriter();
for(Map.Entry<DN,DN> mapEntry : modDNmap.entrySet())
{
writer.write(mapEntry.getKey().toNormalizedString() + "\t" +
mapEntry.getValue().toNormalizedString());
writer.newLine();
}
writer.flush();
writer.close();
}
catch (IOException io)
{
logError(ERR_PLUGIN_REFERENT_CLOSE_LOGFILE.get(io.getMessage()));
}
}
}
/**
* Write the specified entry DNs to the log file.
* These entry DNs are related to a delete operation.
*
* @param deletedEntryDN The DN of the deleted entry.
*
*/
private void writeLog(Set<DN> deleteDNset) {
synchronized(logFile)
{
try
{
setupWriter();
for (DN deletedEntryDN : deleteDNset)
{
writer.write(deletedEntryDN.toNormalizedString());
writer.newLine();
}
writer.flush();
writer.close();
}
catch (IOException io)
{
logError(ERR_PLUGIN_REFERENT_CLOSE_LOGFILE.get(io.getMessage()));
}
}
}
/**
* Process all of the records in the log file. Each line of the file is read
* and parsed to determine if it was a delete operation (a single normalized
* DN) or a modify DN operation (two normalized DNs separated by a tab). The
* corresponding operation method is called to perform the referential
* integrity processing as though the operation was just processed. After
* all of the records in log file have been processed, the log file is
* cleared so that new records can be added.
*
*/
private void processLog() {
synchronized(logFile) {
try {
if(logFile.length() == 0)
{
return;
}
setupReader();
String line;
while((line=reader.readLine()) != null) {
try {
String[] a=line.split("[\t]");
DN origDn = DN.decode(a[0]);
//If there is only a single DN string than it must be a delete.
if(a.length == 1) {
processDelete(Collections.singleton(origDn), false);
} else {
DN movedDN=DN.decode(a[1]);
processModifyDN(origDn, movedDN);
}
} catch (DirectoryException ex) {
//This exception should rarely happen since the plugin wrote the DN
//strings originally.
Message message=
ERR_PLUGIN_REFERENT_CANNOT_DECODE_STRING_AS_DN.
get(ex.getMessage());
logError(message);
}
}
reader.close();
logFile.delete();
logFile.createNewFile();
} catch (IOException io) {
logError(ERR_PLUGIN_REFERENT_REPLACE_LOGFILE.get(io.getMessage()));
}
}
}
/**
* Return the listener name.
*
* @return The name of the listener.
*
*/
@Override
public String getShutdownListenerName() {
return name;
}
/**
* {@inheritDoc}
*/
@Override()
public final void finalizePlugin() {
currentConfiguration.removeReferentialIntegrityChangeListener(this);
if(interval > 0)
{
processServerShutdown(null);
}
}
/**
* Process a server shutdown. If the background thread is running it needs
* to be interrupted so it can read the stop request variable and exit.
*
* @param reason The reason message for the shutdown.
*
*/
@Override
public void processServerShutdown(Message reason)
{
stopRequested = true;
// Wait for back ground thread to terminate
while (backGroundThread != null && backGroundThread.isAlive()) {
try {
// Interrupt if its sleeping
backGroundThread.interrupt();
backGroundThread.join();
}
catch (InterruptedException ex) {
//Expected.
}
}
DirectoryServer.deregisterShutdownListener(this);
backGroundThread=null;
}
/**
* Returns the interval time converted to milliseconds.
*
* @return The interval time for the background thread.
*/
private long getInterval() {
return interval * 1000;
}
/**
* Sets up background processing of referential integrity by creating a
* new background thread to process updates.
*
*/
private void setUpBackGroundProcessing() {
if(backGroundThread == null) {
DirectoryServer.registerShutdownListener(this);
stopRequested = false;
backGroundThread = new BackGroundThread();
backGroundThread.start();
}
}
/**
* Used by the background thread to determine if it should exit.
*
* @return Returns <code>true</code> if the background thread should exit.
*
*/
private boolean isShuttingDown() {
return stopRequested;
}
/**
* The background referential integrity processing thread. Wakes up after
* sleeping for a configurable interval and checks the log file for update
* records.
*
*/
private class BackGroundThread extends DirectoryThread {
/**
* Constructor for the background thread.
*/
public
BackGroundThread() {
super(name);
}
/**
* Run method for the background thread.
*/
@Override
public void run() {
while(!isShuttingDown()) {
try {
sleep(getInterval());
} catch(InterruptedException e) {
continue;
} catch(Exception e) {
if (debugEnabled()) {
TRACER.debugCaught(DebugLogLevel.ERROR, e);
}
}
processLog();
}
}
}
/**
* {@inheritDoc}
*/
@Override
public PluginResult.PreOperation doPreOperation(
PreOperationModifyOperation modifyOperation)
{
/* Skip the integrity checks if the enforcing is not enabled
*/
if (!currentConfiguration.isCheckReferences())
{
return PluginResult.PreOperation.continueOperationProcessing();
}
final List<Modification> mods = modifyOperation.getModifications();
final Entry entry = modifyOperation.getModifiedEntry();
/* Make sure the entry belongs to one of the configured naming
* contexts.
*/
DN entryDN = entry.getDN();
DN entryBaseDN = getEntryBaseDN(entryDN);
if (entryBaseDN == null)
{
return PluginResult.PreOperation.continueOperationProcessing();
}
for (Modification mod : mods)
{
final ModificationType modType = mod.getModificationType();
/* Process only ADD and REPLACE modification types.
*/
if ((modType != ModificationType.ADD)
&& (modType != ModificationType.REPLACE))
{
break;
}
AttributeType attrType = mod.getAttribute().getAttributeType();
Set<String> attrOptions = mod.getAttribute().getOptions();
Attribute modifiedAttribute = entry.getExactAttribute(attrType,
attrOptions);
if (modifiedAttribute != null)
{
PluginResult.PreOperation result =
isIntegrityMaintained(modifiedAttribute, entryDN, entryBaseDN);
if (result.getResultCode() != ResultCode.SUCCESS)
{
return result;
}
}
}
/* At this point, everything is fine.
*/
return PluginResult.PreOperation.continueOperationProcessing();
}
/**
* {@inheritDoc}
*/
@Override
public PluginResult.PreOperation doPreOperation(
PreOperationAddOperation addOperation)
{
/* Skip the integrity checks if the enforcing is not enabled.
*/
if (!currentConfiguration.isCheckReferences())
{
return PluginResult.PreOperation.continueOperationProcessing();
}
final Entry entry = addOperation.getEntryToAdd();
/* Make sure the entry belongs to one of the configured naming
* contexts.
*/
DN entryDN = entry.getDN();
DN entryBaseDN = getEntryBaseDN(entryDN);
if (entryBaseDN == null)
{
return PluginResult.PreOperation.continueOperationProcessing();
}
for (AttributeType attrType : attributeTypes)
{
final List<Attribute> attrs = entry.getAttribute(attrType, false);
if (attrs != null)
{
PluginResult.PreOperation result =
isIntegrityMaintained(attrs, entryDN, entryBaseDN);
if (result.getResultCode() != ResultCode.SUCCESS)
{
return result;
}
}
}
/* If we reahed this point, everything is fine.
*/
return PluginResult.PreOperation.continueOperationProcessing();
}
/**
* Verifies that the integrity of values is maintained.
* @param attrs Attribute list which refers to another entry in the
* directory.
* @param entryDN DN of the entry which contains the <CODE>attr</CODE>
* attribute.
* @return The SUCCESS if the integrity is maintained or
* CONSTRAINT_VIOLATION oherwise
*/
private PluginResult.PreOperation
isIntegrityMaintained(List<Attribute> attrs, DN entryDN, DN entryBaseDN)
{
for(Attribute attr : attrs)
{
PluginResult.PreOperation result =
isIntegrityMaintained(attr, entryDN, entryBaseDN);
if (result != PluginResult.PreOperation.continueOperationProcessing())
{
return result;
}
}
return PluginResult.PreOperation.continueOperationProcessing();
}
/**
* Verifies that the integrity of values is maintained.
* @param attr Attribute which refers to another entry in the
* directory.
* @param entryDN DN of the entry which contains the <CODE>attr</CODE>
* attribute.
* @return The SUCCESS if the integrity is maintained or
* CONSTRAINT_VIOLATION oherwise
*/
private PluginResult.PreOperation isIntegrityMaintained(Attribute attr,
DN entryDN,
DN entryBaseDN)
{
/* Iterate over the list of attributes */
Iterator<AttributeValue> attrValIt = attr.iterator();
try
{
while (attrValIt.hasNext())
{
AttributeValue attrVal = attrValIt.next();
Entry valueEntry = null;
DN valueEntryDN = DN.decode(attrVal.getNormalizedValue());
if (currentConfiguration.getCheckReferencesScopeCriteria()
== CheckReferencesScopeCriteria.NAMING_CONTEXT)
{
if (valueEntryDN.matchesBaseAndScope(entryBaseDN,
SearchScope.SUBORDINATE_SUBTREE))
{
return PluginResult.PreOperation.stopProcessing(
ResultCode.CONSTRAINT_VIOLATION,
ERR_PLUGIN_REFERENT_NAMINGCONTEXT_MISMATCH.get(
valueEntryDN.toString(),
attr.getName(),
entryDN.toString()
)
);
}
valueEntry = DirectoryServer.getEntry(valueEntryDN);
}
else
{
valueEntry = DirectoryServer.getEntry(valueEntryDN);
}
/* Verify that the value entry exists in the backend.
*/
if (valueEntry == null)
{
return PluginResult.PreOperation.stopProcessing(
ResultCode.CONSTRAINT_VIOLATION,
ERR_PLUGIN_REFERENT_ENTRY_MISSING.get(
valueEntryDN.toString(),
attr.getName(),
entryDN.toString()
));
}
/* Verify that the value entry conforms to the filter.
*/
SearchFilter filter = attrFiltMap.get(attr.getAttributeType());
if (filter != null && !filter.matchesEntry(valueEntry))
{
return PluginResult.PreOperation.stopProcessing(
ResultCode.CONSTRAINT_VIOLATION,
ERR_PLUGIN_REFERENT_FILTER_MISMATCH.get(
valueEntry.getDN().toString(),
attr.getName(),
entryDN.toString(),
filter.toString())
);
}
}
}
catch (DirectoryException de)
{
return PluginResult.PreOperation.stopProcessing(
ResultCode.OTHER,
ERR_PLUGIN_REFERENT_EXCEPTION.get(de.getLocalizedMessage()));
}
return PluginResult.PreOperation.continueOperationProcessing();
}
/**
* Verifies if the entry with the specified DN belongs to the
* configured naming contexts.
* @param dn DN of the entry.
* @return Returns <code>true</code> if the entry matches any of the
* configured base DNs, and <code>false</code> if not.
*/
private DN getEntryBaseDN(DN dn)
{
/* Verify that the entry belongs to one of the configured naming
* contexts.
*/
DN namingContext = null;
if (baseDNs.isEmpty())
{
baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
}
for (DN baseDN : baseDNs)
{
if (dn.matchesBaseAndScope(baseDN, SearchScope.SUBORDINATE_SUBTREE))
{
namingContext = baseDN;
break;
}
}
return namingContext;
}
}