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