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}