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