001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2008-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 * Portions copyright 2011 profiq s.r.o.
017 */
018package org.opends.server.plugins;
019
020import static org.opends.messages.PluginMessages.*;
021import static org.opends.server.protocols.internal.InternalClientConnection.*;
022import static org.opends.server.protocols.internal.Requests.*;
023import static org.opends.server.schema.SchemaConstants.*;
024import static org.opends.server.util.StaticUtils.*;
025
026import java.io.BufferedReader;
027import java.io.BufferedWriter;
028import java.io.File;
029import java.io.FileReader;
030import java.io.FileWriter;
031import java.io.IOException;
032import java.util.Collections;
033import java.util.HashSet;
034import java.util.LinkedHashMap;
035import java.util.LinkedHashSet;
036import java.util.LinkedList;
037import java.util.List;
038import java.util.Map;
039import java.util.Set;
040
041import org.forgerock.i18n.LocalizableMessage;
042import org.forgerock.i18n.LocalizedIllegalArgumentException;
043import org.forgerock.i18n.slf4j.LocalizedLogger;
044import org.forgerock.opendj.config.server.ConfigChangeResult;
045import org.forgerock.opendj.config.server.ConfigException;
046import org.forgerock.opendj.ldap.ByteString;
047import org.forgerock.opendj.ldap.ModificationType;
048import org.forgerock.opendj.ldap.ResultCode;
049import org.forgerock.opendj.ldap.SearchScope;
050import org.forgerock.opendj.ldap.schema.AttributeType;
051import org.opends.server.admin.server.ConfigurationChangeListener;
052import org.opends.server.admin.std.meta.PluginCfgDefn;
053import org.opends.server.admin.std.meta.ReferentialIntegrityPluginCfgDefn.CheckReferencesScopeCriteria;
054import org.opends.server.admin.std.server.PluginCfg;
055import org.opends.server.admin.std.server.ReferentialIntegrityPluginCfg;
056import org.opends.server.api.Backend;
057import org.opends.server.api.DirectoryThread;
058import org.opends.server.api.ServerShutdownListener;
059import org.opends.server.api.plugin.DirectoryServerPlugin;
060import org.opends.server.api.plugin.PluginResult;
061import org.opends.server.api.plugin.PluginType;
062import org.opends.server.core.DeleteOperation;
063import org.opends.server.core.DirectoryServer;
064import org.opends.server.core.ModifyOperation;
065import org.opends.server.protocols.internal.InternalClientConnection;
066import org.opends.server.protocols.internal.InternalSearchOperation;
067import org.opends.server.protocols.internal.SearchRequest;
068import org.opends.server.types.Attribute;
069import org.opends.server.types.Attributes;
070import org.forgerock.opendj.ldap.DN;
071import org.opends.server.types.DirectoryException;
072import org.opends.server.types.Entry;
073import org.opends.server.types.IndexType;
074import org.opends.server.types.Modification;
075import org.opends.server.types.SearchFilter;
076import org.opends.server.types.SearchResultEntry;
077import org.opends.server.types.operation.PostOperationDeleteOperation;
078import org.opends.server.types.operation.PostOperationModifyDNOperation;
079import org.opends.server.types.operation.PreOperationAddOperation;
080import org.opends.server.types.operation.PreOperationModifyOperation;
081import org.opends.server.types.operation.SubordinateModifyDNOperation;
082
083/**
084 * This class implements a Directory Server post operation plugin that performs
085 * Referential Integrity processing on successful delete and modify DN
086 * operations. The plugin uses a set of configuration criteria to determine
087 * what attribute types to check referential integrity on, and, the set of
088 * base DNs to search for entries that might need referential integrity
089 * processing. If none of these base DNs are specified in the configuration,
090 * then the public naming contexts are used as the base DNs by default.
091 * <BR><BR>
092 * The plugin also has an option to process changes in background using
093 * a thread that wakes up periodically looking for change records in a log
094 * file.
095 */
096public class ReferentialIntegrityPlugin
097        extends DirectoryServerPlugin<ReferentialIntegrityPluginCfg>
098        implements ConfigurationChangeListener<ReferentialIntegrityPluginCfg>,
099                   ServerShutdownListener
100{
101  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
102
103
104
105  /** Current plugin configuration. */
106  private ReferentialIntegrityPluginCfg currentConfiguration;
107
108  /** List of attribute types that will be checked during referential integrity processing. */
109  private LinkedHashSet<AttributeType> attributeTypes = new LinkedHashSet<>();
110  /** List of base DNs that limit the scope of the referential integrity checking. */
111  private Set<DN> baseDNs = new LinkedHashSet<>();
112
113  /**
114   * The update interval the background thread uses. If it is 0, then
115   * the changes are processed in foreground.
116   */
117  private long interval;
118
119  /** The flag used by the background thread to check if it should exit. */
120  private boolean stopRequested;
121
122  /** The thread name. */
123  private static final String name =
124      "Referential Integrity Background Update Thread";
125
126  /**
127   * The name of the logfile that the update thread uses to process change
128   * records. Defaults to "logs/referint", but can be changed in the
129   * configuration.
130   */
131  private String logFileName;
132
133  /** The File class that logfile corresponds to. */
134  private File logFile;
135
136  /** The Thread class that the background thread corresponds to. */
137  private Thread backGroundThread;
138
139  /**
140   * Used to save a map in the modifyDN operation attachment map that holds
141   * the old entry DNs and the new entry DNs related to a modify DN rename to
142   * new superior operation.
143   */
144  public static final String MODIFYDN_DNS="modifyDNs";
145
146  /**
147   * Used to save a set in the delete operation attachment map that
148   * holds the subordinate entry DNs related to a delete operation.
149   */
150  public static final String DELETE_DNS="deleteDNs";
151
152  /**
153   * The buffered reader that is used to read the log file by the background
154   * thread.
155   */
156  private BufferedReader reader;
157
158  /**
159   * The buffered writer that is used to write update records in the log
160   * when the plugin is in background processing mode.
161   */
162  private BufferedWriter writer;
163
164  /**
165   * Specifies the mapping between the attribute type (specified in the
166   * attributeTypes list) and the filter which the plugin should use
167   * to verify the integrity of the value of the given attribute.
168   */
169  private LinkedHashMap<AttributeType, SearchFilter> attrFiltMap = new LinkedHashMap<>();
170
171  @Override
172  public final void initializePlugin(Set<PluginType> pluginTypes,
173                                     ReferentialIntegrityPluginCfg pluginCfg)
174         throws ConfigException
175  {
176    pluginCfg.addReferentialIntegrityChangeListener(this);
177    LinkedList<LocalizableMessage> unacceptableReasons = new LinkedList<>();
178
179    if (!isConfigurationAcceptable(pluginCfg, unacceptableReasons))
180    {
181      throw new ConfigException(unacceptableReasons.getFirst());
182    }
183
184    applyConfigurationChange(pluginCfg);
185
186    // Set up log file. Note: it is not allowed to change once the plugin is active.
187    setUpLogFile(pluginCfg.getLogFile());
188    interval=pluginCfg.getUpdateInterval();
189
190    //Set up background processing if interval > 0.
191    if(interval > 0)
192    {
193      setUpBackGroundProcessing();
194    }
195  }
196
197
198
199  @Override
200  public ConfigChangeResult applyConfigurationChange(
201          ReferentialIntegrityPluginCfg newConfiguration)
202  {
203    final ConfigChangeResult ccr = new ConfigChangeResult();
204
205    //Load base DNs from new configuration.
206    LinkedHashSet<DN> newConfiguredBaseDNs = new LinkedHashSet<>(newConfiguration.getBaseDN());
207    //Load attribute types from new configuration.
208    LinkedHashSet<AttributeType> newAttributeTypes =
209            new LinkedHashSet<>(newConfiguration.getAttributeType());
210
211    // Load the attribute-filter mapping
212    LinkedHashMap<AttributeType, SearchFilter> newAttrFiltMap = new LinkedHashMap<>();
213
214    for (String attrFilt : newConfiguration.getCheckReferencesFilterCriteria())
215    {
216      int sepInd = attrFilt.lastIndexOf(":");
217      String attr = attrFilt.substring(0, sepInd);
218      String filtStr = attrFilt.substring(sepInd + 1);
219
220      AttributeType attrType = DirectoryServer.getAttributeType(attr);
221      try
222      {
223        newAttrFiltMap.put(attrType, SearchFilter.createFilterFromString(filtStr));
224      }
225      catch (DirectoryException unexpected)
226      {
227        // This should never happen because the filter has already been verified.
228        logger.error(unexpected.getMessageObject());
229      }
230    }
231
232    //User is not allowed to change the logfile name, append a message that the
233    //server needs restarting for change to take effect.
234    // The first time the plugin is initialised the 'logFileName' is
235    // not initialised, so in order to verify if it is equal to the new
236    // log file name, we have to make sure the variable is not null.
237    String newLogFileName=newConfiguration.getLogFile();
238    if(logFileName != null && !logFileName.equals(newLogFileName))
239    {
240      ccr.setAdminActionRequired(true);
241      ccr.addMessage(INFO_PLUGIN_REFERENT_LOGFILE_CHANGE_REQUIRES_RESTART.get(logFileName, newLogFileName));
242    }
243
244    //Switch to the new lists.
245    baseDNs = newConfiguredBaseDNs;
246    attributeTypes = newAttributeTypes;
247    attrFiltMap = newAttrFiltMap;
248
249    //If the plugin is enabled and the interval has changed, process that
250    //change. The change might start or stop the background processing thread.
251    long newInterval=newConfiguration.getUpdateInterval();
252    if (newConfiguration.isEnabled() && newInterval != interval)
253    {
254      processIntervalChange(newInterval, ccr.getMessages());
255    }
256
257    currentConfiguration = newConfiguration;
258    return ccr;
259  }
260
261  @Override
262  public boolean isConfigurationAcceptable(PluginCfg configuration,
263                                           List<LocalizableMessage> unacceptableReasons)
264  {
265    boolean isAcceptable = true;
266    ReferentialIntegrityPluginCfg pluginCfg =
267         (ReferentialIntegrityPluginCfg) configuration;
268
269    for (PluginCfgDefn.PluginType t : pluginCfg.getPluginType())
270    {
271      switch (t)
272      {
273        case POSTOPERATIONDELETE:
274        case POSTOPERATIONMODIFYDN:
275        case SUBORDINATEMODIFYDN:
276        case SUBORDINATEDELETE:
277        case PREOPERATIONMODIFY:
278        case PREOPERATIONADD:
279          // These are acceptable.
280          break;
281
282        default:
283          isAcceptable = false;
284          unacceptableReasons.add(ERR_PLUGIN_REFERENT_INVALID_PLUGIN_TYPE.get(t));
285      }
286    }
287
288    Set<DN> cfgBaseDNs = pluginCfg.getBaseDN();
289    if (cfgBaseDNs == null || cfgBaseDNs.isEmpty())
290    {
291      cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
292    }
293
294    // Iterate through all of the defined attribute types and ensure that they
295    // have acceptable syntaxes and that they are indexed for equality below all
296    // base DNs.
297    Set<AttributeType> theAttributeTypes = pluginCfg.getAttributeType();
298    for (AttributeType type : theAttributeTypes)
299    {
300      if (! isAttributeSyntaxValid(type))
301      {
302        isAcceptable = false;
303        unacceptableReasons.add(
304                       ERR_PLUGIN_REFERENT_INVALID_ATTRIBUTE_SYNTAX.get(
305                            type.getNameOrOID(),
306                             type.getSyntax().getName()));
307      }
308
309      for (DN baseDN : cfgBaseDNs)
310      {
311        Backend<?> b = DirectoryServer.getBackend(baseDN);
312        if (b != null && !b.isIndexed(type, IndexType.EQUALITY))
313        {
314          isAcceptable = false;
315          unacceptableReasons.add(ERR_PLUGIN_REFERENT_ATTR_UNINDEXED.get(
316              pluginCfg.dn(), type.getNameOrOID(), b.getBackendID()));
317        }
318      }
319    }
320
321    /* Iterate through the attribute-filter mapping and verify that the
322     * map contains attributes listed in the attribute-type parameter
323     * and that the filter is valid.
324     */
325
326    for (String attrFilt : pluginCfg.getCheckReferencesFilterCriteria())
327    {
328      int sepInd = attrFilt.lastIndexOf(":");
329      String attr = attrFilt.substring(0, sepInd).trim();
330      String filtStr = attrFilt.substring(sepInd + 1).trim();
331
332      /* TODO: strip the ;options part? */
333
334      /* Get the attribute type for the given attribute. The attribute
335       * type has to be present in the attributeType list.
336       */
337
338      AttributeType attrType = DirectoryServer.getAttributeType(attr);
339      if (attrType.isPlaceHolder() || !theAttributeTypes.contains(attrType))
340      {
341        isAcceptable = false;
342        unacceptableReasons.add(ERR_PLUGIN_REFERENT_ATTR_NOT_LISTED.get(attr));
343      }
344
345      /* Verify the filter.
346       */
347      try
348      {
349        SearchFilter.createFilterFromString(filtStr);
350      }
351      catch (DirectoryException de)
352      {
353        isAcceptable = false;
354        unacceptableReasons.add(
355          ERR_PLUGIN_REFERENT_BAD_FILTER.get(filtStr, de.getMessage()));
356      }
357    }
358
359    return isAcceptable;
360  }
361
362  @Override
363  public boolean isConfigurationChangeAcceptable(
364          ReferentialIntegrityPluginCfg configuration,
365          List<LocalizableMessage> unacceptableReasons)
366  {
367    return isConfigurationAcceptable(configuration, unacceptableReasons);
368  }
369
370  @SuppressWarnings("unchecked")
371  @Override
372  public PluginResult.PostOperation
373         doPostOperation(PostOperationModifyDNOperation
374          modifyDNOperation)
375  {
376    // If the operation itself failed, then we don't need to do anything because
377    // nothing changed.
378    if (modifyDNOperation.getResultCode() != ResultCode.SUCCESS)
379    {
380      return PluginResult.PostOperation.continueOperationProcessing();
381    }
382
383    Map<DN,DN>modDNmap=
384         (Map<DN, DN>) modifyDNOperation.getAttachment(MODIFYDN_DNS);
385    if(modDNmap == null)
386    {
387      modDNmap = new LinkedHashMap<>();
388      modifyDNOperation.setAttachment(MODIFYDN_DNS, modDNmap);
389    }
390    DN oldEntryDN=modifyDNOperation.getOriginalEntry().getName();
391    DN newEntryDN=modifyDNOperation.getUpdatedEntry().getName();
392    modDNmap.put(oldEntryDN, newEntryDN);
393
394    processModifyDN(modDNmap, interval != 0);
395
396    return PluginResult.PostOperation.continueOperationProcessing();
397  }
398
399  @SuppressWarnings("unchecked")
400  @Override
401  public PluginResult.PostOperation doPostOperation(
402              PostOperationDeleteOperation deleteOperation)
403  {
404    // If the operation itself failed, then we don't need to do anything because
405    // nothing changed.
406    if (deleteOperation.getResultCode() != ResultCode.SUCCESS)
407    {
408      return PluginResult.PostOperation.continueOperationProcessing();
409    }
410
411    Set<DN> deleteDNset =
412         (Set<DN>) deleteOperation.getAttachment(DELETE_DNS);
413    if(deleteDNset == null)
414    {
415      deleteDNset = new HashSet<>();
416      deleteOperation.setAttachment(MODIFYDN_DNS, deleteDNset);
417    }
418    deleteDNset.add(deleteOperation.getEntryDN());
419
420    processDelete(deleteDNset, interval != 0);
421    return PluginResult.PostOperation.continueOperationProcessing();
422  }
423
424  @SuppressWarnings("unchecked")
425  @Override
426  public PluginResult.SubordinateModifyDN processSubordinateModifyDN(
427          SubordinateModifyDNOperation modifyDNOperation, Entry oldEntry,
428          Entry newEntry, List<Modification> modifications)
429  {
430    //This cast gives an unchecked cast warning, suppress it since the cast
431    //is ok.
432    Map<DN,DN>modDNmap=
433         (Map<DN, DN>) modifyDNOperation.getAttachment(MODIFYDN_DNS);
434    if(modDNmap == null)
435    {
436      // First time through, create the map and set it in the operation attachment.
437      modDNmap = new LinkedHashMap<>();
438      modifyDNOperation.setAttachment(MODIFYDN_DNS, modDNmap);
439    }
440    modDNmap.put(oldEntry.getName(), newEntry.getName());
441    return PluginResult.SubordinateModifyDN.continueOperationProcessing();
442  }
443
444  @SuppressWarnings("unchecked")
445  @Override
446  public PluginResult.SubordinateDelete processSubordinateDelete(
447          DeleteOperation deleteOperation, Entry entry)
448  {
449    // This cast gives an unchecked cast warning, suppress it since the cast is ok.
450    Set<DN> deleteDNset = (Set<DN>) deleteOperation.getAttachment(DELETE_DNS);
451    if(deleteDNset == null)
452    {
453      // First time through, create the set and set it in the operation attachment.
454      deleteDNset = new HashSet<>();
455      deleteOperation.setAttachment(DELETE_DNS, deleteDNset);
456    }
457    deleteDNset.add(entry.getName());
458    return PluginResult.SubordinateDelete.continueOperationProcessing();
459  }
460
461  /**
462   * Verify that the specified attribute has either a distinguished name syntax
463   * or "name and optional UID" syntax.
464   *
465   * @param attribute The attribute to check the syntax of.
466   * @return  Returns <code>true</code> if the attribute has a valid syntax.
467   */
468  private boolean isAttributeSyntaxValid(AttributeType attribute)
469  {
470    return attribute.getSyntax().getOID().equals(SYNTAX_DN_OID) ||
471            attribute.getSyntax().getOID().equals(SYNTAX_NAME_AND_OPTIONAL_UID_OID);
472  }
473
474  /**
475   * Process the specified new interval value. This processing depends on what
476   * the current interval value is and new value will be. The values have been
477   * checked for equality at this point and are not equal.
478   *
479   * If the old interval is 0, then the server is in foreground mode and
480   * the background thread needs to be started using the new interval value.
481   *
482   * If the new interval value is 0, the the server is in background mode
483   * and the the background thread needs to be stopped.
484   *
485   * If the user just wants to change the interval value, the background thread
486   * needs to be interrupted so that it can use the new interval value.
487   *
488   * @param newInterval The new interval value to use.
489   *
490   * @param msgs An array list of messages that thread stop and start messages
491   *             can be added to.
492   */
493  private void processIntervalChange(long newInterval, List<LocalizableMessage> msgs)
494  {
495    if(interval == 0) {
496      DirectoryServer.registerShutdownListener(this);
497      interval=newInterval;
498      msgs.add(INFO_PLUGIN_REFERENT_BACKGROUND_PROCESSING_STARTING.get(interval));
499      setUpBackGroundProcessing();
500    } else if(newInterval == 0) {
501      LocalizableMessage message=
502              INFO_PLUGIN_REFERENT_BACKGROUND_PROCESSING_STOPPING.get();
503      msgs.add(message);
504      processServerShutdown(message);
505      interval=newInterval;
506    } else {
507      interval=newInterval;
508      backGroundThread.interrupt();
509      msgs.add(INFO_PLUGIN_REFERENT_BACKGROUND_PROCESSING_UPDATE_INTERVAL_CHANGED.get(interval, newInterval));
510    }
511  }
512
513  /**
514   * Process a modify DN post operation using the specified map of old and new
515   * entry DNs.  The boolean "log" is used to determine if the  map
516   * is written to the log file for the background thread to pick up. If the
517   * map is to be processed in foreground, than each base DN or public
518   * naming context (if the base DN configuration is empty) is processed.
519   *
520   * @param modDNMap  The map of old entry and new entry DNs from the modify
521   *                  DN operation.
522   *
523   * @param log Set to <code>true</code> if the map should be written to a log
524   *            file so that the background thread can process the changes at
525   *            a later time.
526   *
527   */
528  private void processModifyDN(Map<DN, DN> modDNMap, boolean log)
529  {
530    if(modDNMap != null)
531    {
532      if(log)
533      {
534        writeLog(modDNMap);
535      }
536      else
537      {
538        for(DN baseDN : getBaseDNsToSearch())
539        {
540          doBaseDN(baseDN, modDNMap);
541        }
542      }
543    }
544  }
545
546  /**
547   * Used by both the background thread and the delete post operation to
548   * process a delete operation on the specified entry DN.  The
549   * boolean "log" is used to determine if the DN is written to the log file
550   * for the background thread to pick up. This value is set to false if the
551   * background thread is processing changes. If this method is being called
552   * by a delete post operation, then setting the "log" value to false will
553   * cause the DN to be processed in foreground
554   * <p>
555   * If the DN is to be processed, than each base DN or public naming
556   * context (if the base DN configuration is empty) is checked to see if
557   * entries under it contain references to the deleted entry DN that need
558   * to be removed.
559   *
560   * @param entryDN  The DN of the deleted entry.
561   *
562   * @param log Set to <code>true</code> if the DN should be written to a log
563   *            file so that the background thread can process the change at
564   *            a later time.
565   *
566   */
567  private void processDelete(Set<DN> deleteDNset, boolean log)
568  {
569    if(log)
570    {
571      writeLog(deleteDNset);
572    }
573    else
574    {
575      for(DN baseDN : getBaseDNsToSearch())
576      {
577        doBaseDN(baseDN, deleteDNset);
578      }
579    }
580  }
581
582  /**
583   * Used by the background thread to process the specified old entry DN and
584   * new entry DN. Each base DN or public naming context (if the base DN
585   * configuration is empty) is checked to see  if they contain entries with
586   * references to the old entry DN that need to be changed to the new entry DN.
587   *
588   * @param oldEntryDN  The entry DN before the modify DN operation.
589   *
590   * @param newEntryDN The entry DN after the modify DN operation.
591   *
592   */
593  private void processModifyDN(DN oldEntryDN, DN newEntryDN)
594  {
595    for(DN baseDN : getBaseDNsToSearch())
596    {
597      searchBaseDN(baseDN, oldEntryDN, newEntryDN);
598    }
599  }
600
601  /**
602   * Return a set of DNs that are used to search for references under. If the
603   * base DN configuration set is empty, then the public naming contexts
604   * are used.
605   *
606   * @return A set of DNs to use in the reference searches.
607   *
608   */
609  private Set<DN> getBaseDNsToSearch()
610  {
611    if (baseDNs.isEmpty())
612    {
613      return DirectoryServer.getPublicNamingContexts().keySet();
614    }
615    return baseDNs;
616  }
617
618  /**
619   * Search a base DN using a filter built from the configured attribute
620   * types and the specified old entry DN. For each entry that is found from
621   * the search, delete the old entry DN from the entry. If the new entry
622   * DN is not null, then add it to the entry.
623   *
624   * @param baseDN  The DN to base the search at.
625   *
626   * @param oldEntryDN The old entry DN that needs to be deleted or replaced.
627   *
628   * @param newEntryDN The new entry DN that needs to be added. May be null
629   *                   if the original operation was a delete.
630   *
631   */
632  private void searchBaseDN(DN baseDN, DN oldEntryDN, DN newEntryDN)
633  {
634    //Build an equality search with all of the configured attribute types
635    //and the old entry DN.
636    HashSet<SearchFilter> componentFilters=new HashSet<>();
637    for(AttributeType attributeType : attributeTypes)
638    {
639      componentFilters.add(SearchFilter.createEqualityFilter(attributeType,
640          ByteString.valueOfUtf8(oldEntryDN.toString())));
641    }
642
643    SearchFilter orFilter = SearchFilter.createORFilter(componentFilters);
644    final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, orFilter);
645    InternalSearchOperation operation = getRootConnection().processSearch(request);
646
647    switch (operation.getResultCode().asEnum())
648    {
649      case SUCCESS:
650        break;
651
652      case NO_SUCH_OBJECT:
653        logger.debug(INFO_PLUGIN_REFERENT_SEARCH_NO_SUCH_OBJECT, baseDN);
654        return;
655
656      default:
657        logger.error(ERR_PLUGIN_REFERENT_SEARCH_FAILED, operation.getErrorMessage());
658        return;
659    }
660
661    for (SearchResultEntry entry : operation.getSearchEntries())
662    {
663      deleteAddAttributesEntry(entry, oldEntryDN, newEntryDN);
664    }
665  }
666
667  /**
668   * This method is used in foreground processing of a modify DN operation.
669   * It uses the specified map to perform base DN searching for each map
670   * entry. The key is the old entry DN and the value is the
671   * new entry DN.
672   *
673   * @param baseDN The DN to base the search at.
674   *
675   * @param modifyDNmap The map containing the modify DN old and new entry DNs.
676   *
677   */
678  private void doBaseDN(DN baseDN, Map<DN,DN> modifyDNmap)
679  {
680    for(Map.Entry<DN,DN> mapEntry: modifyDNmap.entrySet())
681    {
682      searchBaseDN(baseDN, mapEntry.getKey(), mapEntry.getValue());
683    }
684  }
685
686  /**
687   * This method is used in foreground processing of a delete operation.
688   * It uses the specified set to perform base DN searching for each
689   * element.
690   *
691   * @param baseDN The DN to base the search at.
692   *
693   * @param deleteDNset The set containing the delete DNs.
694   *
695   */
696  private void doBaseDN(DN baseDN, Set<DN> deleteDNset)
697  {
698    for(DN deletedEntryDN : deleteDNset)
699    {
700      searchBaseDN(baseDN, deletedEntryDN, null);
701    }
702  }
703
704  /**
705   * For each attribute type, delete the specified old entry DN and
706   * optionally add the specified new entry DN if the DN is not null.
707   * The specified entry is used to see if it contains each attribute type so
708   * those types that the entry contains can be modified. An internal modify
709   * is performed to change the entry.
710   *
711   * @param e The entry that contains the old references.
712   *
713   * @param oldEntryDN The old entry DN to remove references to.
714   *
715   * @param newEntryDN The new entry DN to add a reference to, if it is not
716   *                   null.
717   *
718   */
719  private void deleteAddAttributesEntry(Entry e, DN oldEntryDN, DN newEntryDN)
720  {
721    LinkedList<Modification> mods = new LinkedList<>();
722    DN entryDN=e.getName();
723    for(AttributeType type : attributeTypes)
724    {
725      if(e.hasAttribute(type))
726      {
727        ByteString value = ByteString.valueOfUtf8(oldEntryDN.toString());
728        if (e.hasValue(type, value))
729        {
730          mods.add(new Modification(ModificationType.DELETE, Attributes
731              .create(type, value)));
732
733          // If the new entry DN exists, create an ADD modification for it.
734          if(newEntryDN != null)
735          {
736            mods.add(new Modification(ModificationType.ADD, Attributes
737                .create(type, newEntryDN.toString())));
738          }
739        }
740      }
741    }
742
743    InternalClientConnection conn =
744            InternalClientConnection.getRootConnection();
745    ModifyOperation modifyOperation =
746            conn.processModify(entryDN, mods);
747    if(modifyOperation.getResultCode() != ResultCode.SUCCESS)
748    {
749      logger.error(ERR_PLUGIN_REFERENT_MODIFY_FAILED, entryDN, modifyOperation.getErrorMessage());
750    }
751  }
752
753  /**
754   * Sets up the log file that the plugin can write update recored to and
755   * the background thread can use to read update records from. The specifed
756   * log file name is the name to use for the file. If the file exists from
757   * a previous run, use it.
758   *
759   * @param logFileName The name of the file to use, may be absolute.
760   *
761   * @throws ConfigException If a new file cannot be created if needed.
762   *
763   */
764  private void setUpLogFile(String logFileName)
765          throws ConfigException
766  {
767    this.logFileName=logFileName;
768    logFile=getFileForPath(logFileName);
769
770    try
771    {
772      if(!logFile.exists())
773      {
774        logFile.createNewFile();
775      }
776    }
777    catch (IOException io)
778    {
779      throw new ConfigException(ERR_PLUGIN_REFERENT_CREATE_LOGFILE.get(
780                                     io.getMessage()), io);
781    }
782  }
783
784  /**
785   * Sets up a buffered writer that the plugin can use to write update records
786   * with.
787   *
788   * @throws IOException If a new file writer cannot be created.
789   *
790   */
791  private void setupWriter() throws IOException {
792    writer=new BufferedWriter(new FileWriter(logFile, true));
793  }
794
795
796  /**
797   * Sets up a buffered reader that the background thread can use to read
798   * update records with.
799   *
800   * @throws IOException If a new file reader cannot be created.
801   *
802   */
803  private void setupReader() throws IOException {
804    reader=new BufferedReader(new FileReader(logFile));
805  }
806
807  /**
808   * Write the specified map of old entry and new entry DNs to the log
809   * file. Each entry of the map is a line in the file, the key is the old
810   * entry normalized DN and the value is the new entry normalized DN.
811   * The DNs are separated by the tab character. This map is related to a
812   * modify DN operation.
813   *
814   * @param modDNmap The map of old entry and new entry DNs.
815   *
816   */
817  private void writeLog(Map<DN,DN> modDNmap) {
818    synchronized(logFile)
819    {
820      try
821      {
822        setupWriter();
823        for(Map.Entry<DN,DN> mapEntry : modDNmap.entrySet())
824        {
825          writer.write(mapEntry.getKey() + "\t" + mapEntry.getValue());
826          writer.newLine();
827        }
828        writer.flush();
829        writer.close();
830      }
831      catch (IOException io)
832      {
833        logger.error(ERR_PLUGIN_REFERENT_CLOSE_LOGFILE, io.getMessage());
834      }
835    }
836  }
837
838  /**
839   * Write the specified entry DNs to the log file.
840   * These entry DNs are related to a delete operation.
841   *
842   * @param deletedEntryDN The DN of the deleted entry.
843   *
844   */
845  private void writeLog(Set<DN> deleteDNset) {
846    synchronized(logFile)
847    {
848      try
849      {
850        setupWriter();
851        for (DN deletedEntryDN : deleteDNset)
852        {
853          writer.write(deletedEntryDN.toString());
854          writer.newLine();
855        }
856        writer.flush();
857        writer.close();
858      }
859      catch (IOException io)
860      {
861        logger.error(ERR_PLUGIN_REFERENT_CLOSE_LOGFILE, io.getMessage());
862      }
863    }
864  }
865
866  /**
867   * Process all of the records in the log file. Each line of the file is read
868   * and parsed to determine if it was a delete operation (a single normalized
869   * DN) or a modify DN operation (two normalized DNs separated by a tab). The
870   * corresponding operation method is called to perform the referential
871   * integrity processing as though the operation was just processed. After
872   * all of the records in log file have been processed, the log file is
873   * cleared so that new records can be added.
874   *
875   */
876  private void processLog() {
877    synchronized(logFile) {
878      try {
879        if(logFile.length() == 0)
880        {
881          return;
882        }
883
884        setupReader();
885        String line;
886        while((line=reader.readLine()) != null) {
887          try {
888            String[] a=line.split("[\t]");
889            DN origDn = DN.valueOf(a[0]);
890            //If there is only a single DN string than it must be a delete.
891            if(a.length == 1) {
892              processDelete(Collections.singleton(origDn), false);
893            } else {
894              DN movedDN=DN.valueOf(a[1]);
895              processModifyDN(origDn, movedDN);
896            }
897          } catch (LocalizedIllegalArgumentException e) {
898            //This exception should rarely happen since the plugin wrote the DN
899            //strings originally.
900            logger.error(ERR_PLUGIN_REFERENT_CANNOT_DECODE_STRING_AS_DN, e.getMessage());
901          }
902        }
903        reader.close();
904        logFile.delete();
905        logFile.createNewFile();
906      } catch (IOException io) {
907        logger.error(ERR_PLUGIN_REFERENT_REPLACE_LOGFILE, io.getMessage());
908      }
909    }
910  }
911
912  /**
913   * Return the listener name.
914   *
915   * @return The name of the listener.
916   *
917   */
918  @Override
919  public String getShutdownListenerName() {
920    return name;
921  }
922
923  @Override
924  public final void finalizePlugin() {
925    currentConfiguration.removeReferentialIntegrityChangeListener(this);
926    if(interval > 0)
927    {
928      processServerShutdown(null);
929    }
930  }
931
932  /**
933   * Process a server shutdown. If the background thread is running it needs
934   * to be interrupted so it can read the stop request variable and exit.
935   *
936   * @param reason The reason message for the shutdown.
937   *
938   */
939  @Override
940  public void processServerShutdown(LocalizableMessage reason)
941  {
942    stopRequested = true;
943
944    // Wait for back ground thread to terminate
945    while (backGroundThread != null && backGroundThread.isAlive()) {
946      try {
947        // Interrupt if its sleeping
948        backGroundThread.interrupt();
949        backGroundThread.join();
950      }
951      catch (InterruptedException ex) {
952        //Expected.
953      }
954    }
955    DirectoryServer.deregisterShutdownListener(this);
956    backGroundThread=null;
957  }
958
959
960  /**
961   * Returns the interval time converted to milliseconds.
962   *
963   * @return The interval time for the background thread.
964   */
965  private long getInterval() {
966    return interval * 1000;
967  }
968
969  /**
970   * Sets up background processing of referential integrity by creating a
971   * new background thread to process updates.
972   *
973   */
974  private void setUpBackGroundProcessing()  {
975    if(backGroundThread == null) {
976      DirectoryServer.registerShutdownListener(this);
977      stopRequested = false;
978      backGroundThread = new BackGroundThread();
979      backGroundThread.start();
980    }
981  }
982
983
984  /**
985   * Used by the background thread to determine if it should exit.
986   *
987   * @return Returns <code>true</code> if the background thread should exit.
988   *
989   */
990  private boolean isShuttingDown()  {
991    return stopRequested;
992  }
993
994  /**
995   * The background referential integrity processing thread. Wakes up after
996   * sleeping for a configurable interval and checks the log file for update
997   * records.
998   *
999   */
1000  private class BackGroundThread extends DirectoryThread {
1001
1002    /**
1003     * Constructor for the background thread.
1004     */
1005    public
1006    BackGroundThread() {
1007      super(name);
1008    }
1009
1010    /**
1011     * Run method for the background thread.
1012     */
1013    @Override
1014    public void run() {
1015      while(!isShuttingDown())  {
1016        try {
1017          sleep(getInterval());
1018        } catch(InterruptedException e) {
1019          continue;
1020        } catch(Exception e) {
1021          logger.traceException(e);
1022        }
1023        processLog();
1024      }
1025    }
1026  }
1027
1028  @Override
1029  public PluginResult.PreOperation doPreOperation(
1030    PreOperationModifyOperation modifyOperation)
1031  {
1032    /* Skip the integrity checks if the enforcing is not enabled
1033     */
1034
1035    if (!currentConfiguration.isCheckReferences())
1036    {
1037      return PluginResult.PreOperation.continueOperationProcessing();
1038    }
1039
1040    final List<Modification> mods = modifyOperation.getModifications();
1041    final Entry entry = modifyOperation.getModifiedEntry();
1042
1043    /* Make sure the entry belongs to one of the configured naming
1044     * contexts.
1045     */
1046    DN entryDN = entry.getName();
1047    DN entryBaseDN = getEntryBaseDN(entryDN);
1048    if (entryBaseDN == null)
1049    {
1050      return PluginResult.PreOperation.continueOperationProcessing();
1051    }
1052
1053    for (Modification mod : mods)
1054    {
1055      final ModificationType modType = mod.getModificationType();
1056
1057      /* Process only ADD and REPLACE modification types.
1058       */
1059      if (modType != ModificationType.ADD
1060          && modType != ModificationType.REPLACE)
1061      {
1062        break;
1063      }
1064
1065      Attribute modifiedAttribute = entry.getExactAttribute(mod.getAttribute().getAttributeDescription());
1066      if (modifiedAttribute != null)
1067      {
1068        PluginResult.PreOperation result =
1069        isIntegrityMaintained(modifiedAttribute, entryDN, entryBaseDN);
1070        if (result.getResultCode() != ResultCode.SUCCESS)
1071        {
1072          return result;
1073        }
1074      }
1075    }
1076
1077    /* At this point, everything is fine.
1078     */
1079    return PluginResult.PreOperation.continueOperationProcessing();
1080  }
1081
1082  @Override
1083  public PluginResult.PreOperation doPreOperation(PreOperationAddOperation addOperation)
1084  {
1085    // Skip the integrity checks if the enforcing is not enabled.
1086    if (!currentConfiguration.isCheckReferences())
1087    {
1088      return PluginResult.PreOperation.continueOperationProcessing();
1089    }
1090
1091    final Entry entry = addOperation.getEntryToAdd();
1092
1093    // Make sure the entry belongs to one of the configured naming contexts.
1094    DN entryDN = entry.getName();
1095    DN entryBaseDN = getEntryBaseDN(entryDN);
1096    if (entryBaseDN == null)
1097    {
1098      return PluginResult.PreOperation.continueOperationProcessing();
1099    }
1100
1101    for (AttributeType attrType : attributeTypes)
1102    {
1103      final List<Attribute> attrs = entry.getAttribute(attrType, false);
1104      PluginResult.PreOperation result = isIntegrityMaintained(attrs, entryDN, entryBaseDN);
1105      if (result.getResultCode() != ResultCode.SUCCESS)
1106      {
1107        return result;
1108      }
1109    }
1110
1111    return PluginResult.PreOperation.continueOperationProcessing();
1112  }
1113
1114  /**
1115   * Verifies that the integrity of values is maintained.
1116   * @param attrs   Attribute list which refers to another entry in the
1117   *                directory.
1118   * @param entryDN DN of the entry which contains the <CODE>attr</CODE>
1119   *                attribute.
1120   * @return        The SUCCESS if the integrity is maintained or
1121   *                CONSTRAINT_VIOLATION oherwise
1122   */
1123  private PluginResult.PreOperation
1124    isIntegrityMaintained(List<Attribute> attrs, DN entryDN, DN entryBaseDN)
1125  {
1126    for(Attribute attr : attrs)
1127    {
1128      PluginResult.PreOperation result =
1129          isIntegrityMaintained(attr, entryDN, entryBaseDN);
1130      if (result != PluginResult.PreOperation.continueOperationProcessing())
1131      {
1132        return result;
1133      }
1134    }
1135
1136    return PluginResult.PreOperation.continueOperationProcessing();
1137  }
1138
1139  /**
1140   * Verifies that the integrity of values is maintained.
1141   * @param attr    Attribute which refers to another entry in the
1142   *                directory.
1143   * @param entryDN DN of the entry which contains the <CODE>attr</CODE>
1144   *                attribute.
1145   * @return        The SUCCESS if the integrity is maintained or
1146   *                CONSTRAINT_VIOLATION otherwise
1147   */
1148  private PluginResult.PreOperation isIntegrityMaintained(Attribute attr, DN entryDN, DN entryBaseDN)
1149  {
1150    try
1151    {
1152      for (ByteString attrVal : attr)
1153      {
1154        DN valueEntryDN = DN.valueOf(attrVal);
1155
1156        final Entry valueEntry;
1157        if (currentConfiguration.getCheckReferencesScopeCriteria() == CheckReferencesScopeCriteria.NAMING_CONTEXT
1158            && valueEntryDN.isInScopeOf(entryBaseDN, SearchScope.SUBORDINATES))
1159        {
1160          return PluginResult.PreOperation.stopProcessing(ResultCode.CONSTRAINT_VIOLATION,
1161              ERR_PLUGIN_REFERENT_NAMINGCONTEXT_MISMATCH.get(valueEntryDN, attr.getName(), entryDN));
1162        }
1163        valueEntry = DirectoryServer.getEntry(valueEntryDN);
1164
1165        // Verify that the value entry exists in the backend.
1166        if (valueEntry == null)
1167        {
1168          return PluginResult.PreOperation.stopProcessing(ResultCode.CONSTRAINT_VIOLATION,
1169            ERR_PLUGIN_REFERENT_ENTRY_MISSING.get(valueEntryDN, attr.getName(), entryDN));
1170        }
1171
1172        // Verify that the value entry conforms to the filter.
1173        SearchFilter filter = attrFiltMap.get(attr.getAttributeDescription().getAttributeType());
1174        if (filter != null && !filter.matchesEntry(valueEntry))
1175        {
1176          return PluginResult.PreOperation.stopProcessing(ResultCode.CONSTRAINT_VIOLATION,
1177            ERR_PLUGIN_REFERENT_FILTER_MISMATCH.get(valueEntry.getName(), attr.getName(), entryDN, filter));
1178        }
1179      }
1180    }
1181    catch (Exception de)
1182    {
1183      return PluginResult.PreOperation.stopProcessing(ResultCode.OTHER,
1184        ERR_PLUGIN_REFERENT_EXCEPTION.get(de.getLocalizedMessage()));
1185    }
1186
1187    return PluginResult.PreOperation.continueOperationProcessing();
1188  }
1189
1190  /**
1191   * Verifies if the entry with the specified DN belongs to the
1192   * configured naming contexts.
1193   * @param dn DN of the entry.
1194   * @return Returns <code>true</code> if the entry matches any of the
1195   *         configured base DNs, and <code>false</code> if not.
1196   */
1197  private DN getEntryBaseDN(DN dn)
1198  {
1199    /* Verify that the entry belongs to one of the configured naming
1200     * contexts.
1201     */
1202
1203    DN namingContext = null;
1204
1205    if (baseDNs.isEmpty())
1206    {
1207      baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
1208    }
1209
1210    for (DN baseDN : baseDNs)
1211    {
1212      if (dn.isInScopeOf(baseDN, SearchScope.SUBORDINATES))
1213      {
1214        namingContext = baseDN;
1215        break;
1216      }
1217    }
1218
1219    return namingContext;
1220  }
1221}