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