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 }