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