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 }