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