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 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 true 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 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 getBaseDNsToSearch()
{
if (baseDNs.isEmpty())
{
return DirectoryServer.getPublicNamingContexts().keySet();
}
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 componentFilters=new HashSet<>();
for(AttributeType attributeType : attributeTypes)
{
componentFilters.add(SearchFilter.createEqualityFilter(attributeType,
ByteString.valueOfUtf8(oldEntryDN.toString())));
}
SearchFilter orFilter = SearchFilter.createORFilter(componentFilters);
final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, orFilter);
InternalSearchOperation operation = getRootConnection().processSearch(request);
switch (operation.getResultCode().asEnum())
{
case SUCCESS:
break;
case NO_SUCH_OBJECT:
logger.debug(INFO_PLUGIN_REFERENT_SEARCH_NO_SUCH_OBJECT, baseDN);
return;
default:
logger.error(ERR_PLUGIN_REFERENT_SEARCH_FAILED, operation.getErrorMessage());
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 modifyDNmap)
{
for(Map.Entry 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 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 mods = new LinkedList<>();
DN entryDN=e.getName();
for(AttributeType type : attributeTypes)
{
if(e.hasAttribute(type))
{
ByteString value = ByteString.valueOfUtf8(oldEntryDN.toString());
if (e.hasValue(type, 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)
{
logger.error(ERR_PLUGIN_REFERENT_MODIFY_FAILED, entryDN, 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 modDNmap) {
synchronized(logFile)
{
try
{
setupWriter();
for(Map.Entry mapEntry : modDNmap.entrySet())
{
writer.write(mapEntry.getKey() + "\t" + mapEntry.getValue());
writer.newLine();
}
writer.flush();
writer.close();
}
catch (IOException io)
{
logger.error(ERR_PLUGIN_REFERENT_CLOSE_LOGFILE, 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 deleteDNset) {
synchronized(logFile)
{
try
{
setupWriter();
for (DN deletedEntryDN : deleteDNset)
{
writer.write(deletedEntryDN.toString());
writer.newLine();
}
writer.flush();
writer.close();
}
catch (IOException io)
{
logger.error(ERR_PLUGIN_REFERENT_CLOSE_LOGFILE, 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.valueOf(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.valueOf(a[1]);
processModifyDN(origDn, movedDN);
}
} catch (DirectoryException ex) {
//This exception should rarely happen since the plugin wrote the DN
//strings originally.
logger.error(ERR_PLUGIN_REFERENT_CANNOT_DECODE_STRING_AS_DN, ex.getMessage());
}
}
reader.close();
logFile.delete();
logFile.createNewFile();
} catch (IOException io) {
logger.error(ERR_PLUGIN_REFERENT_REPLACE_LOGFILE, io.getMessage());
}
}
}
/**
* Return the listener name.
*
* @return The name of the listener.
*
*/
@Override
public String getShutdownListenerName() {
return name;
}
@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(LocalizableMessage 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 true 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) {
logger.traceException(e);
}
processLog();
}
}
}
@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 mods = modifyOperation.getModifications();
final Entry entry = modifyOperation.getModifiedEntry();
/* Make sure the entry belongs to one of the configured naming
* contexts.
*/
DN entryDN = entry.getName();
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;
}
Attribute modifiedAttribute = entry.getExactAttribute(mod.getAttribute().getAttributeDescription());
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();
}
@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.getName();
DN entryBaseDN = getEntryBaseDN(entryDN);
if (entryBaseDN == null)
{
return PluginResult.PreOperation.continueOperationProcessing();
}
for (AttributeType attrType : attributeTypes)
{
final List attrs = entry.getAttribute(attrType, false);
PluginResult.PreOperation result = isIntegrityMaintained(attrs, entryDN, entryBaseDN);
if (result.getResultCode() != ResultCode.SUCCESS)
{
return result;
}
}
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 attr
* attribute.
* @return The SUCCESS if the integrity is maintained or
* CONSTRAINT_VIOLATION oherwise
*/
private PluginResult.PreOperation
isIntegrityMaintained(List 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 attr
* attribute.
* @return The SUCCESS if the integrity is maintained or
* CONSTRAINT_VIOLATION otherwise
*/
private PluginResult.PreOperation isIntegrityMaintained(Attribute attr, DN entryDN, DN entryBaseDN)
{
try
{
for (ByteString attrVal : attr)
{
DN valueEntryDN = DN.valueOf(attrVal);
final Entry valueEntry;
if (currentConfiguration.getCheckReferencesScopeCriteria() == CheckReferencesScopeCriteria.NAMING_CONTEXT
&& valueEntryDN.isInScopeOf(entryBaseDN, SearchScope.SUBORDINATES))
{
return PluginResult.PreOperation.stopProcessing(ResultCode.CONSTRAINT_VIOLATION,
ERR_PLUGIN_REFERENT_NAMINGCONTEXT_MISMATCH.get(valueEntryDN, attr.getName(), entryDN));
}
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, attr.getName(), entryDN));
}
// Verify that the value entry conforms to the filter.
SearchFilter filter = attrFiltMap.get(attr.getAttributeDescription().getAttributeType());
if (filter != null && !filter.matchesEntry(valueEntry))
{
return PluginResult.PreOperation.stopProcessing(ResultCode.CONSTRAINT_VIOLATION,
ERR_PLUGIN_REFERENT_FILTER_MISMATCH.get(valueEntry.getName(), attr.getName(), entryDN, filter));
}
}
}
catch (Exception 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 true if the entry matches any of the
* configured base DNs, and false 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.isInScopeOf(baseDN, SearchScope.SUBORDINATES))
{
namingContext = baseDN;
break;
}
}
return namingContext;
}
}