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