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 2014-2015 ForgeRock AS 025 */ 026package org.opends.server.core; 027 028import static org.forgerock.util.Utils.*; 029import static org.opends.messages.ConfigMessages.*; 030import static org.opends.server.config.ConfigConstants.*; 031 032import java.io.File; 033import java.io.FileReader; 034import java.io.IOException; 035import java.util.HashSet; 036import java.util.LinkedList; 037import java.util.List; 038import java.util.Set; 039import java.util.concurrent.ConcurrentHashMap; 040import java.util.concurrent.CopyOnWriteArrayList; 041 042import org.forgerock.i18n.LocalizableMessage; 043import org.forgerock.i18n.LocalizableMessageBuilder; 044import org.forgerock.i18n.slf4j.LocalizedLogger; 045import org.forgerock.opendj.adapter.server3x.Converters; 046import org.forgerock.opendj.config.server.ConfigChangeResult; 047import org.forgerock.opendj.config.server.ConfigException; 048import org.forgerock.opendj.config.server.spi.ConfigAddListener; 049import org.forgerock.opendj.config.server.spi.ConfigChangeListener; 050import org.forgerock.opendj.config.server.spi.ConfigDeleteListener; 051import org.forgerock.opendj.config.server.spi.ConfigurationRepository; 052import org.forgerock.opendj.ldap.CancelRequestListener; 053import org.forgerock.opendj.ldap.CancelledResultException; 054import org.forgerock.opendj.ldap.DN; 055import org.forgerock.opendj.ldap.Entry; 056import org.forgerock.opendj.ldap.LdapException; 057import org.forgerock.opendj.ldap.Filter; 058import org.forgerock.opendj.ldap.MemoryBackend; 059import org.forgerock.opendj.ldap.RequestContext; 060import org.forgerock.opendj.ldap.ResultCode; 061import org.forgerock.opendj.ldap.LdapResultHandler; 062import org.forgerock.opendj.ldap.SearchResultHandler; 063import org.forgerock.opendj.ldap.SearchScope; 064import org.forgerock.opendj.ldap.requests.Requests; 065import org.forgerock.opendj.ldap.responses.Result; 066import org.forgerock.opendj.ldap.responses.SearchResultEntry; 067import org.forgerock.opendj.ldap.responses.SearchResultReference; 068import org.forgerock.opendj.ldap.schema.Schema; 069import org.forgerock.opendj.ldap.schema.SchemaBuilder; 070import org.forgerock.opendj.ldif.EntryReader; 071import org.forgerock.opendj.ldif.LDIFEntryReader; 072import org.forgerock.util.Utils; 073import org.opends.server.types.DirectoryEnvironmentConfig; 074import org.opends.server.types.DirectoryException; 075import org.opends.server.types.InitializationException; 076 077/** 078 * Responsible for managing configuration entries and listeners on these 079 * entries. 080 */ 081public class ConfigurationHandler implements ConfigurationRepository 082{ 083 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 084 085 private static final String CONFIGURATION_FILE_NAME = "02-config.ldif"; 086 087 private final ServerContext serverContext; 088 089 /** The complete path to the configuration file to use. */ 090 private File configFile; 091 092 /** Indicates whether to start using the last known good configuration. */ 093 private boolean useLastKnownGoodConfig; 094 095 /** Backend containing the configuration entries. */ 096 private MemoryBackend backend; 097 098 /** The config root entry. */ 099 private Entry rootEntry; 100 101 /** The add/delete/change listeners on configuration entries. */ 102 private final ConcurrentHashMap<DN, EntryListeners> listeners = new ConcurrentHashMap<>(); 103 104 /** Schema with configuration-related elements. */ 105 private Schema configEnabledSchema; 106 107 /** 108 * Creates a new instance. 109 * 110 * @param serverContext 111 * The server context. 112 */ 113 public ConfigurationHandler(final ServerContext serverContext) 114 { 115 this.serverContext = serverContext; 116 } 117 118 /** 119 * Initialize the configuration. 120 * 121 * @throws InitializationException 122 * If an error occurs during the initialization. 123 */ 124 public void initialize() throws InitializationException 125 { 126 final DirectoryEnvironmentConfig environment = serverContext.getEnvironment(); 127 useLastKnownGoodConfig = environment.useLastKnownGoodConfiguration(); 128 configFile = findConfigFileToUse(environment.getConfigFile()); 129 130 configEnabledSchema = loadConfigEnabledSchema(); 131 loadConfiguration(configFile, configEnabledSchema); 132 } 133 134 /** Holds add, change and delete listeners for a given configuration entry. */ 135 private static class EntryListeners { 136 137 /** The set of add listeners that have been registered with this entry. */ 138 private final CopyOnWriteArrayList<ConfigAddListener> addListeners = new CopyOnWriteArrayList<>(); 139 /** The set of change listeners that have been registered with this entry. */ 140 private final CopyOnWriteArrayList<ConfigChangeListener> changeListeners = new CopyOnWriteArrayList<>(); 141 /** The set of delete listeners that have been registered with this entry. */ 142 private final CopyOnWriteArrayList<ConfigDeleteListener> deleteListeners = new CopyOnWriteArrayList<>(); 143 144 CopyOnWriteArrayList<ConfigChangeListener> getChangeListeners() 145 { 146 return changeListeners; 147 } 148 149 void registerChangeListener(final ConfigChangeListener listener) 150 { 151 changeListeners.add(listener); 152 } 153 154 boolean deregisterChangeListener(final ConfigChangeListener listener) 155 { 156 return changeListeners.remove(listener); 157 } 158 159 CopyOnWriteArrayList<ConfigAddListener> getAddListeners() 160 { 161 return addListeners; 162 } 163 164 void registerAddListener(final ConfigAddListener listener) 165 { 166 addListeners.addIfAbsent(listener); 167 } 168 169 void deregisterAddListener(final ConfigAddListener listener) 170 { 171 addListeners.remove(listener); 172 } 173 174 CopyOnWriteArrayList<ConfigDeleteListener> getDeleteListeners() 175 { 176 return deleteListeners; 177 } 178 179 void registerDeleteListener(final ConfigDeleteListener listener) 180 { 181 deleteListeners.addIfAbsent(listener); 182 } 183 184 void deregisterDeleteListener(final ConfigDeleteListener listener) 185 { 186 deleteListeners.remove(listener); 187 } 188 189 } 190 191 /** Request context to be used when requesting the internal backend. */ 192 private static final RequestContext UNCANCELLABLE_REQUEST_CONTEXT = 193 new RequestContext() 194 { 195 /** {@inheritDoc} */ 196 @Override 197 public void removeCancelRequestListener(final CancelRequestListener listener) 198 { 199 // nothing to do 200 } 201 202 /** {@inheritDoc} */ 203 @Override 204 public int getMessageID() 205 { 206 return -1; 207 } 208 209 /** {@inheritDoc} */ 210 @Override 211 public void checkIfCancelled(final boolean signalTooLate) 212 throws CancelledResultException 213 { 214 // nothing to do 215 } 216 217 /** {@inheritDoc} */ 218 @Override 219 public void addCancelRequestListener(final CancelRequestListener listener) 220 { 221 // nothing to do 222 223 } 224 }; 225 226 /** Handler for search results. */ 227 private static final class ConfigSearchHandler implements SearchResultHandler 228 { 229 private final Set<Entry> entries = new HashSet<>(); 230 231 Set<Entry> getEntries() 232 { 233 return entries; 234 } 235 236 /** {@inheritDoc} */ 237 @Override 238 public boolean handleReference(SearchResultReference reference) 239 { 240 throw new UnsupportedOperationException("Search references are not supported for configuration entries."); 241 } 242 243 /** {@inheritDoc} */ 244 @Override 245 public boolean handleEntry(SearchResultEntry entry) 246 { 247 entries.add(entry); 248 return true; 249 } 250 } 251 252 /** Handler for LDAP operations. */ 253 private static final class ConfigResultHandler implements LdapResultHandler<Result> { 254 255 private LdapException resultError; 256 257 LdapException getResultError() 258 { 259 return resultError; 260 } 261 262 boolean hasCompletedSuccessfully() { 263 return resultError == null; 264 } 265 266 /** {@inheritDoc} */ 267 @Override 268 public void handleResult(Result result) 269 { 270 // nothing to do 271 } 272 273 /** {@inheritDoc} */ 274 @Override 275 public void handleException(LdapException exception) 276 { 277 resultError = exception; 278 } 279 } 280 281 /** 282 * Returns the configuration root entry. 283 * 284 * @return the root entry 285 */ 286 public Entry getRootEntry() { 287 return rootEntry; 288 } 289 290 /** {@inheritDoc} */ 291 @Override 292 public Entry getEntry(final DN dn) throws ConfigException { 293 Entry entry = backend.get(dn); 294 if (entry == null) 295 { 296 // TODO : fix message 297 LocalizableMessage message = LocalizableMessage.raw("Unable to retrieve the configuration entry %s", dn); 298 throw new ConfigException(message); 299 } 300 return entry; 301 } 302 303 /** {@inheritDoc} */ 304 @Override 305 public boolean hasEntry(final DN dn) throws ConfigException { 306 return backend.get(dn) != null; 307 } 308 309 /** {@inheritDoc} */ 310 @Override 311 public Set<DN> getChildren(DN dn) throws ConfigException { 312 final ConfigResultHandler resultHandler = new ConfigResultHandler(); 313 final ConfigSearchHandler searchHandler = new ConfigSearchHandler(); 314 315 backend.handleSearch( 316 UNCANCELLABLE_REQUEST_CONTEXT, 317 Requests.newSearchRequest(dn, SearchScope.SINGLE_LEVEL, Filter.objectClassPresent()), 318 null, searchHandler, resultHandler); 319 320 if (resultHandler.hasCompletedSuccessfully()) 321 { 322 final Set<DN> children = new HashSet<>(); 323 for (final Entry entry : searchHandler.getEntries()) 324 { 325 children.add(entry.getName()); 326 } 327 return children; 328 } 329 else { 330 // TODO : fix message 331 throw new ConfigException( 332 LocalizableMessage.raw("Unable to retrieve children of configuration entry : %s", dn), 333 resultHandler.getResultError()); 334 } 335 } 336 337 /** 338 * Retrieves the number of subordinates for the requested entry. 339 * 340 * @param entryDN 341 * The distinguished name of the entry. 342 * @param subtree 343 * {@code true} to include all entries from the requested entry 344 * to the lowest level in the tree or {@code false} to only 345 * include the entries immediately below the requested entry. 346 * @return The number of subordinate entries 347 * @throws ConfigException 348 * If a problem occurs while trying to retrieve the entry. 349 */ 350 public long numSubordinates(final DN entryDN, final boolean subtree) throws ConfigException 351 { 352 final ConfigResultHandler resultHandler = new ConfigResultHandler(); 353 final ConfigSearchHandler searchHandler = new ConfigSearchHandler(); 354 final SearchScope scope = subtree ? SearchScope.SUBORDINATES : SearchScope.SINGLE_LEVEL; 355 backend.handleSearch( 356 UNCANCELLABLE_REQUEST_CONTEXT, 357 Requests.newSearchRequest(entryDN, scope, Filter.objectClassPresent()), 358 null, searchHandler, resultHandler); 359 360 if (resultHandler.hasCompletedSuccessfully()) 361 { 362 return searchHandler.getEntries().size(); 363 } 364 else { 365 // TODO : fix the message 366 throw new ConfigException( 367 LocalizableMessage.raw("Unable to retrieve children of configuration entry : %s", entryDN), 368 resultHandler.getResultError()); 369 } 370 } 371 372 /** 373 * Add a configuration entry 374 * <p> 375 * The add is performed only if all Add listeners on the parent entry accept 376 * the changes. Once the change is accepted, entry is effectively added and 377 * all Add listeners are called again to apply the change resulting from this 378 * new entry. 379 * 380 * @param entry 381 * The configuration entry to add. 382 * @throws DirectoryException 383 * If an error occurs. 384 */ 385 public void addEntry(final Entry entry) throws DirectoryException 386 { 387 final DN entryDN = entry.getName(); 388 if (backend.contains(entryDN)) 389 { 390 throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, ERR_CONFIG_FILE_ADD_ALREADY_EXISTS.get(entryDN)); 391 } 392 393 final DN parentDN = retrieveParentDN(entryDN); 394 395 // Iterate through add listeners to make sure the new entry is acceptable. 396 final List<ConfigAddListener> addListeners = getAddListeners(parentDN); 397 final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder(); 398 for (final ConfigAddListener listener : addListeners) 399 { 400 if (!listener.configAddIsAcceptable(entry, unacceptableReason)) 401 { 402 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 403 ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER.get(entryDN, parentDN, unacceptableReason)); 404 } 405 } 406 407 // Add the entry. 408 final ConfigResultHandler resultHandler = new ConfigResultHandler(); 409 backend.handleAdd(UNCANCELLABLE_REQUEST_CONTEXT, Requests.newAddRequest(entry), null, resultHandler); 410 411 if (!resultHandler.hasCompletedSuccessfully()) { 412 // TODO fix the message : error when adding config entry 413 // use resultHandler.getResultError() to get the error 414 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 415 ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER.get(entryDN, parentDN, unacceptableReason)); 416 } 417 418 // Notify all the add listeners to apply the new configuration entry. 419 ResultCode resultCode = ResultCode.SUCCESS; 420 final List<LocalizableMessage> messages = new LinkedList<>(); 421 for (final ConfigAddListener listener : addListeners) 422 { 423 final ConfigChangeResult result = listener.applyConfigurationAdd(entry); 424 if (result.getResultCode() != ResultCode.SUCCESS) 425 { 426 resultCode = resultCode == ResultCode.SUCCESS ? result.getResultCode() : resultCode; 427 messages.addAll(result.getMessages()); 428 } 429 430 handleConfigChangeResult(result, entry.getName(), listener.getClass().getName(), "applyConfigurationAdd"); 431 } 432 433 if (resultCode != ResultCode.SUCCESS) 434 { 435 final String reasons = Utils.joinAsString(". ", messages); 436 throw new DirectoryException(resultCode, ERR_CONFIG_FILE_ADD_APPLY_FAILED.get(reasons)); 437 } 438 } 439 440 /** 441 * Delete a configuration entry. 442 * <p> 443 * The delete is performed only if all Delete listeners on the parent entry 444 * accept the changes. Once the change is accepted, entry is effectively 445 * deleted and all Delete listeners are called again to apply the change 446 * resulting from this deletion. 447 * 448 * @param dn 449 * DN of entry to delete. 450 * @throws DirectoryException 451 * If a problem occurs. 452 */ 453 public void deleteEntry(final DN dn) throws DirectoryException 454 { 455 // Entry must exist. 456 if (!backend.contains(dn)) 457 { 458 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 459 ERR_CONFIG_FILE_DELETE_NO_SUCH_ENTRY.get(dn), Converters.to(getMatchedDN(dn)), null); 460 } 461 462 // Entry must not have children. 463 try 464 { 465 if (!getChildren(dn).isEmpty()) 466 { 467 throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF, 468 ERR_CONFIG_FILE_DELETE_HAS_CHILDREN.get(dn)); 469 } 470 } 471 catch (ConfigException e) 472 { 473 // TODO : fix message = ERROR BACKEND CONFIG 474 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 475 ERR_CONFIG_FILE_DELETE_HAS_CHILDREN.get(dn), e); 476 } 477 478 // TODO : pass in the localizable message (2) 479 final DN parentDN = retrieveParentDN(dn); 480 481 // Iterate through delete listeners to make sure the deletion is acceptable. 482 final List<ConfigDeleteListener> deleteListeners = getDeleteListeners(parentDN); 483 final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder(); 484 final Entry entry = backend.get(dn); 485 for (final ConfigDeleteListener listener : deleteListeners) 486 { 487 if (!listener.configDeleteIsAcceptable(entry, unacceptableReason)) 488 { 489 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 490 ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER.get(entry, parentDN, unacceptableReason)); 491 } 492 } 493 494 // Delete the entry 495 final ConfigResultHandler resultHandler = new ConfigResultHandler(); 496 backend.handleDelete(UNCANCELLABLE_REQUEST_CONTEXT, Requests.newDeleteRequest(dn), null, resultHandler); 497 498 if (!resultHandler.hasCompletedSuccessfully()) { 499 // TODO fix message : error when deleting config entry 500 // use resultHandler.getResultError() to get the error 501 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 502 ERR_CONFIG_FILE_DELETE_REJECTED.get(dn, parentDN, unacceptableReason)); 503 } 504 505 // Notify all the delete listeners that the entry has been removed. 506 ResultCode resultCode = ResultCode.SUCCESS; 507 final List<LocalizableMessage> messages = new LinkedList<>(); 508 for (final ConfigDeleteListener listener : deleteListeners) 509 { 510 final ConfigChangeResult result = listener.applyConfigurationDelete(entry); 511 if (result.getResultCode() != ResultCode.SUCCESS) 512 { 513 resultCode = resultCode == ResultCode.SUCCESS ? result.getResultCode() : resultCode; 514 messages.addAll(result.getMessages()); 515 } 516 517 handleConfigChangeResult(result, dn, listener.getClass().getName(), "applyConfigurationDelete"); 518 } 519 520 if (resultCode != ResultCode.SUCCESS) 521 { 522 final String reasons = Utils.joinAsString(". ", messages); 523 throw new DirectoryException(resultCode, ERR_CONFIG_FILE_DELETE_APPLY_FAILED.get(reasons)); 524 } 525 } 526 527 /** 528 * Replaces the old configuration entry with the new configuration entry 529 * provided. 530 * <p> 531 * The replacement is performed only if all Change listeners on the entry 532 * accept the changes. Once the change is accepted, entry is effectively 533 * replaced and all Change listeners are called again to apply the change 534 * resulting from the replacement. 535 * 536 * @param oldEntry 537 * The original entry that is being replaced. 538 * @param newEntry 539 * The new entry to use in place of the existing entry with the same 540 * DN. 541 * @throws DirectoryException 542 * If a problem occurs while trying to replace the entry. 543 */ 544 public void replaceEntry(final Entry oldEntry, final Entry newEntry) 545 throws DirectoryException 546 { 547 final DN entryDN = oldEntry.getName(); 548 if (!backend.contains(entryDN)) 549 { 550 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 551 ERR_CONFIG_FILE_MODIFY_NO_SUCH_ENTRY.get(oldEntry), Converters.to(getMatchedDN(entryDN)), null); 552 } 553 554 //TODO : add objectclass and attribute to the config schema in order to get this code run 555// if (!Entries.getStructuralObjectClass(oldEntry, configEnabledSchema) 556// .equals(Entries.getStructuralObjectClass(newEntry, configEnabledSchema))) 557// { 558// throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 559// ERR_CONFIG_FILE_MODIFY_STRUCTURAL_CHANGE_NOT_ALLOWED.get(entryDN)); 560// } 561 562 // Iterate through change listeners to make sure the change is acceptable. 563 final List<ConfigChangeListener> changeListeners = getChangeListeners(entryDN); 564 final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder(); 565 for (ConfigChangeListener listeners : changeListeners) 566 { 567 if (!listeners.configChangeIsAcceptable(newEntry, unacceptableReason)) 568 { 569 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 570 ERR_CONFIG_FILE_MODIFY_REJECTED_BY_CHANGE_LISTENER.get(entryDN, unacceptableReason)); 571 } 572 } 573 574 // Replace the old entry with new entry. 575 final ConfigResultHandler resultHandler = new ConfigResultHandler(); 576 backend.handleModify( 577 UNCANCELLABLE_REQUEST_CONTEXT, 578 Requests.newModifyRequest(oldEntry, newEntry), 579 null, 580 resultHandler); 581 582 if (!resultHandler.hasCompletedSuccessfully()) 583 { 584 // TODO fix message : error when replacing config entry 585 // use resultHandler.getResultError() to get the error 586 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 587 ERR_CONFIG_FILE_DELETE_REJECTED.get(entryDN, entryDN, unacceptableReason)); 588 } 589 590 // Notify all the change listeners of the update. 591 ResultCode resultCode = ResultCode.SUCCESS; 592 final List<LocalizableMessage> messages = new LinkedList<>(); 593 for (final ConfigChangeListener listener : changeListeners) 594 { 595 final ConfigChangeResult result = listener.applyConfigurationChange(newEntry); 596 if (result.getResultCode() != ResultCode.SUCCESS) 597 { 598 resultCode = resultCode == ResultCode.SUCCESS ? result.getResultCode() : resultCode; 599 messages.addAll(result.getMessages()); 600 } 601 602 handleConfigChangeResult(result, entryDN, listener.getClass().getName(), "applyConfigurationChange"); 603 } 604 605 if (resultCode != ResultCode.SUCCESS) 606 { 607 throw new DirectoryException(resultCode, 608 ERR_CONFIG_FILE_MODIFY_APPLY_FAILED.get(Utils.joinAsString(". ", messages))); 609 } 610 } 611 612 /** {@inheritDoc} */ 613 @Override 614 public void registerAddListener(final DN dn, final ConfigAddListener listener) 615 { 616 getEntryListeners(dn).registerAddListener(listener); 617 } 618 619 /** {@inheritDoc} */ 620 @Override 621 public void registerDeleteListener(final DN dn, final ConfigDeleteListener listener) 622 { 623 getEntryListeners(dn).registerDeleteListener(listener); 624 } 625 626 /** {@inheritDoc} */ 627 @Override 628 public void registerChangeListener(final DN dn, final ConfigChangeListener listener) 629 { 630 getEntryListeners(dn).registerChangeListener(listener); 631 } 632 633 /** {@inheritDoc} */ 634 @Override 635 public void deregisterAddListener(final DN dn, final ConfigAddListener listener) 636 { 637 getEntryListeners(dn).deregisterAddListener(listener); 638 } 639 640 /** {@inheritDoc} */ 641 @Override 642 public void deregisterDeleteListener(final DN dn, final ConfigDeleteListener listener) 643 { 644 getEntryListeners(dn).deregisterDeleteListener(listener); 645 } 646 647 /** {@inheritDoc} */ 648 @Override 649 public boolean deregisterChangeListener(final DN dn, final ConfigChangeListener listener) 650 { 651 return getEntryListeners(dn).deregisterChangeListener(listener); 652 } 653 654 /** {@inheritDoc} */ 655 @Override 656 public List<ConfigAddListener> getAddListeners(final DN dn) 657 { 658 return getEntryListeners(dn).getAddListeners(); 659 } 660 661 /** {@inheritDoc} */ 662 @Override 663 public List<ConfigDeleteListener> getDeleteListeners(final DN dn) 664 { 665 return getEntryListeners(dn).getDeleteListeners(); 666 } 667 668 /** {@inheritDoc} */ 669 @Override 670 public List<ConfigChangeListener> getChangeListeners(final DN dn) 671 { 672 return getEntryListeners(dn).getChangeListeners(); 673 } 674 675 /** Load the configuration-enabled schema that will allow to read configuration file. */ 676 private Schema loadConfigEnabledSchema() throws InitializationException { 677 LDIFEntryReader reader = null; 678 try 679 { 680 final File schemaDir = serverContext.getEnvironment().getSchemaDirectory(); 681 reader = new LDIFEntryReader(new FileReader(new File(schemaDir, CONFIGURATION_FILE_NAME))); 682 reader.setSchema(Schema.getDefaultSchema()); 683 final Entry entry = reader.readEntry(); 684 return new SchemaBuilder(Schema.getDefaultSchema()).addSchema(entry, false).toSchema(); 685 } 686 catch (Exception e) 687 { 688 // TODO : fix message 689 throw new InitializationException(LocalizableMessage.raw("Unable to load config-enabled schema"), e); 690 } 691 finally { 692 closeSilently(reader); 693 } 694 } 695 696 /** 697 * Read configuration entries from provided configuration file. 698 * 699 * @param configFile 700 * LDIF file with configuration entries. 701 * @param schema 702 * Schema to validate entries when reading the config file. 703 * @throws InitializationException 704 * If an errors occurs. 705 */ 706 private void loadConfiguration(final File configFile, final Schema schema) 707 throws InitializationException 708 { 709 EntryReader reader = null; 710 try 711 { 712 reader = getLDIFReader(configFile, schema); 713 backend = new MemoryBackend(schema, reader); 714 } 715 catch (IOException e) 716 { 717 throw new InitializationException( 718 ERR_CONFIG_FILE_GENERIC_ERROR.get(configFile.getAbsolutePath(), e.getCause()), e); 719 } 720 finally 721 { 722 closeSilently(reader); 723 } 724 725 // Check that root entry is the expected one 726 rootEntry = backend.get(DN_CONFIG_ROOT); 727 if (rootEntry == null) 728 { 729 // fix message : we didn't find the expected root in the file 730 throw new InitializationException(ERR_CONFIG_FILE_INVALID_BASE_DN.get( 731 configFile.getAbsolutePath(), "", DN_CONFIG_ROOT)); 732 } 733 } 734 735 /** 736 * Returns the LDIF reader on configuration entries. 737 * <p> 738 * It is the responsability of the caller to ensure that reader 739 * is closed after usage. 740 * 741 * @param configFile 742 * LDIF file containing the configuration entries. 743 * @param schema 744 * Schema to validate entries when reading the config file. 745 * @return the LDIF reader 746 * @throws InitializationException 747 * If an error occurs. 748 */ 749 private EntryReader getLDIFReader(final File configFile, final Schema schema) 750 throws InitializationException 751 { 752 LDIFEntryReader reader = null; 753 try 754 { 755 reader = new LDIFEntryReader(new FileReader(configFile)); 756 reader.setSchema(schema); 757 } 758 catch (Exception e) 759 { 760 throw new InitializationException( 761 ERR_CONFIG_FILE_CANNOT_OPEN_FOR_READ.get(configFile.getAbsolutePath(), e), e); 762 } 763 return reader; 764 } 765 766 /** 767 * Returns the entry listeners attached to the provided DN. 768 * <p> 769 * If no listener exist for the provided DN, then a new set of empty listeners 770 * is created and returned. 771 * 772 * @param dn 773 * DN of a configuration entry. 774 * @return the listeners attached to the corresponding configuration entry. 775 */ 776 private EntryListeners getEntryListeners(final DN dn) { 777 EntryListeners entryListeners = listeners.get(dn); 778 if (entryListeners == null) { 779 entryListeners = new EntryListeners(); 780 final EntryListeners previousListeners = listeners.putIfAbsent(dn, entryListeners); 781 if (previousListeners != null) { 782 entryListeners = previousListeners; 783 } 784 } 785 return entryListeners; 786 } 787 788 /** 789 * Returns the parent DN of the configuration entry corresponding to the 790 * provided DN. 791 * 792 * @param entryDN 793 * DN of entry to retrieve the parent from. 794 * @return the parent DN 795 * @throws DirectoryException 796 * If entry has no parent or parent entry does not exist. 797 */ 798 private DN retrieveParentDN(final DN entryDN) throws DirectoryException 799 { 800 final DN parentDN = entryDN.parent(); 801 // Entry must have a parent. 802 if (parentDN == null) 803 { 804 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_CONFIG_FILE_ADD_NO_PARENT_DN.get(entryDN)); 805 } 806 807 // Parent entry must exist. 808 if (!backend.contains(parentDN)) 809 { 810 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 811 ERR_CONFIG_FILE_ADD_NO_PARENT.get(entryDN, parentDN), Converters.to(getMatchedDN(parentDN)), null); 812 } 813 return parentDN; 814 } 815 816 /** 817 * Returns the matched DN that is available in the configuration for the 818 * provided DN. 819 */ 820 private DN getMatchedDN(final DN dn) 821 { 822 DN matchedDN = null; 823 DN parentDN = dn.parent(); 824 while (parentDN != null) 825 { 826 if (backend.contains(parentDN)) 827 { 828 matchedDN = parentDN; 829 break; 830 } 831 parentDN = parentDN.parent(); 832 } 833 return matchedDN; 834 } 835 836 /** 837 * Find the actual configuration file to use to load configuration, given the 838 * standard config file. 839 * 840 * @param standardConfigFile 841 * "Standard" configuration file provided. 842 * @return the actual configuration file to use, which is either the standard 843 * config file provided or the config file corresponding to the last 844 * known good configuration 845 * @throws InitializationException 846 * If a problem occurs. 847 */ 848 private File findConfigFileToUse(final File standardConfigFile) throws InitializationException 849 { 850 File configFileToUse = null; 851 if (useLastKnownGoodConfig) 852 { 853 configFileToUse = new File(standardConfigFile + ".startok"); 854 if (! configFileToUse.exists()) 855 { 856 logger.warn(WARN_CONFIG_FILE_NO_STARTOK_FILE, configFileToUse.getAbsolutePath(), standardConfigFile); 857 useLastKnownGoodConfig = false; 858 configFileToUse = standardConfigFile; 859 } 860 else 861 { 862 logger.info(NOTE_CONFIG_FILE_USING_STARTOK_FILE, configFileToUse.getAbsolutePath(), standardConfigFile); 863 } 864 } 865 else 866 { 867 configFileToUse = standardConfigFile; 868 } 869 870 try 871 { 872 if (! configFileToUse.exists()) 873 { 874 throw new InitializationException(ERR_CONFIG_FILE_DOES_NOT_EXIST.get(configFileToUse.getAbsolutePath())); 875 } 876 } 877 catch (Exception e) 878 { 879 throw new InitializationException( 880 ERR_CONFIG_FILE_CANNOT_VERIFY_EXISTENCE.get(configFileToUse.getAbsolutePath(), e)); 881 } 882 return configFileToUse; 883 } 884 885 /** 886 * Examines the provided result and logs a message if appropriate. If the 887 * result code is anything other than {@code SUCCESS}, then it will log an 888 * error message. If the operation was successful but admin action is 889 * required, then it will log a warning message. If no action is required but 890 * messages were generated, then it will log an informational message. 891 * 892 * @param result 893 * The config change result object that 894 * @param entryDN 895 * The DN of the entry that was added, deleted, or modified. 896 * @param className 897 * The name of the class for the object that generated the provided 898 * result. 899 * @param methodName 900 * The name of the method that generated the provided result. 901 */ 902 private void handleConfigChangeResult(ConfigChangeResult result, DN entryDN, String className, String methodName) 903 { 904 if (result == null) 905 { 906 logger.error(ERR_CONFIG_CHANGE_NO_RESULT, className, methodName, entryDN); 907 return; 908 } 909 910 final ResultCode resultCode = result.getResultCode(); 911 final boolean adminActionRequired = result.adminActionRequired(); 912 final List<LocalizableMessage> messages = result.getMessages(); 913 914 final String messageBuffer = Utils.joinAsString(" ", messages); 915 if (resultCode != ResultCode.SUCCESS) 916 { 917 logger.error(ERR_CONFIG_CHANGE_RESULT_ERROR, className, methodName, entryDN, resultCode, 918 adminActionRequired, messageBuffer); 919 } 920 else if (adminActionRequired) 921 { 922 logger.warn(WARN_CONFIG_CHANGE_RESULT_ACTION_REQUIRED, className, methodName, entryDN, messageBuffer); 923 } 924 else if (messageBuffer.length() > 0) 925 { 926 logger.debug(INFO_CONFIG_CHANGE_RESULT_MESSAGES, className, methodName, entryDN, messageBuffer); 927 } 928 } 929 930}