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
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.plugins;
028    
029    
030    
031    import java.io.BufferedReader;
032    import java.io.BufferedWriter;
033    import java.io.File;
034    import java.io.FileReader;
035    import java.io.FileWriter;
036    import java.io.IOException;
037    import java.util.ArrayList;
038    import java.util.HashSet;
039    import java.util.LinkedHashMap;
040    import java.util.LinkedHashSet;
041    import java.util.LinkedList;
042    import java.util.List;
043    import java.util.Map;
044    import java.util.Set;
045    
046    import org.opends.messages.Message;
047    import org.opends.server.admin.std.server.ReferentialIntegrityPluginCfg;
048    import org.opends.server.admin.std.server.PluginCfg;
049    import org.opends.server.admin.std.meta.PluginCfgDefn;
050    import org.opends.server.admin.server.ConfigurationChangeListener;
051    import org.opends.server.api.Backend;
052    import org.opends.server.api.DirectoryThread;
053    import org.opends.server.api.ServerShutdownListener;
054    import org.opends.server.api.plugin.*;
055    import org.opends.server.config.ConfigException;
056    import org.opends.server.core.DirectoryServer;
057    import org.opends.server.core.ModifyOperation;
058    import org.opends.server.loggers.debug.DebugTracer;
059    import org.opends.server.protocols.internal.InternalClientConnection;
060    import org.opends.server.protocols.internal.InternalSearchOperation;
061    import org.opends.server.types.Attribute;
062    import org.opends.server.types.AttributeType;
063    import org.opends.server.types.AttributeValue;
064    import org.opends.server.types.ConfigChangeResult;
065    import org.opends.server.types.DebugLogLevel;
066    import org.opends.server.types.DereferencePolicy;
067    import org.opends.server.types.DirectoryException;
068    import org.opends.server.types.DN;
069    import org.opends.server.types.Entry;
070    import org.opends.server.types.IndexType;
071    import org.opends.server.types.Modification;
072    import org.opends.server.types.ModificationType;
073    import org.opends.server.types.ResultCode;
074    import org.opends.server.types.SearchResultEntry;
075    import org.opends.server.types.SearchFilter;
076    import org.opends.server.types.SearchScope;
077    import org.opends.server.types.operation.SubordinateModifyDNOperation;
078    import org.opends.server.types.operation.PostOperationModifyDNOperation;
079    import org.opends.server.types.operation.PostOperationDeleteOperation;
080    
081    import static org.opends.messages.PluginMessages.*;
082    import static org.opends.server.loggers.ErrorLogger.*;
083    import static org.opends.server.loggers.debug.DebugLogger.getTracer;
084    import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
085    import static org.opends.server.schema.SchemaConstants.*;
086    import static org.opends.server.util.StaticUtils.*;
087    
088    
089    
090    /**
091     * This class implements a Directory Server post operation plugin that performs
092     * Referential Integrity processing on successful delete and modify DN
093     * operations. The plugin uses a set of configuration criteria to determine
094     * what attribute types to check referential integrity on, and, the set of
095     * base DNs to search for entries that might need referential integrity
096     * processing. If none of these base DNs are specified in the configuration,
097     * then the public naming contexts are used as the base DNs by default.
098     * <BR><BR>
099     * The plugin also has an option to process changes in background using
100     * a thread that wakes up periodically looking for change records in a log
101     * file.
102     */
103    public class ReferentialIntegrityPlugin
104            extends DirectoryServerPlugin<ReferentialIntegrityPluginCfg>
105            implements ConfigurationChangeListener<ReferentialIntegrityPluginCfg>,
106                       ServerShutdownListener
107    {
108      /**
109       * The tracer object for the debug logger.
110       */
111      private static final DebugTracer TRACER = getTracer();
112    
113    
114    
115      //Current plugin configuration.
116      private ReferentialIntegrityPluginCfg currentConfiguration;
117    
118      //List of attribute types that will be checked during referential integrity
119      //processing.
120      private LinkedHashSet<AttributeType>
121              attributeTypes = new LinkedHashSet<AttributeType>();
122    
123      //List of base DNs that limit the scope of the referential integrity checking.
124      private Set<DN> baseDNs = new LinkedHashSet<DN>();
125    
126      //The update interval the background thread uses. If it is 0, then
127      //the changes are processed in foreground.
128      private long interval;
129    
130      //The flag used by the background thread to check if it should exit.
131      private boolean stopRequested=false;
132    
133      //The thread name.
134      private final String name="Referential Integrity Background Update Thread";
135    
136      //The name of the logfile that the update thread uses to process change
137      //records. Defaults to "logs/referint", but can be changed in the
138      //configuration.
139      private String logFileName;
140    
141      //The File class that logfile corresponds to.
142      private File logFile;
143    
144      //The Thread class that the background thread corresponds to.
145      private Thread backGroundThread=null;
146    
147      /**
148       * Used to save a map in the modifyDN operation attachment map that holds
149       * the old entry DNs and the new entry DNs related to a modify DN rename to
150       * new superior operation.
151       */
152      public static final String MODIFYDN_DNS="modifyDNs";
153    
154      //The buffered reader that is used to read the log file by the background
155      //thread.
156      private BufferedReader reader;
157    
158      //The buffered writer that is used to write update records in the log
159      //when the plugin is in background processing mode.
160      private BufferedWriter writer;
161    
162    
163    
164      /**
165       * {@inheritDoc}
166       */
167      public final void initializePlugin(Set<PluginType> pluginTypes,
168                                         ReferentialIntegrityPluginCfg pluginCfg)
169             throws ConfigException
170      {
171        pluginCfg.addReferentialIntegrityChangeListener(this);
172        currentConfiguration = pluginCfg;
173    
174        for (PluginType t : pluginTypes)
175        {
176          switch (t)
177          {
178            case POST_OPERATION_DELETE:
179            case POST_OPERATION_MODIFY_DN:
180            case SUBORDINATE_MODIFY_DN:
181              // These are acceptable.
182              break;
183    
184            default:
185              throw new
186                 ConfigException(ERR_PLUGIN_REFERENT_INVALID_PLUGIN_TYPE.get(
187                                      t.toString()));
188          }
189        }
190    
191        Set<DN> cfgBaseDNs = pluginCfg.getBaseDN();
192        if ((cfgBaseDNs == null) || cfgBaseDNs.isEmpty())
193        {
194          cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
195        }
196        else
197        {
198          baseDNs.addAll(cfgBaseDNs);
199        }
200    
201        // Iterate through all of the defined attribute types and ensure that they
202        // have acceptable syntaxes and that they are indexed for equality below all
203        // base DNs.
204        for (AttributeType type : pluginCfg.getAttributeType())
205        {
206          if (! isAttributeSyntaxValid(type))
207          {
208            throw new ConfigException(
209                           ERR_PLUGIN_REFERENT_INVALID_ATTRIBUTE_SYNTAX.get(
210                                type.getNameOrOID(),
211                                 type.getSyntax().getSyntaxName()));
212          }
213    
214          for (DN baseDN : cfgBaseDNs)
215          {
216            Backend b = DirectoryServer.getBackend(baseDN);
217            if ((b != null) && (! b.isIndexed(type, IndexType.EQUALITY)))
218            {
219              throw new ConfigException(ERR_PLUGIN_REFERENT_ATTR_UNINDEXED.get(
220                                             pluginCfg.dn().toString(),
221                                             type.getNameOrOID(),
222                                             b.getBackendID()));
223            }
224          }
225    
226          attributeTypes.add(type);
227        }
228    
229    
230        // Set up log file. Note: it is not allowed to change once the plugin is
231        // active.
232        setUpLogFile(pluginCfg.getLogFile());
233        interval=pluginCfg.getUpdateInterval();
234    
235        //Set up background processing if interval > 0.
236        if(interval > 0)
237        {
238          setUpBackGroundProcessing();
239        }
240      }
241    
242    
243    
244      /**
245       * {@inheritDoc}
246       */
247      public ConfigChangeResult applyConfigurationChange(
248              ReferentialIntegrityPluginCfg newConfiguration)
249      {
250        ResultCode         resultCode          = ResultCode.SUCCESS;
251        boolean            adminActionRequired = false;
252        ArrayList<Message> messages            = new ArrayList<Message>();
253    
254        //Load base DNs from new configuration.
255        LinkedHashSet<DN> newConfiguredBaseDNs = new LinkedHashSet<DN>();
256        for(DN baseDN : newConfiguration.getBaseDN())
257        {
258          newConfiguredBaseDNs.add(baseDN);
259        }
260    
261        //Load attribute types from new configuration.
262        LinkedHashSet<AttributeType> newAttributeTypes =
263                new LinkedHashSet<AttributeType>();
264        for (AttributeType type : newConfiguration.getAttributeType())
265        {
266          newAttributeTypes.add(type);
267        }
268    
269        //User is not allowed to change the logfile name, append a message that the
270        //server needs restarting for change to take effect.
271        String newLogFileName=newConfiguration.getLogFile();
272        if(!logFileName.equals(newLogFileName))
273        {
274          adminActionRequired=true;
275          messages.add(
276               INFO_PLUGIN_REFERENT_LOGFILE_CHANGE_REQUIRES_RESTART.get(logFileName,
277                    newLogFileName));
278        }
279    
280        //Switch to the new lists.
281        baseDNs = newConfiguredBaseDNs;
282        attributeTypes = newAttributeTypes;
283    
284        //If the plugin is enabled and the interval has changed, process that
285        //change. The change might start or stop the background processing thread.
286        long newInterval=newConfiguration.getUpdateInterval();
287        if(newConfiguration.isEnabled() && newInterval != interval)
288          processIntervalChange(newInterval, messages);
289    
290        currentConfiguration = newConfiguration;
291        return new ConfigChangeResult(resultCode, adminActionRequired, messages);
292      }
293    
294    
295      /**
296       * {@inheritDoc}
297       */
298      @Override()
299      public boolean isConfigurationAcceptable(PluginCfg configuration,
300                                               List<Message> unacceptableReasons)
301      {
302        ReferentialIntegrityPluginCfg cfg =
303             (ReferentialIntegrityPluginCfg) configuration;
304        return isConfigurationChangeAcceptable(cfg, unacceptableReasons);
305      }
306    
307    
308      /**
309       * {@inheritDoc}
310       */
311      public boolean isConfigurationChangeAcceptable(
312              ReferentialIntegrityPluginCfg configuration,
313              List<Message> unacceptableReasons)
314      {
315        boolean configAcceptable = true;
316        for (PluginCfgDefn.PluginType pluginType : configuration.getPluginType())
317        {
318          switch (pluginType)
319          {
320            case POSTOPERATIONDELETE:
321            case POSTOPERATIONMODIFYDN:
322            case SUBORDINATEMODIFYDN:
323              // These are acceptable.
324              break;
325            default:
326              unacceptableReasons.add(ERR_PLUGIN_REFERENT_INVALID_PLUGIN_TYPE.
327                                      get(pluginType.toString()));
328              configAcceptable = false;
329          }
330        }
331    
332        // Iterate through the set of base DNs that we will check and ensure that
333        // the corresponding backend is indexed appropriately.
334        Set<DN> cfgBaseDNs = configuration.getBaseDN();
335        if ((cfgBaseDNs == null) || cfgBaseDNs.isEmpty())
336        {
337          cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet();
338        }
339        else
340        {
341          baseDNs.addAll(cfgBaseDNs);
342        }
343    
344        //Iterate through attributes and check that each has a valid syntax
345        for (AttributeType type : configuration.getAttributeType())
346        {
347          if (!isAttributeSyntaxValid(type))
348          {
349            unacceptableReasons.add(
350                 ERR_PLUGIN_REFERENT_INVALID_ATTRIBUTE_SYNTAX.get(
351                      type.getNameOrOID(), type.getSyntax().getSyntaxName()));
352            configAcceptable = false;
353          }
354    
355          for (DN baseDN : cfgBaseDNs)
356          {
357            Backend b = DirectoryServer.getBackend(baseDN);
358            if ((b != null) && (! b.isIndexed(type, IndexType.EQUALITY)))
359            {
360              unacceptableReasons.add(ERR_PLUGIN_REFERENT_ATTR_UNINDEXED.get(
361                                           configuration.dn().toString(),
362                                           type.getNameOrOID(), b.getBackendID()));
363              configAcceptable = false;
364            }
365          }
366        }
367    
368        return configAcceptable;
369      }
370    
371    
372    
373      /**
374       * {@inheritDoc}
375       */
376      @SuppressWarnings("unchecked")
377      public PluginResult.PostOperation
378             doPostOperation(PostOperationModifyDNOperation
379              modifyDNOperation)
380      {
381        // If the operation itself failed, then we don't need to do anything because
382        // nothing changed.
383        if (modifyDNOperation.getResultCode() != ResultCode.SUCCESS)
384        {
385          return PluginResult.PostOperation.continueOperationProcessing();
386        }
387    
388        if (modifyDNOperation.getNewSuperior() == null)
389        {
390          // The entry was simply renamed below the same parent.
391          DN oldEntryDN=modifyDNOperation.getOriginalEntry().getDN();
392          DN newEntryDN=modifyDNOperation.getUpdatedEntry().getDN();
393          Map<DN,DN> modDNmap=new LinkedHashMap<DN,DN>();
394          modDNmap.put(oldEntryDN, newEntryDN);
395          processModifyDN(modDNmap,(interval != 0));
396        }
397        else
398        {
399          // The entry was moved below a new parent.  Use the saved map of old DNs
400          // and new DNs from the operation attachment.
401          Map<DN,DN> modDNmap =
402               (Map<DN, DN>) modifyDNOperation.getAttachment(MODIFYDN_DNS);
403          processModifyDN(modDNmap, (interval != 0));
404        }
405    
406        return PluginResult.PostOperation.continueOperationProcessing();
407      }
408    
409    
410    
411      /**
412       * {@inheritDoc}
413       */
414      public PluginResult.PostOperation doPostOperation(
415                  PostOperationDeleteOperation deleteOperation)
416      {
417        // If the operation itself failed, then we don't need to do anything because
418        // nothing changed.
419        if (deleteOperation.getResultCode() != ResultCode.SUCCESS)
420        {
421          return PluginResult.PostOperation.continueOperationProcessing();
422        }
423    
424        processDelete(deleteOperation.getEntryDN(), (interval != 0));
425        return PluginResult.PostOperation.continueOperationProcessing();
426      }
427    
428      /**
429       * {@inheritDoc}
430       */
431      @SuppressWarnings("unchecked")
432      public PluginResult.SubordinateModifyDN processSubordinateModifyDN(
433              SubordinateModifyDNOperation modifyDNOperation, Entry oldEntry,
434              Entry newEntry, List<Modification> modifications)
435      {
436        //This cast gives an unchecked cast warning, suppress it since the cast
437        //is ok.
438        Map<DN,DN>modDNmap=
439             (Map<DN, DN>) modifyDNOperation.getAttachment(MODIFYDN_DNS);
440        if(modDNmap == null)
441        {
442          //First time through, create the map and set it in the operation
443          //attachment.
444          modDNmap=new LinkedHashMap<DN,DN>();
445          modifyDNOperation.setAttachment(MODIFYDN_DNS, modDNmap);
446        }
447        modDNmap.put(oldEntry.getDN(), newEntry.getDN());
448        return PluginResult.SubordinateModifyDN.continueOperationProcessing();
449      }
450    
451    
452      /**
453       * Verify that the specified attribute has either a distinguished name syntax
454       * or "name and optional UID" syntax.
455       *
456       * @param attribute The attribute to check the syntax of.
457       *
458       * @return  Returns <code>true</code> if the attribute has a valid syntax.
459       *
460       */
461      private boolean isAttributeSyntaxValid(AttributeType attribute)
462      {
463        return (attribute.getSyntaxOID().equals(SYNTAX_DN_OID) ||
464                attribute.getSyntaxOID().equals(SYNTAX_NAME_AND_OPTIONAL_UID_OID));
465      }
466    
467      /**
468       * Process the specifed new interval value. This processing depends on what
469       * the current interval value is and new value will be. The values have been
470       * checked for equality at this point and are not equal.
471       *
472       * If the old interval is 0, then the server is in foreground mode and
473       * the background thread needs to be started using the new interval value.
474       *
475       * If the new interval value is 0, the the server is in background mode
476       * and the the background thread needs to be stopped.
477       *
478       * If the user just wants to change the interval value, the background thread
479       * needs to be interrupted so that it can use the new interval value.
480       *
481       * @param newInterval The new interval value to use.
482       *
483       * @param msgs An array list of messages that thread stop and start messages
484       *             can be added to.
485       *
486       */
487      private void processIntervalChange(long newInterval,
488                                         ArrayList<Message> msgs) {
489        if(interval == 0) {
490          DirectoryServer.registerShutdownListener(this);
491          interval=newInterval;
492          msgs.add(INFO_PLUGIN_REFERENT_BACKGROUND_PROCESSING_STARTING.
493                  get(Long.toString(interval)));
494          setUpBackGroundProcessing();
495        } else if(newInterval == 0) {
496          Message message=
497                  INFO_PLUGIN_REFERENT_BACKGROUND_PROCESSING_STOPPING.get();
498          msgs.add(message);
499          processServerShutdown(message);
500          interval=newInterval;
501        } else {
502          interval=newInterval;
503          backGroundThread.interrupt();
504          msgs.add(
505                 INFO_PLUGIN_REFERENT_BACKGROUND_PROCESSING_UPDATE_INTERVAL_CHANGED.
506                          get(Long.toString(interval),Long.toString(newInterval)));
507        }
508      }
509    
510      /**
511       * Process a modify DN post operation using the specified map of old and new
512       * entry DNs.  The boolean "log" is used to determine if the  map
513       * is written to the log file for the background thread to pick up. If the
514       * map is to be processed in foreground, than each base DN or public
515       * naming context (if the base DN configuration is empty) is processed.
516       *
517       * @param modDNMap  The map of old entry and new entry DNs from the modify
518       *                  DN operation.
519       *
520       * @param log Set to <code>true</code> if the map should be written to a log
521       *            file so that the background thread can process the changes at
522       *            a later time.
523       *
524       */
525      private void processModifyDN(Map<DN, DN> modDNMap, boolean log)
526      {
527        if(modDNMap != null)
528        {
529          if(log)
530          {
531            writeLog(modDNMap);
532          }
533          else
534          {
535            for(DN baseDN : getBaseDNsToSearch())
536            {
537              doBaseDN(baseDN, modDNMap);
538            }
539          }
540        }
541      }
542    
543      /**
544       * Used by both the background thread and the delete post operation to
545       * process a delete operation on the specified entry DN.  The
546       * boolean "log" is used to determine if the DN is written to the log file
547       * for the background thread to pick up. This value is set to false if the
548       * background thread is processing changes. If this method is being called
549       * by a delete post operation, then setting the "log" value to false will
550       * cause the DN to be processed in foreground
551       *
552       * If the DN is to be processed, than each base DN or public naming
553       * context (if the base DN configuration is empty) is is checked to see if
554       * entries under it contain references to the deleted entry DN that need
555       * to be removed.
556       *
557       * @param entryDN  The DN of the deleted entry.
558       *
559       * @param log Set to <code>true</code> if the DN should be written to a log
560       *            file so that the background thread can process the change at
561       *            a later time.
562       *
563       */
564      private void processDelete(DN entryDN, boolean log)
565      {
566        if(log)
567        {
568          writeLog(entryDN);
569        }
570        else
571        {
572          for(DN baseDN : getBaseDNsToSearch())
573          {
574            searchBaseDN(baseDN, entryDN, null);
575          }
576        }
577      }
578    
579      /**
580       * Used by the background thread to process the specified old entry DN and
581       * new entry DN. Each base DN or public naming context (if the base DN
582       * configuration is empty) is checked to see  if they contain entries with
583       * references to the old entry DN that need to be changed to the new entry DN.
584       *
585       * @param oldEntryDN  The entry DN before the modify DN operation.
586       *
587       * @param newEntryDN The entry DN after the modify DN operation.
588       *
589       */
590      private void processModifyDN(DN oldEntryDN, DN newEntryDN)
591      {
592        for(DN baseDN : getBaseDNsToSearch())
593        {
594          searchBaseDN(baseDN, oldEntryDN, newEntryDN);
595        }
596      }
597    
598      /**
599       * Return a set of DNs that are used to search for references under. If the
600       * base DN configuration set is empty, then the public naming contexts
601       * are used.
602       *
603       * @return A set of DNs to use in the reference searches.
604       *
605       */
606      private Set<DN> getBaseDNsToSearch()
607      {
608        if(baseDNs.isEmpty())
609        {
610          return DirectoryServer.getPublicNamingContexts().keySet();
611        }
612        else
613        {
614          return baseDNs;
615        }
616      }
617    
618      /**
619       * Search a base DN using a filter built from the configured attribute
620       * types and the specified old entry DN. For each entry that is found from
621       * the search, delete the old entry DN from the entry. If the new entry
622       * DN is not null, then add it to the entry.
623       *
624       * @param baseDN  The DN to base the search at.
625       *
626       * @param oldEntryDN The old entry DN that needs to be deleted or replaced.
627       *
628       * @param newEntryDN The new entry DN that needs to be added. May be null
629       *                   if the original operation was a delete.
630       *
631       */
632      private void searchBaseDN(DN baseDN, DN oldEntryDN, DN newEntryDN)
633      {
634        //Build an equality search with all of the configured attribute types
635        //and the old entry DN.
636        HashSet<SearchFilter> componentFilters=new HashSet<SearchFilter>();
637        for(AttributeType attributeType : attributeTypes)
638        {
639          componentFilters.add(SearchFilter.createEqualityFilter(attributeType,
640                  new AttributeValue(attributeType, oldEntryDN.toString())));
641        }
642    
643        InternalClientConnection conn =
644             InternalClientConnection.getRootConnection();
645        InternalSearchOperation operation = conn.processSearch(baseDN,
646             SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0,
647             false, SearchFilter.createORFilter(componentFilters), null);
648    
649        switch (operation.getResultCode())
650        {
651          case SUCCESS:
652            break;
653    
654          case NO_SUCH_OBJECT:
655            logError(INFO_PLUGIN_REFERENT_SEARCH_NO_SUCH_OBJECT.get(
656                          baseDN.toString()));
657            return;
658    
659          default:
660            Message message1 = ERR_PLUGIN_REFERENT_SEARCH_FAILED.
661                    get(String.valueOf(operation.getErrorMessage()));
662            logError(message1);
663            return;
664        }
665    
666        for (SearchResultEntry entry : operation.getSearchEntries())
667        {
668          deleteAddAttributesEntry(entry, oldEntryDN, newEntryDN);
669        }
670      }
671    
672      /**
673       * This method is used in foreground processing of a modify DN operation.
674       * It uses the specified map to perform base DN searching for each map
675       * entry. The key is the old entry DN and the value is the
676       * new entry DN.
677       *
678       * @param baseDN The DN to base the search at.
679       *
680       * @param modifyDNmap The map containing the modify DN old and new entry DNs.
681       *
682       */
683      private void doBaseDN(DN baseDN, Map<DN,DN> modifyDNmap)
684      {
685        for(Map.Entry<DN,DN> mapEntry: modifyDNmap.entrySet())
686        {
687          searchBaseDN(baseDN, mapEntry.getKey(), mapEntry.getValue());
688        }
689      }
690    
691      /**
692       * For each attribute type, delete the specified old entry DN and
693       * optionally add the specified new entry DN if the DN is not null.
694       * The specified entry is used to see if it contains each attribute type so
695       * those types that the entry contains can be modified. An internal modify
696       * is performed to change the entry.
697       *
698       * @param e The entry that contains the old references.
699       *
700       * @param oldEntryDN The old entry DN to remove references to.
701       *
702       * @param newEntryDN The new entry DN to add a reference to, if it is not
703       *                   null.
704       *
705       */
706      private void deleteAddAttributesEntry(Entry e, DN oldEntryDN, DN newEntryDN)
707      {
708        LinkedList<Modification> mods = new LinkedList<Modification>();
709        DN entryDN=e.getDN();
710        for(AttributeType type : attributeTypes)
711        {
712          if(e.hasAttribute(type))
713          {
714            AttributeValue deleteValue=
715                 new AttributeValue(type, oldEntryDN.toString());
716            LinkedHashSet<AttributeValue> deleteValues=
717                 new LinkedHashSet<AttributeValue>();
718    
719            deleteValues.add(deleteValue);
720            mods.add(new Modification(ModificationType.DELETE,
721                    new Attribute(type, type.getNameOrOID(), deleteValues)));
722    
723            //If the new entry DN exists, create an ADD modification for it.
724            if(newEntryDN != null)
725            {
726              LinkedHashSet<AttributeValue> addValues=
727                   new LinkedHashSet<AttributeValue>();
728              AttributeValue addValue=
729                   new AttributeValue(type, newEntryDN.toString());
730              addValues.add(addValue);
731              mods.add(new Modification(ModificationType.ADD,
732                      new Attribute(type, type.getNameOrOID(), addValues)));
733            }
734          }
735        }
736    
737        InternalClientConnection conn =
738                InternalClientConnection.getRootConnection();
739        ModifyOperation modifyOperation =
740                conn.processModify(entryDN, mods);
741        if(modifyOperation.getResultCode() != ResultCode.SUCCESS)
742        {
743          logError(ERR_PLUGIN_REFERENT_MODIFY_FAILED.get(entryDN.toString(),
744                          String.valueOf(modifyOperation.getErrorMessage())));
745        }
746      }
747    
748      /**
749       * Sets up the log file that the plugin can write update recored to and
750       * the background thread can use to read update records from. The specifed
751       * log file name is the name to use for the file. If the file exists from
752       * a previous run, use it.
753       *
754       * @param logFileName The name of the file to use, may be absolute.
755       *
756       * @throws ConfigException If a new file cannot be created if needed.
757       *
758       */
759      private void setUpLogFile(String logFileName)
760              throws ConfigException
761      {
762        this.logFileName=logFileName;
763        logFile=getFileForPath(logFileName);
764    
765        try
766        {
767          if(!logFile.exists())
768          {
769            logFile.createNewFile();
770          }
771        }
772        catch (IOException io)
773        {
774          throw new ConfigException(ERR_PLUGIN_REFERENT_CREATE_LOGFILE.get(
775                                         io.getMessage()), io);
776        }
777      }
778    
779      /**
780       * Sets up a buffered writer that the plugin can use to write update records
781       * with.
782       *
783       * @throws IOException If a new file writer cannot be created.
784       *
785       */
786      private void setupWriter() throws IOException {
787        writer=new BufferedWriter(new FileWriter(logFile, true));
788      }
789    
790    
791      /**
792       * Sets up a buffered reader that the background thread can use to read
793       * update records with.
794       *
795       * @throws IOException If a new file reader cannot be created.
796       *
797       */
798      private void setupReader() throws IOException {
799        reader=new BufferedReader(new FileReader(logFile));
800      }
801    
802      /**
803       * Write the specified map of old entry and new entry DNs to the log
804       * file. Each entry of the map is a line in the file, the key is the old
805       * entry normalized DN and the value is the new entry normalized DN.
806       * The DNs are separated by the tab character. This map is related to a
807       * modify DN operation.
808       *
809       * @param modDNmap The map of old entry and new entry DNs.
810       *
811       */
812      private void writeLog(Map<DN,DN> modDNmap) {
813        synchronized(logFile)
814        {
815          try
816          {
817            setupWriter();
818            for(Map.Entry<DN,DN> mapEntry : modDNmap.entrySet())
819            {
820              writer.write(mapEntry.getKey().toNormalizedString() + "\t" +
821                      mapEntry.getValue().toNormalizedString());
822              writer.newLine();
823            }
824            writer.flush();
825            writer.close();
826          }
827          catch (IOException io)
828          {
829            logError(ERR_PLUGIN_REFERENT_CLOSE_LOGFILE.get(io.getMessage()));
830          }
831        }
832      }
833    
834      /**
835       * Write the specified entry DN to the log file. This entry DN is related to
836       * a delete operation.
837       *
838       * @param deletedEntryDN The DN of the deleted entry.
839       *
840       */
841      private void writeLog(DN deletedEntryDN) {
842        synchronized(logFile) {
843          try {
844            setupWriter();
845            writer.write(deletedEntryDN.toNormalizedString());
846            writer.newLine();
847            writer.flush();
848            writer.close();
849          }
850          catch (IOException io)
851          {
852            logError(ERR_PLUGIN_REFERENT_CLOSE_LOGFILE.get(io.getMessage()));
853          }
854        }
855      }
856    
857      /**
858       * Process all of the records in the log file. Each line of the file is read
859       * and parsed to determine if it was a delete operation (a single normalized
860       * DN) or a modify DN operation (two normalized DNs separated by a tab). The
861       * corresponding operation method is called to perform the referential
862       * integrity processing as though the operation was just processed. After
863       * all of the records in log file have been processed, the log file is
864       * cleared so that new records can be added.
865       *
866       */
867      private void processLog() {
868        synchronized(logFile) {
869          try {
870            if(logFile.length() == 0)
871            {
872              return;
873            }
874    
875            setupReader();
876            String line;
877            while((line=reader.readLine()) != null) {
878              try {
879                String[] a=line.split("[\t]");
880                DN origDn = DN.decode(a[0]);
881                //If there is only a single DN string than it must be a delete.
882                if(a.length == 1) {
883                  processDelete(origDn, false);
884                } else {
885                  DN movedDN=DN.decode(a[1]);
886                  processModifyDN(origDn, movedDN);
887                }
888              } catch (DirectoryException ex) {
889                //This exception should rarely happen since the plugin wrote the DN
890                //strings originally.
891                Message message=
892                        ERR_PLUGIN_REFERENT_CANNOT_DECODE_STRING_AS_DN.
893                                                               get(ex.getMessage());
894                logError(message);
895              }
896            }
897            reader.close();
898            logFile.delete();
899            logFile.createNewFile();
900          } catch (IOException io) {
901            logError(ERR_PLUGIN_REFERENT_REPLACE_LOGFILE.get(io.getMessage()));
902          }
903        }
904      }
905    
906      /**
907       * Return the listener name.
908       *
909       * @return The name of the listener.
910       *
911       */
912      public String getShutdownListenerName() {
913        return name;
914      }
915    
916    
917      /**
918       * {@inheritDoc}
919       */
920      @Override()
921      public final void finalizePlugin() {
922        currentConfiguration.removeReferentialIntegrityChangeListener(this);
923        if(interval > 0)
924        {
925          processServerShutdown(null);
926        }
927      }
928    
929      /**
930       * Process a server shutdown. If the background thread is running it needs
931       * to be interrupted so it can read the stop request variable and exit.
932       *
933       * @param reason The reason message for the shutdown.
934       *
935       */
936      public void processServerShutdown(Message reason)
937      {
938        stopRequested = true;
939    
940        // Wait for back ground thread to terminate
941        while (backGroundThread != null && backGroundThread.isAlive()) {
942          try {
943            // Interrupt if its sleeping
944            backGroundThread.interrupt();
945            backGroundThread.join();
946          }
947          catch (InterruptedException ex) {
948            //Expected.
949          }
950        }
951        DirectoryServer.deregisterShutdownListener(this);
952        backGroundThread=null;
953      }
954    
955    
956      /**
957       * Returns the interval time converted to milliseconds.
958       *
959       * @return The interval time for the background thread.
960       */
961      private long getInterval() {
962        return interval * 1000;
963      }
964    
965      /**
966       * Sets up background processing of referential integrity by creating a
967       * new background thread to process updates.
968       *
969       */
970      private void setUpBackGroundProcessing()  {
971        if(backGroundThread == null) {
972          DirectoryServer.registerShutdownListener(this);
973          stopRequested = false;
974          backGroundThread = new BackGroundThread();
975          backGroundThread.start();
976        }
977      }
978    
979    
980      /**
981       * Used by the background thread to determine if it should exit.
982       *
983       * @return Returns <code>true</code> if the background thread should exit.
984       *
985       */
986      private boolean isShuttingDown()  {
987        return stopRequested;
988      }
989    
990      /**
991       * The background referential integrity processing thread. Wakes up after
992       * sleeping for a configurable interval and checks the log file for update
993       * records.
994       *
995       */
996      private class BackGroundThread extends DirectoryThread {
997    
998        /**
999         * Constructor for the background thread.
1000         */
1001        public
1002        BackGroundThread() {
1003          super(name);
1004        }
1005    
1006        /**
1007         * Run method for the background thread.
1008         */
1009        public void run() {
1010          while(!isShuttingDown())  {
1011            try {
1012              sleep(getInterval());
1013            } catch(InterruptedException e) {
1014              continue;
1015            } catch(Exception e) {
1016              if (debugEnabled()) {
1017                TRACER.debugCaught(DebugLogLevel.ERROR, e);
1018              }
1019            }
1020            processLog();
1021          }
1022        }
1023      }
1024    }