001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2014-2016 ForgeRock AS. 015 */ 016package org.opends.server.config; 017 018import static org.opends.messages.ConfigMessages.*; 019import static org.opends.server.config.ConfigConstants.*; 020import static org.opends.server.extensions.ExtensionsConstants.*; 021import static org.opends.server.util.ServerConstants.*; 022import static org.opends.server.util.StaticUtils.*; 023 024import java.io.File; 025import java.io.FileInputStream; 026import java.io.FileNotFoundException; 027import java.io.FileOutputStream; 028import java.io.FileReader; 029import java.io.IOException; 030import java.io.InputStream; 031import java.security.MessageDigest; 032import java.security.NoSuchAlgorithmException; 033import java.util.ArrayList; 034import java.util.Arrays; 035import java.util.HashMap; 036import java.util.HashSet; 037import java.util.Iterator; 038import java.util.LinkedHashMap; 039import java.util.LinkedList; 040import java.util.List; 041import java.util.Map; 042import java.util.Set; 043import java.util.TreeSet; 044import java.util.concurrent.ConcurrentHashMap; 045import java.util.concurrent.CopyOnWriteArrayList; 046import java.util.zip.GZIPInputStream; 047import java.util.zip.GZIPOutputStream; 048 049import org.forgerock.i18n.LocalizableMessage; 050import org.forgerock.i18n.LocalizableMessageBuilder; 051import org.forgerock.i18n.slf4j.LocalizedLogger; 052import org.forgerock.opendj.adapter.server3x.Converters; 053import org.forgerock.opendj.config.ConfigurationFramework; 054import org.forgerock.opendj.config.server.ConfigChangeResult; 055import org.forgerock.opendj.config.server.ConfigException; 056import org.forgerock.opendj.config.server.spi.ConfigAddListener; 057import org.forgerock.opendj.config.server.spi.ConfigChangeListener; 058import org.forgerock.opendj.config.server.spi.ConfigDeleteListener; 059import org.forgerock.opendj.config.server.spi.ConfigurationRepository; 060import org.forgerock.opendj.ldap.ByteString; 061import org.forgerock.opendj.ldap.CancelRequestListener; 062import org.forgerock.opendj.ldap.CancelledResultException; 063import org.forgerock.opendj.ldap.DN; 064import org.forgerock.opendj.ldap.Entries; 065import org.forgerock.opendj.ldap.Entry; 066import org.forgerock.opendj.ldap.Filter; 067import org.forgerock.opendj.ldap.LdapException; 068import org.forgerock.opendj.ldap.LdapResultHandler; 069import org.forgerock.opendj.ldap.MemoryBackend; 070import org.forgerock.opendj.ldap.RequestContext; 071import org.forgerock.opendj.ldap.ResultCode; 072import org.forgerock.opendj.ldap.SearchResultHandler; 073import org.forgerock.opendj.ldap.SearchScope; 074import org.forgerock.opendj.ldap.requests.ModifyRequest; 075import org.forgerock.opendj.ldap.requests.Requests; 076import org.forgerock.opendj.ldap.requests.SearchRequest; 077import org.forgerock.opendj.ldap.responses.Result; 078import org.forgerock.opendj.ldap.responses.SearchResultEntry; 079import org.forgerock.opendj.ldap.responses.SearchResultReference; 080import org.forgerock.opendj.ldap.schema.Schema; 081import org.forgerock.opendj.ldap.schema.SchemaBuilder; 082import org.forgerock.opendj.ldif.EntryReader; 083import org.forgerock.opendj.ldif.LDIFEntryReader; 084import org.forgerock.opendj.ldif.LDIFEntryWriter; 085import org.forgerock.util.Utils; 086import org.forgerock.util.annotations.VisibleForTesting; 087import org.opends.server.api.AlertGenerator; 088import org.opends.server.core.DirectoryServer; 089import org.opends.server.core.SearchOperation; 090import org.opends.server.core.ServerContext; 091import org.opends.server.schema.GeneralizedTimeSyntax; 092import org.opends.server.tools.LDIFModify; 093import org.opends.server.types.DirectoryEnvironmentConfig; 094import org.opends.server.types.DirectoryException; 095import org.opends.server.types.ExistingFileBehavior; 096import org.opends.server.types.InitializationException; 097import org.opends.server.types.LDIFExportConfig; 098import org.opends.server.types.LDIFImportConfig; 099import org.opends.server.util.ActivateOnceSDKSchemaIsUsed; 100import org.opends.server.util.LDIFException; 101import org.opends.server.util.LDIFReader; 102import org.opends.server.util.LDIFWriter; 103import org.opends.server.util.TimeThread; 104 105/** 106 * Responsible for managing configuration, including listeners on configuration entries. 107 * <p> 108 * Configuration is represented by configuration entries, persisted on the file system. 109 * Configuration entries are initially read from configuration file ("config/config.ldif" by default), then stored 110 * in a {@code MemoryBackend} during server uptime. 111 * <p> 112 * The handler allows to register and unregister some listeners on any configuration entry 113 * (add, change or delete listener). 114 * Configuration entries can be added, replaced or deleted to the handler. 115 * Any change of a configuration entry will trigger the listeners registered for this entry, and will also 116 * trigger an update of configuration file. 117 * <p> 118 * The handler also maintains an up-to-date archive of configuration files. 119 */ 120public class ConfigurationHandler implements ConfigurationRepository, AlertGenerator 121{ 122 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 123 124 private static final String CONFIGURATION_FILE_NAME = "02-config.ldif"; 125 private static final String CLASS_NAME = ConfigurationHandler.class.getName(); 126 127 private final ServerContext serverContext; 128 129 /** The complete path to the default configuration file. */ 130 private File configFile; 131 132 /** Indicates whether to start using the last known good configuration. */ 133 private boolean useLastKnownGoodConfig; 134 135 /** Indicates whether to maintain a configuration archive. */ 136 private boolean maintainConfigArchive; 137 138 /** The maximum config archive size to maintain. */ 139 private int maxConfigArchiveSize; 140 141 /** 142 * A SHA-1 digest of the last known configuration. This should only be incorrect if the server 143 * configuration file has been manually edited with the server online, which is a bad thing. 144 */ 145 private byte[] configurationDigest; 146 147 /** Backend containing the configuration entries. */ 148 private MemoryBackend backend; 149 150 /** The config root entry. */ 151 private Entry rootEntry; 152 153 /** The add/delete/change listeners on configuration entries. */ 154 private final ConcurrentHashMap<DN, EntryListeners> listeners = new ConcurrentHashMap<>(); 155 156 /** 157 * Creates a new instance. 158 * 159 * @param serverContext 160 * The server context. 161 */ 162 public ConfigurationHandler(final ServerContext serverContext) 163 { 164 this.serverContext = serverContext; 165 } 166 167 /** 168 * Bootstraps the server configuration. 169 * <p> 170 * The returned ConfigurationHandler is initialized with a partial schema and must be later 171 * re-initialized with the full schema by calling {@link #reinitializeWithFullSchema(Schema)} 172 * method once the schema has been fully loaded. 173 * 174 * @param serverContext 175 * The server context. 176 * @return the configuration handler 177 * @throws InitializationException 178 * If an error occurs during bootstrapping. 179 */ 180 public static ConfigurationHandler bootstrapConfiguration(ServerContext serverContext) 181 throws InitializationException { 182 final ConfigurationFramework configFramework = ConfigurationFramework.getInstance(); 183 try 184 { 185 if (!configFramework.isInitialized()) 186 { 187 configFramework.initialize(); 188 } 189 } 190 catch (ConfigException e) 191 { 192 LocalizableMessage msg = ERR_CANNOT_INITIALIZE_CONFIGURATION_FRAMEWORK.get(stackTraceToSingleLineString(e)); 193 throw new InitializationException(msg, e); 194 } 195 196 final ConfigurationHandler configHandler = new ConfigurationHandler(serverContext); 197 configHandler.initializeWithPartialSchema(); 198 return configHandler; 199 } 200 201 /** 202 * Initializes the configuration with an incomplete schema. 203 * <p> 204 * As configuration contains schema-related items, the initialization of the configuration can 205 * only be performed with an incomplete schema before a complete schema is available. Once a 206 * complete schema is available, the {@link #reinitializeWithFullSchema(Schema)} method should be 207 * called to have a fully validated configuration. 208 * 209 * @throws InitializationException 210 * If an error occurs. 211 */ 212 @VisibleForTesting 213 void initializeWithPartialSchema() throws InitializationException 214 { 215 File configFileToUse = preInitialization(); 216 Schema configEnabledSchema = loadSchemaWithConfigurationEnabled(); 217 loadConfiguration(configFileToUse, configEnabledSchema); 218 } 219 220 /** 221 * Re-initializes the configuration handler with a fully initialized schema. 222 * <p> 223 * Previously registered listeners are preserved. 224 * 225 * @param schema 226 * The server schema, fully initialized. 227 * @throws InitializationException 228 * If an error occurs. 229 */ 230 public void reinitializeWithFullSchema(Schema schema) throws InitializationException 231 { 232 final Map<String, EntryListeners> exportedListeners = exportListeners(); 233 finalize(); 234 File configFileToUse = preInitialization(); 235 loadConfiguration(configFileToUse, schema); 236 importListeners(exportedListeners, schema); 237 } 238 239 /** Finalizes the configuration handler. */ 240 @Override 241 public void finalize() 242 { 243 listeners.clear(); 244 backend.clear(); 245 } 246 247 /** 248 * Prepares the initialization of the handler, returning the up-to-date configuration file to use 249 * to load the configuration. 250 * 251 * @return the file containing the configuration 252 * @throws InitializationException 253 * If an error occurs. 254 */ 255 private File preInitialization() throws InitializationException 256 { 257 final DirectoryEnvironmentConfig environment = serverContext.getEnvironment(); 258 useLastKnownGoodConfig = environment.useLastKnownGoodConfiguration(); 259 configFile = environment.getConfigFile(); 260 File configFileToUse = findConfigFileToUse(configFile); 261 ensureArchiveExistsAndIsUpToDate(environment, configFileToUse); 262 applyConfigChangesIfNeeded(configFileToUse); 263 return configFileToUse; 264 } 265 266 /** 267 * Returns a copy of the listeners with DN as strings. 268 * Use strings to avoid holding copies on the old schema. 269 */ 270 private Map<String, EntryListeners> exportListeners() 271 { 272 final Map<String, EntryListeners> listenersCopy = new HashMap<>(); 273 for (Map.Entry<DN, EntryListeners> entry : listeners.entrySet()) 274 { 275 listenersCopy.put(entry.getKey().toString(), entry.getValue()); 276 } 277 return listenersCopy; 278 } 279 280 /** Imports the provided listeners into the configuration handler. */ 281 private void importListeners(Map<String, EntryListeners> listenersCopy, Schema schema) 282 { 283 for (Map.Entry<String, EntryListeners> entry : listenersCopy.entrySet()) 284 { 285 listeners.put(DN.valueOf(entry.getKey(), schema), entry.getValue()); 286 } 287 } 288 289 @Override 290 public Map<String, String> getAlerts() 291 { 292 Map<String, String> alerts = new LinkedHashMap<>(); 293 294 alerts.put(ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, ALERT_DESCRIPTION_CANNOT_WRITE_CONFIGURATION); 295 alerts.put(ALERT_TYPE_MANUAL_CONFIG_EDIT_HANDLED, ALERT_DESCRIPTION_MANUAL_CONFIG_EDIT_HANDLED); 296 alerts.put(ALERT_TYPE_MANUAL_CONFIG_EDIT_LOST, ALERT_DESCRIPTION_MANUAL_CONFIG_EDIT_LOST); 297 298 return alerts; 299 } 300 301 @Override 302 public Set<DN> getChildren(DN dn) throws ConfigException 303 { 304 final ConfigLdapResultHandler resultHandler = new ConfigLdapResultHandler(); 305 final CollectorSearchResultHandler searchHandler = new CollectorSearchResultHandler(); 306 307 SearchRequest searchRequest = Requests.newSearchRequest(dn, SearchScope.SINGLE_LEVEL, Filter.alwaysTrue()); 308 backend.handleSearch(UNCANCELLABLE_REQUEST_CONTEXT, searchRequest, null, searchHandler, resultHandler); 309 310 if (resultHandler.hasCompletedSuccessfully()) 311 { 312 final Set<DN> children = new HashSet<>(); 313 for (final Entry entry : searchHandler.getEntries()) 314 { 315 children.add(entry.getName()); 316 } 317 return children; 318 } 319 throw new ConfigException(ERR_UNABLE_TO_RETRIEVE_CHILDREN_OF_CONFIGURATION_ENTRY.get(dn), 320 resultHandler.getResultError()); 321 } 322 323 @Override 324 public String getClassName() 325 { 326 return CLASS_NAME; 327 } 328 329 @Override 330 public DN getComponentEntryDN() 331 { 332 return rootEntry.getName(); 333 } 334 335 /** 336 * Returns the configuration file containing all configuration entries. 337 * 338 * @return the configuration file 339 */ 340 public File getConfigurationFile() 341 { 342 return configFile; 343 } 344 345 @Override 346 public Entry getEntry(final DN dn) throws ConfigException 347 { 348 Entry entry = backend.get(dn); 349 if (entry != null) 350 { 351 entry = Entries.unmodifiableEntry(entry); 352 } 353 return entry; 354 } 355 356 /** 357 * Returns the configuration root entry. 358 * 359 * @return the root entry 360 */ 361 public Entry getRootEntry() 362 { 363 return rootEntry; 364 } 365 366 @Override 367 public List<ConfigAddListener> getAddListeners(final DN dn) 368 { 369 return getEntryListeners(dn).getAddListeners(); 370 } 371 372 @Override 373 public List<ConfigChangeListener> getChangeListeners(final DN dn) 374 { 375 return getEntryListeners(dn).getChangeListeners(); 376 } 377 378 @Override 379 public List<ConfigDeleteListener> getDeleteListeners(final DN dn) 380 { 381 return getEntryListeners(dn).getDeleteListeners(); 382 } 383 384 @Override 385 public boolean hasEntry(final DN dn) throws ConfigException 386 { 387 return backend.get(dn) != null; 388 } 389 390 /** 391 * Search the configuration entries. 392 * 393 * @param searchOperation 394 * Defines the search to perform 395 */ 396 public void search(SearchOperation searchOperation) 397 { 398 // Leave all filtering to the SearchResultHandlerAdapter 399 SearchRequest request = Requests.newSearchRequest( 400 searchOperation.getBaseDN(), searchOperation.getScope(), Filter.alwaysTrue(), "*", "+"); 401 402 LdapResultHandlerAdapter resultHandler = new LdapResultHandlerAdapter(searchOperation); 403 SearchResultHandler entryHandler = new SearchResultHandlerAdapter(searchOperation, resultHandler); 404 backend.handleSearch(UNCANCELLABLE_REQUEST_CONTEXT, request, null, entryHandler, resultHandler); 405 } 406 407 /** 408 * Retrieves the number of subordinates for the requested entry. 409 * 410 * @param entryDN 411 * The distinguished name of the entry. 412 * @param subtree 413 * {@code true} to include all entries from the requested entry to the lowest level in 414 * the tree or {@code false} to only include the entries immediately below the requested 415 * entry. 416 * @return The number of subordinate entries 417 * @throws ConfigException 418 * If a problem occurs while trying to retrieve the entry. 419 */ 420 public long numSubordinates(final DN entryDN, final boolean subtree) throws ConfigException 421 { 422 final ConfigLdapResultHandler resultHandler = new ConfigLdapResultHandler(); 423 final CollectorSearchResultHandler searchHandler = new CollectorSearchResultHandler(); 424 final SearchScope scope = subtree ? SearchScope.SUBORDINATES : SearchScope.SINGLE_LEVEL; 425 final SearchRequest searchRequest = Requests.newSearchRequest(entryDN, scope, Filter.alwaysTrue()); 426 backend.handleSearch(UNCANCELLABLE_REQUEST_CONTEXT, searchRequest, null, searchHandler, resultHandler); 427 428 if (resultHandler.hasCompletedSuccessfully()) 429 { 430 return searchHandler.getEntries().size(); 431 } 432 throw new ConfigException(ERR_UNABLE_TO_RETRIEVE_CHILDREN_OF_CONFIGURATION_ENTRY.get(entryDN), 433 resultHandler.getResultError()); 434 } 435 436 /** 437 * Add a configuration entry. 438 * <p> 439 * The add is performed only if all Add listeners on the parent entry accept the changes. Once the 440 * change is accepted, entry is effectively added and all Add listeners are called again to apply 441 * the change resulting from this new entry. 442 * 443 * @param entry 444 * The configuration entry to add. 445 * @throws DirectoryException 446 * If an error occurs. 447 */ 448 public void addEntry(final Entry entry) throws DirectoryException 449 { 450 final DN entryDN = entry.getName(); 451 if (backend.contains(entryDN)) 452 { 453 throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, ERR_CONFIG_FILE_ADD_ALREADY_EXISTS.get(entryDN)); 454 } 455 456 final DN parentDN = retrieveParentDNForAdd(entryDN); 457 458 // Iterate through add listeners to make sure the new entry is acceptable. 459 final List<ConfigAddListener> addListeners = getAddListeners(parentDN); 460 final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder(); 461 for (final ConfigAddListener listener : addListeners) 462 { 463 if (!listener.configAddIsAcceptable(entry, unacceptableReason)) 464 { 465 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER.get( 466 entryDN, parentDN, unacceptableReason)); 467 } 468 } 469 470 // Add the entry. 471 final ConfigLdapResultHandler resultHandler = new ConfigLdapResultHandler(); 472 backend.handleAdd(UNCANCELLABLE_REQUEST_CONTEXT, Requests.newAddRequest(entry), null, resultHandler); 473 474 if (!resultHandler.hasCompletedSuccessfully()) 475 { 476 LdapException ex = resultHandler.getResultError(); 477 throw new DirectoryException(ex.getResult().getResultCode(), 478 ERR_CONFIG_FILE_ADD_FAILED.get(entryDN, parentDN, ex.getLocalizedMessage()), ex); 479 } 480 writeUpdatedConfig(); 481 482 // Notify all the add listeners to apply the new configuration entry. 483 final ConfigChangeResult ccr = new ConfigChangeResult(); 484 for (final ConfigAddListener listener : addListeners) 485 { 486 final ConfigChangeResult result = listener.applyConfigurationAdd(entry); 487 ccr.aggregate(result); 488 handleConfigChangeResult(result, entry.getName(), listener.getClass().getName(), "applyConfigurationAdd"); 489 } 490 491 if (ccr.getResultCode() != ResultCode.SUCCESS) 492 { 493 final String reasons = Utils.joinAsString(". ", ccr.getMessages()); 494 throw new DirectoryException(ccr.getResultCode(), ERR_CONFIG_FILE_ADD_APPLY_FAILED.get(reasons)); 495 } 496 } 497 498 /** 499 * Delete a configuration entry. 500 * <p> 501 * The delete is performed only if all Delete listeners on the parent entry accept the changes. 502 * Once the change is accepted, entry is effectively deleted and all Delete listeners are called 503 * again to apply the change resulting from this deletion. 504 * 505 * @param dn 506 * DN of entry to delete. 507 * @throws DirectoryException 508 * If a problem occurs. 509 */ 510 public void deleteEntry(final DN dn) throws DirectoryException 511 { 512 // Entry must exist. 513 if (!backend.contains(dn)) 514 { 515 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 516 ERR_CONFIG_FILE_DELETE_NO_SUCH_ENTRY.get(dn), getMatchedDN(dn), null); 517 } 518 519 // Entry must not have children. 520 try 521 { 522 if (!getChildren(dn).isEmpty()) 523 { 524 throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF, ERR_CONFIG_FILE_DELETE_HAS_CHILDREN.get(dn)); 525 } 526 } 527 catch (ConfigException e) 528 { 529 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 530 ERR_CONFIG_BACKEND_CANNOT_DELETE_ENTRY.get(stackTraceToSingleLineString(e)), e); 531 } 532 533 final DN parentDN = retrieveParentDNForDelete(dn); 534 535 // Iterate through delete listeners to make sure the deletion is acceptable. 536 final List<ConfigDeleteListener> deleteListeners = getDeleteListeners(parentDN); 537 final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder(); 538 final Entry entry = backend.get(dn); 539 for (final ConfigDeleteListener listener : deleteListeners) 540 { 541 if (!listener.configDeleteIsAcceptable(entry, unacceptableReason)) 542 { 543 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 544 ERR_CONFIG_FILE_DELETE_REJECTED_BY_LISTENER.get(entry, parentDN, unacceptableReason)); 545 } 546 } 547 548 // Delete the entry and all listeners on the entry 549 final ConfigLdapResultHandler resultHandler = new ConfigLdapResultHandler(); 550 backend.handleDelete(UNCANCELLABLE_REQUEST_CONTEXT, Requests.newDeleteRequest(dn), null, resultHandler); 551 listeners.remove(dn); 552 553 if (!resultHandler.hasCompletedSuccessfully()) 554 { 555 LdapException ex = resultHandler.getResultError(); 556 throw new DirectoryException(ex.getResult().getResultCode(), 557 ERR_CONFIG_FILE_DELETE_FAILED.get(dn, parentDN, ex.getLocalizedMessage()), ex); 558 } 559 writeUpdatedConfig(); 560 561 // Notify all the delete listeners that the entry has been removed. 562 final ConfigChangeResult ccr = new ConfigChangeResult(); 563 for (final ConfigDeleteListener listener : deleteListeners) 564 { 565 final ConfigChangeResult result = listener.applyConfigurationDelete(entry); 566 ccr.aggregate(result); 567 handleConfigChangeResult(result, dn, listener.getClass().getName(), "applyConfigurationDelete"); 568 } 569 570 if (ccr.getResultCode() != ResultCode.SUCCESS) 571 { 572 final String reasons = Utils.joinAsString(". ", ccr.getMessages()); 573 throw new DirectoryException(ccr.getResultCode(), ERR_CONFIG_FILE_DELETE_APPLY_FAILED.get(reasons)); 574 } 575 } 576 577 /** 578 * Replaces the old configuration entry with the new configuration entry provided. 579 * <p> 580 * The replacement is performed only if all Change listeners on the entry accept the changes. Once 581 * the change is accepted, entry is effectively replaced and all Change listeners are called again 582 * to apply the change resulting from the replacement. 583 * 584 * @param oldEntry 585 * The original entry that is being replaced. 586 * @param newEntry 587 * The new entry to use in place of the existing entry with the same DN. 588 * @throws DirectoryException 589 * If a problem occurs while trying to replace the entry. 590 */ 591 @ActivateOnceSDKSchemaIsUsed("uncomment code down below in this method") 592 public void replaceEntry(final Entry oldEntry, final Entry newEntry) throws DirectoryException 593 { 594 final DN newEntryDN = newEntry.getName(); 595 if (!backend.contains(newEntryDN)) 596 { 597 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 598 ERR_CONFIG_FILE_MODIFY_NO_SUCH_ENTRY.get(oldEntry), getMatchedDN(newEntryDN), null); 599 } 600 601 // TODO : add objectclass and attribute to the config schema in order to get this code run 602 // if (!Entries.getStructuralObjectClass(oldEntry, configEnabledSchema) 603 // .equals(Entries.getStructuralObjectClass(newEntry, configEnabledSchema))) 604 // { 605 // throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, 606 // ERR_CONFIG_FILE_MODIFY_STRUCTURAL_CHANGE_NOT_ALLOWED.get(entryDN)); 607 // } 608 609 // Iterate through change listeners to make sure the change is acceptable. 610 final List<ConfigChangeListener> changeListeners = getChangeListeners(newEntryDN); 611 final LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder(); 612 for (ConfigChangeListener listeners : changeListeners) 613 { 614 if (!listeners.configChangeIsAcceptable(newEntry, unacceptableReason)) 615 { 616 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 617 ERR_CONFIG_FILE_MODIFY_REJECTED_BY_CHANGE_LISTENER.get(newEntryDN, unacceptableReason)); 618 } 619 } 620 621 // Replace the old entry with new entry. 622 ModifyRequest modifyRequest = Entries.diffEntries(oldEntry, newEntry, Entries.diffOptions().attributes("*", "+")); 623 final ConfigLdapResultHandler resultHandler = new ConfigLdapResultHandler(); 624 backend.handleModify(UNCANCELLABLE_REQUEST_CONTEXT, modifyRequest, null, resultHandler); 625 626 if (!resultHandler.hasCompletedSuccessfully()) 627 { 628 LdapException ex = resultHandler.getResultError(); 629 throw new DirectoryException(ex.getResult().getResultCode(), 630 ERR_CONFIG_FILE_MODIFY_FAILED.get(newEntryDN, newEntryDN, ex.getLocalizedMessage()), ex); 631 } 632 writeUpdatedConfig(); 633 634 // Notify all the change listeners of the update. 635 final ConfigChangeResult ccr = new ConfigChangeResult(); 636 for (final ConfigChangeListener listener : changeListeners) 637 { 638 if (!changeListeners.contains(listener)) 639 { 640 // some listeners may have de-registered themselves due to previous changes, ignore them 641 continue; 642 } 643 final ConfigChangeResult result = listener.applyConfigurationChange(newEntry); 644 ccr.aggregate(result); 645 handleConfigChangeResult(result, newEntryDN, listener.getClass().getName(), "applyConfigurationChange"); 646 } 647 648 if (ccr.getResultCode() != ResultCode.SUCCESS) 649 { 650 String reasons = Utils.joinAsString(". ", ccr.getMessages()); 651 throw new DirectoryException(ccr.getResultCode(), ERR_CONFIG_FILE_MODIFY_APPLY_FAILED.get(reasons)); 652 } 653 } 654 655 @Override 656 public void registerAddListener(final DN dn, final ConfigAddListener listener) 657 { 658 getEntryListeners(dn).registerAddListener(listener); 659 } 660 661 @Override 662 public void registerDeleteListener(final DN dn, final ConfigDeleteListener listener) 663 { 664 getEntryListeners(dn).registerDeleteListener(listener); 665 } 666 667 @Override 668 public void registerChangeListener(final DN dn, final ConfigChangeListener listener) 669 { 670 getEntryListeners(dn).registerChangeListener(listener); 671 } 672 673 @Override 674 public void deregisterAddListener(final DN dn, final ConfigAddListener listener) 675 { 676 getEntryListeners(dn).deregisterAddListener(listener); 677 } 678 679 @Override 680 public void deregisterDeleteListener(final DN dn, final ConfigDeleteListener listener) 681 { 682 getEntryListeners(dn).deregisterDeleteListener(listener); 683 } 684 685 @Override 686 public boolean deregisterChangeListener(final DN dn, final ConfigChangeListener listener) 687 { 688 return getEntryListeners(dn).deregisterChangeListener(listener); 689 } 690 691 /** 692 * Writes the current configuration to LDIF with the provided export configuration. 693 * 694 * @param exportConfig 695 * The configuration to use for the export. 696 * @throws DirectoryException 697 * If a problem occurs while writing the LDIF. 698 */ 699 public void writeLDIF(LDIFExportConfig exportConfig) throws DirectoryException 700 { 701 try (LDIFEntryWriter writer = new LDIFEntryWriter(exportConfig.getWriter())) 702 { 703 writer.writeComment(INFO_CONFIG_FILE_HEADER.get().toString()); 704 for (Entry entry : new ArrayList<Entry>(backend.getAll())) 705 { 706 try 707 { 708 writer.writeEntry(entry); 709 } 710 catch (IOException e) 711 { 712 logger.traceException(e); 713 LocalizableMessage message = ERR_CONFIG_FILE_WRITE_ERROR.get(entry.getName(), e); 714 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 715 } 716 } 717 } 718 catch (IOException e) 719 { 720 logger.traceException(e); 721 LocalizableMessage message = ERR_CONFIG_LDIF_WRITE_ERROR.get(e); 722 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 723 } 724 } 725 726 /** 727 * Generates a configuration file with the ".startok" suffix, representing a configuration 728 * file that has a successful start. 729 * <p> 730 * This method must not be called if configuration can't be correctly initialized. 731 * <p> 732 * The actual generation is skipped if last known good configuration is used. 733 */ 734 public void writeSuccessfulStartupConfig() 735 { 736 if (useLastKnownGoodConfig) 737 { 738 // The server was started with the "last known good" configuration, so we 739 // shouldn't overwrite it with something that is probably bad. 740 return; 741 } 742 743 String startOKFilePath = configFile + ".startok"; 744 String tempFilePath = startOKFilePath + ".tmp"; 745 String oldFilePath = startOKFilePath + ".old"; 746 747 // Copy the current config file to a temporary file. 748 File tempFile = new File(tempFilePath); 749 try (FileInputStream inputStream = new FileInputStream(configFile)) 750 { 751 try (FileOutputStream outputStream = new FileOutputStream(tempFilePath, false)) 752 { 753 try 754 { 755 byte[] buffer = new byte[8192]; 756 while (true) 757 { 758 int bytesRead = inputStream.read(buffer); 759 if (bytesRead < 0) 760 { 761 break; 762 } 763 764 outputStream.write(buffer, 0, bytesRead); 765 } 766 } 767 catch (IOException e) 768 { 769 logger.traceException(e); 770 logger.error(ERR_STARTOK_CANNOT_WRITE, configFile, tempFilePath, getExceptionMessage(e)); 771 return; 772 } 773 } 774 catch (FileNotFoundException e) 775 { 776 logger.traceException(e); 777 logger.error(ERR_STARTOK_CANNOT_OPEN_FOR_WRITING, tempFilePath, getExceptionMessage(e)); 778 return; 779 } 780 catch (IOException e) 781 { 782 logger.traceException(e); 783 } 784 } 785 catch (FileNotFoundException e) 786 { 787 logger.traceException(e); 788 logger.error(ERR_STARTOK_CANNOT_OPEN_FOR_READING, configFile, getExceptionMessage(e)); 789 return; 790 } 791 catch (IOException e) 792 { 793 logger.traceException(e); 794 } 795 796 // If a ".startok" file already exists, then move it to an ".old" file. 797 File oldFile = new File(oldFilePath); 798 try 799 { 800 if (oldFile.exists()) 801 { 802 oldFile.delete(); 803 } 804 } 805 catch (Exception e) 806 { 807 logger.traceException(e); 808 } 809 810 File startOKFile = new File(startOKFilePath); 811 try 812 { 813 if (startOKFile.exists()) 814 { 815 startOKFile.renameTo(oldFile); 816 } 817 } 818 catch (Exception e) 819 { 820 logger.traceException(e); 821 } 822 823 // Rename the temp file to the ".startok" file. 824 try 825 { 826 tempFile.renameTo(startOKFile); 827 } 828 catch (Exception e) 829 { 830 logger.traceException(e); 831 logger.error(ERR_STARTOK_CANNOT_RENAME, tempFilePath, startOKFilePath, getExceptionMessage(e)); 832 return; 833 } 834 835 // Remove the ".old" file if there is one. 836 try 837 { 838 if (oldFile.exists()) 839 { 840 oldFile.delete(); 841 } 842 } 843 catch (Exception e) 844 { 845 logger.traceException(e); 846 } 847 } 848 849 private void writeUpdatedConfig() throws DirectoryException 850 { 851 // FIXME -- This needs support for encryption. 852 853 // Calculate an archive for the current server configuration file and see if 854 // it matches what we expect. If not, then the file has been manually 855 // edited with the server online which is a bad thing. In that case, we'll 856 // copy the current config off to the side before writing the new config 857 // so that the manual changes don't get lost but also don't get applied. 858 // Also, send an admin alert notifying administrators about the problem. 859 if (maintainConfigArchive) 860 { 861 try 862 { 863 byte[] currentDigest = calculateConfigDigest(); 864 if (!Arrays.equals(configurationDigest, currentDigest)) 865 { 866 File existingCfg = configFile; 867 File newConfigFile = 868 new File(existingCfg.getParent(), "config.manualedit-" + TimeThread.getGMTTime() + ".ldif"); 869 int counter = 2; 870 while (newConfigFile.exists()) 871 { 872 newConfigFile = new File(newConfigFile.getAbsolutePath() + "." + counter); 873 } 874 875 try (FileInputStream inputStream = new FileInputStream(existingCfg); 876 FileOutputStream outputStream = new FileOutputStream(newConfigFile)) 877 { 878 byte[] buffer = new byte[8192]; 879 while (true) 880 { 881 int bytesRead = inputStream.read(buffer); 882 if (bytesRead < 0) 883 { 884 break; 885 } 886 outputStream.write(buffer, 0, bytesRead); 887 } 888 } 889 890 LocalizableMessage message = 891 WARN_CONFIG_MANUAL_CHANGES_DETECTED.get(configFile, newConfigFile.getAbsolutePath()); 892 logger.warn(message); 893 894 DirectoryServer.sendAlertNotification(this, ALERT_TYPE_MANUAL_CONFIG_EDIT_HANDLED, message); 895 } 896 } 897 catch (Exception e) 898 { 899 logger.traceException(e); 900 901 LocalizableMessage message = ERR_CONFIG_MANUAL_CHANGES_LOST.get(configFile, stackTraceToSingleLineString(e)); 902 logger.error(message); 903 904 DirectoryServer.sendAlertNotification(this, ALERT_TYPE_MANUAL_CONFIG_EDIT_HANDLED, message); 905 } 906 } 907 908 // Write the new configuration to a temporary file. 909 String tempConfig = configFile + ".tmp"; 910 try 911 { 912 LDIFExportConfig exportConfig = new LDIFExportConfig(tempConfig, ExistingFileBehavior.OVERWRITE); 913 914 // FIXME -- Add all the appropriate configuration options. 915 writeLDIF(exportConfig); 916 } 917 catch (Exception e) 918 { 919 logger.traceException(e); 920 921 LocalizableMessage message = 922 ERR_CONFIG_FILE_WRITE_CANNOT_EXPORT_NEW_CONFIG.get(tempConfig, stackTraceToSingleLineString(e)); 923 logger.error(message); 924 925 DirectoryServer.sendAlertNotification(this, ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message); 926 return; 927 } 928 929 // Delete the previous version of the configuration and rename the new one. 930 try 931 { 932 File actualConfig = configFile; 933 File tmpConfig = new File(tempConfig); 934 renameFile(tmpConfig, actualConfig); 935 } 936 catch (Exception e) 937 { 938 logger.traceException(e); 939 940 LocalizableMessage message = 941 ERR_CONFIG_FILE_WRITE_CANNOT_RENAME_NEW_CONFIG.get(tempConfig, configFile, stackTraceToSingleLineString(e)); 942 logger.error(message); 943 944 DirectoryServer.sendAlertNotification(this, ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message); 945 return; 946 } 947 948 configurationDigest = calculateConfigDigest(); 949 950 // Try to write the archive for the new configuration. 951 if (maintainConfigArchive) 952 { 953 writeConfigArchive(); 954 } 955 } 956 957 /** Request context to be used when requesting the internal backend. */ 958 private static final RequestContext UNCANCELLABLE_REQUEST_CONTEXT = new RequestContext() 959 { 960 @Override 961 public void removeCancelRequestListener(final CancelRequestListener listener) 962 { 963 // nothing to do 964 } 965 966 @Override 967 public int getMessageID() 968 { 969 return -1; 970 } 971 972 @Override 973 public void checkIfCancelled(final boolean signalTooLate) throws CancelledResultException 974 { 975 // nothing to do 976 } 977 978 @Override 979 public void addCancelRequestListener(final CancelRequestListener listener) 980 { 981 // nothing to do 982 } 983 }; 984 985 /** Holds add, change and delete listeners for a given configuration entry. */ 986 private static class EntryListeners 987 { 988 /** The set of add listeners that have been registered with this entry. */ 989 private final CopyOnWriteArrayList<ConfigAddListener> addListeners = new CopyOnWriteArrayList<>(); 990 /** The set of change listeners that have been registered with this entry. */ 991 private final CopyOnWriteArrayList<ConfigChangeListener> changeListeners = new CopyOnWriteArrayList<>(); 992 /** The set of delete listeners that have been registered with this entry. */ 993 private final CopyOnWriteArrayList<ConfigDeleteListener> deleteListeners = new CopyOnWriteArrayList<>(); 994 995 CopyOnWriteArrayList<ConfigChangeListener> getChangeListeners() 996 { 997 return changeListeners; 998 } 999 1000 void registerChangeListener(final ConfigChangeListener listener) 1001 { 1002 changeListeners.add(listener); 1003 } 1004 1005 boolean deregisterChangeListener(final ConfigChangeListener listener) 1006 { 1007 return changeListeners.remove(listener); 1008 } 1009 1010 CopyOnWriteArrayList<ConfigAddListener> getAddListeners() 1011 { 1012 return addListeners; 1013 } 1014 1015 void registerAddListener(final ConfigAddListener listener) 1016 { 1017 addListeners.addIfAbsent(listener); 1018 } 1019 1020 void deregisterAddListener(final ConfigAddListener listener) 1021 { 1022 addListeners.remove(listener); 1023 } 1024 1025 CopyOnWriteArrayList<ConfigDeleteListener> getDeleteListeners() 1026 { 1027 return deleteListeners; 1028 } 1029 1030 void registerDeleteListener(final ConfigDeleteListener listener) 1031 { 1032 deleteListeners.addIfAbsent(listener); 1033 } 1034 1035 void deregisterDeleteListener(final ConfigDeleteListener listener) 1036 { 1037 deleteListeners.remove(listener); 1038 } 1039 } 1040 1041 /** Handler for search results collecting all received entries. */ 1042 private static final class CollectorSearchResultHandler implements SearchResultHandler 1043 { 1044 private final Set<Entry> entries = new HashSet<>(); 1045 1046 Set<Entry> getEntries() 1047 { 1048 return entries; 1049 } 1050 1051 @Override 1052 public boolean handleReference(SearchResultReference reference) 1053 { 1054 throw new UnsupportedOperationException("Search references are not supported for configuration entries."); 1055 } 1056 1057 @Override 1058 public boolean handleEntry(SearchResultEntry entry) 1059 { 1060 entries.add(entry); 1061 return true; 1062 } 1063 } 1064 1065 /** Handler for search results redirecting to a SearchOperation. */ 1066 private static final class SearchResultHandlerAdapter implements SearchResultHandler 1067 { 1068 private final SearchOperation searchOperation; 1069 private final LdapResultHandlerAdapter resultHandler; 1070 1071 private SearchResultHandlerAdapter(SearchOperation searchOperation, LdapResultHandlerAdapter resultHandler) 1072 { 1073 this.searchOperation = searchOperation; 1074 this.resultHandler = resultHandler; 1075 } 1076 1077 @Override 1078 public boolean handleReference(SearchResultReference reference) 1079 { 1080 throw new UnsupportedOperationException("Search references are not supported for configuration entries."); 1081 } 1082 1083 @Override 1084 public boolean handleEntry(SearchResultEntry entry) 1085 { 1086 org.opends.server.types.Entry serverEntry = Converters.to(entry); 1087 serverEntry.processVirtualAttributes(); 1088 return !filterMatchesEntry(serverEntry) || searchOperation.returnEntry(serverEntry, null); 1089 } 1090 1091 private boolean filterMatchesEntry(org.opends.server.types.Entry serverEntry) 1092 { 1093 try 1094 { 1095 return searchOperation.getFilter().matchesEntry(serverEntry); 1096 } 1097 catch (DirectoryException e) 1098 { 1099 resultHandler.handleException(LdapException.newLdapException(ResultCode.UNWILLING_TO_PERFORM, e)); 1100 return false; 1101 } 1102 } 1103 } 1104 1105 /** Handler for LDAP operations. */ 1106 private static final class ConfigLdapResultHandler implements LdapResultHandler<Result> 1107 { 1108 private LdapException resultError; 1109 1110 LdapException getResultError() 1111 { 1112 return resultError; 1113 } 1114 1115 boolean hasCompletedSuccessfully() 1116 { 1117 return resultError == null; 1118 } 1119 1120 @Override 1121 public void handleResult(Result result) 1122 { 1123 // nothing to do 1124 } 1125 1126 @Override 1127 public void handleException(LdapException exception) 1128 { 1129 resultError = exception; 1130 } 1131 } 1132 1133 /** Handler for LDAP operations redirecting to a SearchOperation. */ 1134 private static final class LdapResultHandlerAdapter implements LdapResultHandler<Result> 1135 { 1136 private final SearchOperation searchOperation; 1137 1138 LdapResultHandlerAdapter(SearchOperation searchOperation) 1139 { 1140 this.searchOperation = searchOperation; 1141 } 1142 1143 @Override 1144 public void handleResult(Result result) 1145 { 1146 searchOperation.setResultCode(result.getResultCode()); 1147 } 1148 1149 @Override 1150 public void handleException(LdapException exception) 1151 { 1152 searchOperation.setResultCode(exception.getResult().getResultCode()); 1153 searchOperation.setErrorMessage( 1154 new LocalizableMessageBuilder(LocalizableMessage.raw(exception.getLocalizedMessage()))); 1155 String matchedDNString = exception.getResult().getMatchedDN(); 1156 if (matchedDNString != null) 1157 { 1158 searchOperation.setMatchedDN(DN.valueOf(matchedDNString)); 1159 } 1160 } 1161 } 1162 1163 /** 1164 * Find the actual configuration file to use to load configuration, given the standard 1165 * configuration file. 1166 * 1167 * @param standardConfigFile 1168 * "Standard" configuration file provided. 1169 * @return the actual configuration file to use, which is either the standard config file provided 1170 * or the config file corresponding to the last known good configuration 1171 * @throws InitializationException 1172 * If a problem occurs. 1173 */ 1174 private File findConfigFileToUse(final File standardConfigFile) throws InitializationException 1175 { 1176 File fileToUse; 1177 if (useLastKnownGoodConfig) 1178 { 1179 fileToUse = new File(standardConfigFile.getPath() + ".startok"); 1180 if (fileToUse.exists()) 1181 { 1182 logger.info(NOTE_CONFIG_FILE_USING_STARTOK_FILE, fileToUse.getAbsolutePath(), standardConfigFile); 1183 } 1184 else 1185 { 1186 logger.warn(WARN_CONFIG_FILE_NO_STARTOK_FILE, fileToUse.getAbsolutePath(), standardConfigFile); 1187 useLastKnownGoodConfig = false; 1188 fileToUse = standardConfigFile; 1189 } 1190 } 1191 else 1192 { 1193 fileToUse = standardConfigFile; 1194 } 1195 1196 boolean fileExists = false; 1197 try 1198 { 1199 fileExists = fileToUse.exists(); 1200 } 1201 catch (Exception e) 1202 { 1203 logger.traceException(e); 1204 throw new InitializationException(ERR_CONFIG_FILE_CANNOT_VERIFY_EXISTENCE.get(fileToUse.getAbsolutePath(), e)); 1205 } 1206 if (!fileExists) 1207 { 1208 throw new InitializationException(ERR_CONFIG_FILE_DOES_NOT_EXIST.get(fileToUse.getAbsolutePath())); 1209 } 1210 return fileToUse; 1211 } 1212 1213 /** Load the configuration-enabled schema that will allow to read the configuration file. */ 1214 private Schema loadSchemaWithConfigurationEnabled() throws InitializationException 1215 { 1216 final File schemaDir = serverContext.getEnvironment().getSchemaDirectory(); 1217 try (LDIFEntryReader reader = new LDIFEntryReader(new FileReader(new File(schemaDir, CONFIGURATION_FILE_NAME)))) 1218 { 1219 final Schema schema = Schema.getDefaultSchema(); 1220 reader.setSchema(schema); 1221 final Entry entry = reader.readEntry(); 1222 return new SchemaBuilder(schema).addSchema(entry, false).toSchema().asNonStrictSchema(); 1223 } 1224 catch (Exception e) 1225 { 1226 throw new InitializationException( 1227 ERR_UNABLE_TO_LOAD_CONFIGURATION_ENABLED_SCHEMA.get(stackTraceToSingleLineString(e)), e); 1228 } 1229 } 1230 1231 /** 1232 * Read configuration entries from provided configuration file. 1233 * 1234 * @param configFile 1235 * LDIF file with configuration entries. 1236 * @param schema 1237 * Schema to validate entries when reading the config file. 1238 * @throws InitializationException 1239 * If an errors occurs. 1240 */ 1241 private void loadConfiguration(final File configFile, final Schema schema) throws InitializationException 1242 { 1243 try (EntryReader reader = getLDIFReader(configFile, schema)) 1244 { 1245 backend = new MemoryBackend(schema, reader); 1246 } 1247 catch (IOException e) 1248 { 1249 throw new InitializationException( 1250 ERR_CONFIG_FILE_GENERIC_ERROR.get(configFile.getAbsolutePath(), e.getCause()), e); 1251 } 1252 1253 // Check that root entry is the expected one 1254 rootEntry = backend.get(DN_CONFIG_ROOT); 1255 if (rootEntry == null) 1256 { 1257 // fix message : we didn't find the expected root in the file 1258 throw new InitializationException( 1259 ERR_CONFIG_FILE_INVALID_BASE_DN.get(configFile.getAbsolutePath(), "", DN_CONFIG_ROOT)); 1260 } 1261 } 1262 1263 /** 1264 * Ensure there is an-up-to-date configuration archive. 1265 * <p> 1266 * Check to see if a configuration archive exists. If not, then create one. 1267 * If so, then check whether the current configuration matches the last 1268 * configuration in the archive. If it doesn't, then archive it. 1269 */ 1270 private void ensureArchiveExistsAndIsUpToDate(DirectoryEnvironmentConfig environment, File configFileToUse) 1271 throws InitializationException 1272 { 1273 maintainConfigArchive = environment.maintainConfigArchive(); 1274 maxConfigArchiveSize = environment.getMaxConfigArchiveSize(); 1275 if (maintainConfigArchive && !useLastKnownGoodConfig) 1276 { 1277 try 1278 { 1279 configurationDigest = calculateConfigDigest(); 1280 } 1281 catch (DirectoryException e) 1282 { 1283 throw new InitializationException(e.getMessageObject(), e.getCause()); 1284 } 1285 1286 File archiveDirectory = new File(configFileToUse.getParent(), CONFIG_ARCHIVE_DIR_NAME); 1287 if (archiveDirectory.exists()) 1288 { 1289 try 1290 { 1291 byte[] lastDigest = getLastConfigDigest(archiveDirectory); 1292 if (!Arrays.equals(configurationDigest, lastDigest)) 1293 { 1294 writeConfigArchive(); 1295 } 1296 } 1297 catch (DirectoryException e) 1298 { 1299 throw new InitializationException(e.getMessageObject(), e.getCause()); 1300 } 1301 } 1302 else 1303 { 1304 writeConfigArchive(); 1305 } 1306 } 1307 } 1308 1309 /** Writes the current configuration to the configuration archive. This will be a best-effort attempt. */ 1310 private void writeConfigArchive() 1311 { 1312 if (!maintainConfigArchive) 1313 { 1314 return; 1315 } 1316 File archiveDirectory = new File(configFile.getParentFile(), CONFIG_ARCHIVE_DIR_NAME); 1317 try 1318 { 1319 createArchiveDirectoryIfNeeded(archiveDirectory); 1320 File archiveFile = getNewArchiveFile(archiveDirectory); 1321 copyCurrentConfigFileToArchiveFile(archiveFile); 1322 removeOldArchiveFilesIfNeeded(archiveDirectory); 1323 } 1324 catch (DirectoryException e) 1325 { 1326 LocalizableMessage message = e.getMessageObject(); 1327 logger.error(message); 1328 DirectoryServer.sendAlertNotification(this, ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message); 1329 } 1330 } 1331 1332 private void createArchiveDirectoryIfNeeded(File archiveDirectory) throws DirectoryException 1333 { 1334 if (!archiveDirectory.exists()) 1335 { 1336 try 1337 { 1338 if (!archiveDirectory.mkdirs()) 1339 { 1340 throw new DirectoryException(ResultCode.UNDEFINED, 1341 ERR_CONFIG_FILE_CANNOT_CREATE_ARCHIVE_DIR_NO_REASON.get(archiveDirectory.getAbsolutePath())); 1342 } 1343 } 1344 catch (Exception e) 1345 { 1346 logger.traceException(e); 1347 throw new DirectoryException(ResultCode.UNDEFINED, 1348 ERR_CONFIG_FILE_CANNOT_CREATE_ARCHIVE_DIR.get(archiveDirectory.getAbsolutePath(), 1349 stackTraceToSingleLineString(e)), e); 1350 } 1351 } 1352 } 1353 1354 private File getNewArchiveFile(File archiveDirectory) throws DirectoryException 1355 { 1356 try 1357 { 1358 String timestamp = TimeThread.getGMTTime(); 1359 File archiveFile = new File(archiveDirectory, "config-" + timestamp + ".gz"); 1360 if (archiveFile.exists()) 1361 { 1362 int counter = 1; 1363 do 1364 { 1365 counter++; 1366 archiveFile = new File(archiveDirectory, "config-" + timestamp + "-" + counter + ".gz"); 1367 } 1368 while (archiveFile.exists()); 1369 } 1370 return archiveFile; 1371 } 1372 catch (Exception e) 1373 { 1374 logger.traceException(e); 1375 throw new DirectoryException(ResultCode.UNDEFINED, 1376 ERR_CONFIG_FILE_CANNOT_WRITE_CONFIG_ARCHIVE.get(stackTraceToSingleLineString(e))); 1377 } 1378 } 1379 1380 /** Copy the current configuration file to the archive configuration file. */ 1381 private void copyCurrentConfigFileToArchiveFile(File archiveFile) throws DirectoryException 1382 { 1383 byte[] buffer = new byte[8192]; 1384 try(FileInputStream inputStream = new FileInputStream(configFile); 1385 GZIPOutputStream outputStream = new GZIPOutputStream(new FileOutputStream(archiveFile))) 1386 { 1387 int bytesRead = inputStream.read(buffer); 1388 while (bytesRead > 0) 1389 { 1390 outputStream.write(buffer, 0, bytesRead); 1391 bytesRead = inputStream.read(buffer); 1392 } 1393 } 1394 catch (IOException e) 1395 { 1396 logger.traceException(e); 1397 throw new DirectoryException(ResultCode.UNDEFINED, 1398 ERR_CONFIG_FILE_CANNOT_WRITE_CONFIG_ARCHIVE.get(stackTraceToSingleLineString(e))); 1399 } 1400 } 1401 1402 /** Deletes old archives files if we should enforce a maximum number of archived configurations. */ 1403 private void removeOldArchiveFilesIfNeeded(File archiveDirectory) 1404 { 1405 if (maxConfigArchiveSize > 0) 1406 { 1407 String[] archivedFileList = archiveDirectory.list(); 1408 int numToDelete = archivedFileList.length - maxConfigArchiveSize; 1409 if (numToDelete > 0) 1410 { 1411 Set<String> archiveSet = new TreeSet<>(); 1412 for (String name : archivedFileList) 1413 { 1414 if (!name.startsWith("config-")) 1415 { 1416 continue; 1417 } 1418 // Simply ordering by filename should work, even when there are 1419 // timestamp conflicts, because the dash comes before the period in 1420 // the ASCII character set. 1421 archiveSet.add(name); 1422 } 1423 Iterator<String> iterator = archiveSet.iterator(); 1424 for (int i = 0; i < numToDelete && iterator.hasNext(); i++) 1425 { 1426 File archive = new File(archiveDirectory, iterator.next()); 1427 try 1428 { 1429 archive.delete(); 1430 } 1431 catch (Exception e) 1432 { 1433 // do nothing 1434 } 1435 } 1436 } 1437 } 1438 } 1439 1440 /** 1441 * Looks at the existing archive directory, finds the latest archive file, and calculates a SHA-1 1442 * digest of that file. 1443 * 1444 * @return The calculated digest of the most recent archived configuration file. 1445 * @throws DirectoryException 1446 * If a problem occurs while calculating the digest. 1447 */ 1448 private byte[] getLastConfigDigest(File archiveDirectory) throws DirectoryException 1449 { 1450 int latestCounter = 0; 1451 long latestTimestamp = -1; 1452 String latestFileName = null; 1453 for (String name : archiveDirectory.list()) 1454 { 1455 if (!name.startsWith("config-")) 1456 { 1457 continue; 1458 } 1459 int dotPos = name.indexOf('.', 7); 1460 if (dotPos < 0) 1461 { 1462 continue; 1463 } 1464 int dashPos = name.indexOf('-', 7); 1465 if (dashPos < 0) 1466 { 1467 try 1468 { 1469 ByteString ts = ByteString.valueOfUtf8(name.substring(7, dotPos)); 1470 long timestamp = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(ts); 1471 if (timestamp > latestTimestamp) 1472 { 1473 latestFileName = name; 1474 latestTimestamp = timestamp; 1475 latestCounter = 0; 1476 continue; 1477 } 1478 } 1479 catch (Exception e) 1480 { 1481 continue; 1482 } 1483 } 1484 else 1485 { 1486 try 1487 { 1488 ByteString ts = ByteString.valueOfUtf8(name.substring(7, dashPos)); 1489 long timestamp = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(ts); 1490 int counter = Integer.parseInt(name.substring(dashPos + 1, dotPos)); 1491 1492 if (timestamp > latestTimestamp 1493 || (timestamp == latestTimestamp && counter > latestCounter)) 1494 { 1495 latestFileName = name; 1496 latestTimestamp = timestamp; 1497 latestCounter = counter; 1498 continue; 1499 } 1500 } 1501 catch (Exception e) 1502 { 1503 continue; 1504 } 1505 } 1506 } 1507 1508 if (latestFileName == null) 1509 { 1510 return null; 1511 } 1512 1513 File latestFile = new File(archiveDirectory, latestFileName); 1514 try (GZIPInputStream inputStream = new GZIPInputStream(new FileInputStream(latestFile))) 1515 { 1516 return calculateDigest(inputStream); 1517 } 1518 catch (Exception e) 1519 { 1520 LocalizableMessage message = 1521 ERR_CONFIG_CANNOT_CALCULATE_DIGEST.get(latestFile.getAbsolutePath(), stackTraceToSingleLineString(e)); 1522 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 1523 } 1524 } 1525 1526 /** 1527 * Calculates a SHA-1 digest of the current configuration file. 1528 * 1529 * @return The calculated configuration digest. 1530 * @throws DirectoryException 1531 * If a problem occurs while calculating the digest. 1532 */ 1533 private byte[] calculateConfigDigest() throws DirectoryException 1534 { 1535 try (InputStream inputStream = new FileInputStream(configFile)) 1536 { 1537 return calculateDigest(inputStream); 1538 } 1539 catch (Exception e) 1540 { 1541 LocalizableMessage message = ERR_CONFIG_CANNOT_CALCULATE_DIGEST.get(configFile, stackTraceToSingleLineString(e)); 1542 throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e); 1543 } 1544 } 1545 1546 private byte[] calculateDigest(InputStream inputStream) throws NoSuchAlgorithmException, IOException 1547 { 1548 MessageDigest sha1Digest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1); 1549 byte[] buffer = new byte[8192]; 1550 while (true) 1551 { 1552 int bytesRead = inputStream.read(buffer); 1553 if (bytesRead < 0) 1554 { 1555 break; 1556 } 1557 sha1Digest.update(buffer, 0, bytesRead); 1558 } 1559 return sha1Digest.digest(); 1560 } 1561 1562 /** 1563 * Applies the updates in the provided changes file to the content in the specified source file. 1564 * The result will be written to a temporary file, the current source file will be moved out of 1565 * place, and then the updated file will be moved into the place of the original file. The changes 1566 * file will also be renamed so it won't be applied again. <BR> 1567 * <BR> 1568 * If any problems are encountered, then the config initialization process will be aborted. 1569 * 1570 * @param sourceFile 1571 * The LDIF file containing the source data. 1572 * @param changesFile 1573 * The LDIF file containing the changes to apply. 1574 * @throws IOException 1575 * If a problem occurs while performing disk I/O. 1576 * @throws LDIFException 1577 * If a problem occurs while trying to interpret the data. 1578 */ 1579 private void applyChangesFile(File sourceFile, File changesFile) throws IOException, LDIFException 1580 { 1581 // Create the appropriate LDIF readers and writer. 1582 LDIFImportConfig sourceImportCfg = new LDIFImportConfig(sourceFile.getAbsolutePath()); 1583 sourceImportCfg.setValidateSchema(false); 1584 1585 LDIFImportConfig changesImportCfg = new LDIFImportConfig(changesFile.getAbsolutePath()); 1586 changesImportCfg.setValidateSchema(false); 1587 1588 String tempFile = changesFile.getAbsolutePath() + ".tmp"; 1589 LDIFExportConfig exportConfig = new LDIFExportConfig(tempFile, ExistingFileBehavior.OVERWRITE); 1590 1591 List<LocalizableMessage> errorList = new LinkedList<>(); 1592 boolean successful; 1593 try (LDIFReader sourceReader = new LDIFReader(sourceImportCfg); 1594 LDIFReader changesReader = new LDIFReader(changesImportCfg); 1595 LDIFWriter targetWriter = new LDIFWriter(exportConfig)) 1596 { 1597 // Apply the changes and make sure there were no errors. 1598 successful = LDIFModify.modifyLDIF(sourceReader, changesReader, targetWriter, errorList); 1599 } 1600 1601 if (!successful) 1602 { 1603 for (LocalizableMessage s : errorList) 1604 { 1605 logger.error(ERR_CONFIG_ERROR_APPLYING_STARTUP_CHANGE, s); 1606 } 1607 throw new LDIFException(ERR_CONFIG_UNABLE_TO_APPLY_CHANGES_FILE.get(Utils.joinAsString("; ", errorList))); 1608 } 1609 1610 // Move the current config file out of the way and replace it with the updated version. 1611 File oldSource = new File(sourceFile.getAbsolutePath() + ".prechanges"); 1612 if (oldSource.exists()) 1613 { 1614 oldSource.delete(); 1615 } 1616 sourceFile.renameTo(oldSource); 1617 new File(tempFile).renameTo(sourceFile); 1618 1619 // Move the changes file out of the way so it doesn't get applied again. 1620 File newChanges = new File(changesFile.getAbsolutePath() + ".applied"); 1621 if (newChanges.exists()) 1622 { 1623 newChanges.delete(); 1624 } 1625 changesFile.renameTo(newChanges); 1626 } 1627 1628 private void applyConfigChangesIfNeeded(File configFileToUse) throws InitializationException 1629 { 1630 // See if there is a config changes file. If there is, then try to apply 1631 // the changes contained in it. 1632 File changesFile = new File(configFileToUse.getParent(), CONFIG_CHANGES_NAME); 1633 try 1634 { 1635 if (changesFile.exists()) 1636 { 1637 applyChangesFile(configFileToUse, changesFile); 1638 if (maintainConfigArchive) 1639 { 1640 configurationDigest = calculateConfigDigest(); 1641 writeConfigArchive(); 1642 } 1643 } 1644 } 1645 catch (Exception e) 1646 { 1647 logger.traceException(e); 1648 1649 LocalizableMessage message = ERR_CONFIG_UNABLE_TO_APPLY_STARTUP_CHANGES.get(changesFile.getAbsolutePath(), e); 1650 throw new InitializationException(message, e); 1651 } 1652 } 1653 1654 /** 1655 * Returns the LDIF reader on configuration entries. 1656 * <p> 1657 * It is the responsibility of the caller to ensure that reader is closed after usage. 1658 * 1659 * @param configFile 1660 * LDIF file containing the configuration entries. 1661 * @param schema 1662 * Schema to validate entries when reading the config file. 1663 * @return the LDIF reader 1664 * @throws InitializationException 1665 * If an error occurs. 1666 */ 1667 private EntryReader getLDIFReader(final File configFile, final Schema schema) throws InitializationException 1668 { 1669 try 1670 { 1671 LDIFEntryReader reader = new LDIFEntryReader(new FileReader(configFile)); 1672 reader.setSchema(schema); 1673 return reader; 1674 } 1675 catch (Exception e) 1676 { 1677 throw new InitializationException( 1678 ERR_CONFIG_FILE_CANNOT_OPEN_FOR_READ.get(configFile.getAbsolutePath(), e.getLocalizedMessage()), e); 1679 } 1680 } 1681 1682 /** 1683 * Returns the entry listeners attached to the provided DN. 1684 * <p> 1685 * If no listener exist for the provided DN, then a new set of empty listeners is created and 1686 * returned. 1687 * 1688 * @param dn 1689 * DN of a configuration entry. 1690 * @return the listeners attached to the corresponding configuration entry. 1691 */ 1692 private EntryListeners getEntryListeners(final DN dn) 1693 { 1694 EntryListeners entryListeners = listeners.get(dn); 1695 if (entryListeners == null) 1696 { 1697 entryListeners = new EntryListeners(); 1698 final EntryListeners previousListeners = listeners.putIfAbsent(dn, entryListeners); 1699 if (previousListeners != null) 1700 { 1701 entryListeners = previousListeners; 1702 } 1703 } 1704 return entryListeners; 1705 } 1706 1707 /** 1708 * Returns the parent DN of the configuration entry corresponding to the provided DN. 1709 * 1710 * @param entryDN 1711 * DN of entry to retrieve the parent from. 1712 * @return the parent DN 1713 * @throws DirectoryException 1714 * If entry has no parent or parent entry does not exist. 1715 */ 1716 private DN retrieveParentDNForAdd(final DN entryDN) throws DirectoryException 1717 { 1718 final DN parentDN = entryDN.parent(); 1719 if (parentDN == null) 1720 { 1721 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_CONFIG_FILE_ADD_NO_PARENT_DN.get(entryDN)); 1722 } 1723 if (!backend.contains(parentDN)) 1724 { 1725 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_CONFIG_FILE_ADD_NO_PARENT.get(entryDN, parentDN), 1726 getMatchedDN(parentDN), null); 1727 } 1728 return parentDN; 1729 } 1730 1731 /** 1732 * Returns the parent DN of the configuration entry corresponding to the provided DN. 1733 * 1734 * @param entryDN 1735 * DN of entry to retrieve the parent from. 1736 * @return the parent DN 1737 * @throws DirectoryException 1738 * If entry has no parent or parent entry does not exist. 1739 */ 1740 private DN retrieveParentDNForDelete(final DN entryDN) throws DirectoryException 1741 { 1742 final DN parentDN = entryDN.parent(); 1743 if (parentDN == null) 1744 { 1745 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_CONFIG_FILE_DELETE_NO_PARENT_DN.get(entryDN)); 1746 } 1747 if (!backend.contains(parentDN)) 1748 { 1749 throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_CONFIG_FILE_DELETE_NO_PARENT.get(entryDN), 1750 getMatchedDN(parentDN), null); 1751 } 1752 return parentDN; 1753 } 1754 1755 /** Returns the matched DN that is available in the configuration for the provided DN. */ 1756 private DN getMatchedDN(final DN dn) 1757 { 1758 DN matchedDN = null; 1759 DN parentDN = dn.parent(); 1760 while (parentDN != null) 1761 { 1762 if (backend.contains(parentDN)) 1763 { 1764 matchedDN = parentDN; 1765 break; 1766 } 1767 parentDN = parentDN.parent(); 1768 } 1769 return matchedDN; 1770 } 1771 1772 /** 1773 * Examines the provided result and logs a message if appropriate. 1774 * <p> 1775 * <ul> 1776 * <li>If the result code is anything other than {@code SUCCESS}, then it will log an error message.</li> 1777 * <li>If the operation was successful but admin action is required, then it will log a warning message.</li> 1778 * <li>If no action is required but messages were generated, then it will log an informational message.</li> 1779 * </ul> 1780 * 1781 * @param result 1782 * The config change result object that 1783 * @param entryDN 1784 * The DN of the entry that was added, deleted, or modified. 1785 * @param className 1786 * The name of the class for the object that generated the provided result. 1787 * @param methodName 1788 * The name of the method that generated the provided result. 1789 */ 1790 private void handleConfigChangeResult(ConfigChangeResult result, DN entryDN, String className, String methodName) 1791 { 1792 if (result == null) 1793 { 1794 logger.error(ERR_CONFIG_CHANGE_NO_RESULT, className, methodName, entryDN); 1795 return; 1796 } 1797 1798 final ResultCode resultCode = result.getResultCode(); 1799 final boolean adminActionRequired = result.adminActionRequired(); 1800 final String messages = Utils.joinAsString(" ", result.getMessages()); 1801 1802 if (resultCode != ResultCode.SUCCESS) 1803 { 1804 logger.error(ERR_CONFIG_CHANGE_RESULT_ERROR, className, methodName, entryDN, resultCode, adminActionRequired, 1805 messages); 1806 } 1807 else if (adminActionRequired) 1808 { 1809 logger.warn(WARN_CONFIG_CHANGE_RESULT_ACTION_REQUIRED, className, methodName, entryDN, messages); 1810 } 1811 else if (!messages.isEmpty()) 1812 { 1813 logger.debug(INFO_CONFIG_CHANGE_RESULT_MESSAGES, className, methodName, entryDN, messages); 1814 } 1815 } 1816}