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 2006-2009 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017package org.opends.server.extensions;
018
019import static org.forgerock.util.Reject.*;
020import static org.opends.messages.ConfigMessages.*;
021import static org.opends.server.config.ConfigConstants.*;
022import static org.opends.server.extensions.ExtensionsConstants.*;
023import static org.opends.server.util.ServerConstants.*;
024import static org.opends.server.util.StaticUtils.*;
025
026import java.io.File;
027import java.io.FileInputStream;
028import java.io.FileOutputStream;
029import java.io.IOException;
030import java.io.InputStream;
031import java.nio.file.Path;
032import java.security.MessageDigest;
033import java.util.*;
034import java.util.concurrent.ConcurrentHashMap;
035import java.util.concurrent.ConcurrentMap;
036import java.util.zip.GZIPInputStream;
037import java.util.zip.GZIPOutputStream;
038
039import org.forgerock.i18n.LocalizableMessage;
040import org.forgerock.i18n.LocalizableMessageBuilder;
041import org.forgerock.i18n.LocalizableMessageDescriptor.Arg1;
042import org.forgerock.i18n.slf4j.LocalizedLogger;
043import org.forgerock.opendj.config.server.ConfigChangeResult;
044import org.forgerock.opendj.config.server.ConfigException;
045import org.forgerock.opendj.ldap.ByteString;
046import org.forgerock.opendj.ldap.ConditionResult;
047import org.forgerock.opendj.ldap.DN;
048import org.forgerock.opendj.ldap.ResultCode;
049import org.forgerock.opendj.ldap.SearchScope;
050import org.forgerock.util.Utils;
051import org.opends.server.admin.std.server.ConfigFileHandlerBackendCfg;
052import org.opends.server.api.AlertGenerator;
053import org.opends.server.api.Backupable;
054import org.opends.server.api.ClientConnection;
055import org.opends.server.api.ConfigAddListener;
056import org.opends.server.api.ConfigChangeListener;
057import org.opends.server.api.ConfigDeleteListener;
058import org.opends.server.api.ConfigHandler;
059import org.opends.server.config.ConfigEntry;
060import org.opends.server.core.AddOperation;
061import org.opends.server.core.DeleteOperation;
062import org.opends.server.core.DirectoryServer;
063import org.opends.server.core.ModifyDNOperation;
064import org.opends.server.core.ModifyOperation;
065import org.opends.server.core.SearchOperation;
066import org.opends.server.core.ServerContext;
067import org.opends.server.schema.GeneralizedTimeSyntax;
068import org.opends.server.tools.LDIFModify;
069import org.forgerock.opendj.ldap.schema.AttributeType;
070import org.opends.server.types.*;
071import org.opends.server.util.BackupManager;
072import org.opends.server.util.LDIFException;
073import org.opends.server.util.LDIFReader;
074import org.opends.server.util.LDIFWriter;
075import org.opends.server.util.StaticUtils;
076import org.opends.server.util.TimeThread;
077import org.opends.server.types.FilePermission;
078
079/**
080 * This class defines a simple configuration handler for the Directory Server
081 * that will read the server configuration from an LDIF file.
082 */
083public class ConfigFileHandler
084       extends ConfigHandler<ConfigFileHandlerBackendCfg>
085       implements AlertGenerator, Backupable
086{
087  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
088
089  /** The fully-qualified name of this class. */
090  private static final String CLASS_NAME =
091       "org.opends.server.extensions.ConfigFileHandler";
092
093  /**
094   * The privilege array containing both the CONFIG_READ and CONFIG_WRITE
095   * privileges.
096   */
097  private static final Privilege[] CONFIG_READ_AND_WRITE =
098  {
099    Privilege.CONFIG_READ,
100    Privilege.CONFIG_WRITE
101  };
102
103
104
105  /** Indicates whether to maintain a configuration archive. */
106  private boolean maintainConfigArchive;
107
108  /** Indicates whether to start using the last known good configuration. */
109  private boolean useLastKnownGoodConfig;
110
111  /**
112   * A SHA-1 digest of the last known configuration. This should only be
113   * incorrect if the server configuration file has been manually edited with
114   * the server online, which is a bad thing.
115   */
116  private byte[] configurationDigest;
117
118  /**
119   * The mapping that holds all of the configuration entries that have been read
120   * from the LDIF file.
121   */
122  private ConcurrentMap<DN,ConfigEntry> configEntries;
123
124  /** The reference to the configuration root entry. */
125  private ConfigEntry configRootEntry;
126
127  /** The set of base DNs for this config handler backend. */
128  private DN[] baseDNs;
129
130  /** The maximum config archive size to maintain. */
131  private int maxConfigArchiveSize;
132
133  /**
134   * The write lock used to ensure that only one thread can apply a
135   * configuration update at any given time.
136   */
137  private final Object configLock = new Object();
138
139  /** The path to the configuration file. */
140  private String configFile;
141
142  /** The install root directory for the Directory Server. */
143  private String serverRoot;
144
145  /** The instance root directory for the Directory Server. */
146  private String instanceRoot;
147
148  /**
149   * Creates a new instance of this config file handler.  No initialization
150   * should be performed here, as all of that work should be done in the
151   * <CODE>initializeConfigHandler</CODE> method.
152   */
153  public ConfigFileHandler()
154  {
155    super();
156  }
157
158  /** {@inheritDoc} */
159  @Override
160  public void initializeConfigHandler(String configFile, boolean checkSchema)
161         throws InitializationException
162  {
163    // Determine whether we should try to start using the last known good
164    // configuration.  If so, then only do so if such a file exists.  If it
165    // doesn't exist, then fall back on the active configuration file.
166    this.configFile = configFile;
167    DirectoryEnvironmentConfig envConfig = DirectoryServer.getEnvironmentConfig();
168    useLastKnownGoodConfig = envConfig.useLastKnownGoodConfiguration();
169    File f;
170    if (useLastKnownGoodConfig)
171    {
172      f = new File(configFile + ".startok");
173      if (! f.exists())
174      {
175        logger.warn(WARN_CONFIG_FILE_NO_STARTOK_FILE, f.getAbsolutePath(), configFile);
176        useLastKnownGoodConfig = false;
177        f = new File(configFile);
178      }
179      else
180      {
181        logger.info(NOTE_CONFIG_FILE_USING_STARTOK_FILE, f.getAbsolutePath(), configFile);
182      }
183    }
184    else
185    {
186      f = new File(configFile);
187    }
188
189    try
190    {
191      if (! f.exists())
192      {
193        LocalizableMessage message = ERR_CONFIG_FILE_DOES_NOT_EXIST.get(
194                               f.getAbsolutePath());
195        throw new InitializationException(message);
196      }
197    }
198    catch (InitializationException ie)
199    {
200      logger.traceException(ie);
201
202      throw ie;
203    }
204    catch (Exception e)
205    {
206      logger.traceException(e);
207
208      LocalizableMessage message = ERR_CONFIG_FILE_CANNOT_VERIFY_EXISTENCE.get(f.getAbsolutePath(), e);
209      throw new InitializationException(message);
210    }
211
212
213    // Check to see if a configuration archive exists.  If not, then create one.
214    // If so, then check whether the current configuration matches the last
215    // configuration in the archive.  If it doesn't, then archive it.
216    maintainConfigArchive = envConfig.maintainConfigArchive();
217    maxConfigArchiveSize  = envConfig.getMaxConfigArchiveSize();
218    if (maintainConfigArchive && !useLastKnownGoodConfig)
219    {
220      try
221      {
222        configurationDigest = calculateConfigDigest();
223      }
224      catch (DirectoryException de)
225      {
226        throw new InitializationException(de.getMessageObject(), de.getCause());
227      }
228
229      File archiveDirectory = new File(f.getParent(), CONFIG_ARCHIVE_DIR_NAME);
230      if (archiveDirectory.exists())
231      {
232        try
233        {
234          byte[] lastDigest = getLastConfigDigest(archiveDirectory);
235          if (! Arrays.equals(configurationDigest, lastDigest))
236          {
237            writeConfigArchive();
238          }
239        } catch (Exception e) {}
240      }
241      else
242      {
243        writeConfigArchive();
244      }
245    }
246
247
248
249    // Fixme -- Should we add a hash or signature check here?
250
251
252    // See if there is a config changes file.  If there is, then try to apply
253    // the changes contained in it.
254    File changesFile = new File(f.getParent(), CONFIG_CHANGES_NAME);
255    try
256    {
257      if (changesFile.exists())
258      {
259        applyChangesFile(f, changesFile);
260        if (maintainConfigArchive)
261        {
262          configurationDigest = calculateConfigDigest();
263          writeConfigArchive();
264        }
265      }
266    }
267    catch (Exception e)
268    {
269      logger.traceException(e);
270
271      LocalizableMessage message = ERR_CONFIG_UNABLE_TO_APPLY_STARTUP_CHANGES.get(
272          changesFile.getAbsolutePath(), e);
273      throw new InitializationException(message, e);
274    }
275
276
277    // We will use the LDIF reader to read the configuration file.  Create an
278    // LDIF import configuration to do this and then get the reader.
279    LDIFReader reader;
280    try
281    {
282      LDIFImportConfig importConfig = new LDIFImportConfig(f.getAbsolutePath());
283
284      // FIXME -- Should we support encryption or compression for the config?
285
286      reader = new LDIFReader(importConfig);
287    }
288    catch (Exception e)
289    {
290      logger.traceException(e);
291
292      LocalizableMessage message = ERR_CONFIG_FILE_CANNOT_OPEN_FOR_READ.get(
293                             f.getAbsolutePath(), e);
294      throw new InitializationException(message, e);
295    }
296
297
298    // Read the first entry from the configuration file.
299    Entry entry;
300    try
301    {
302      entry = reader.readEntry(checkSchema);
303    }
304    catch (LDIFException le)
305    {
306      logger.traceException(le);
307
308      close(reader);
309
310      LocalizableMessage message = ERR_CONFIG_FILE_INVALID_LDIF_ENTRY.get(
311          le.getLineNumber(), f.getAbsolutePath(), le);
312      throw new InitializationException(message, le);
313    }
314    catch (Exception e)
315    {
316      logger.traceException(e);
317
318      close(reader);
319
320      LocalizableMessage message =
321          ERR_CONFIG_FILE_READ_ERROR.get(f.getAbsolutePath(), e);
322      throw new InitializationException(message, e);
323    }
324
325
326    // Make sure that the provide LDIF file is not empty.
327    if (entry == null)
328    {
329      close(reader);
330
331      LocalizableMessage message = ERR_CONFIG_FILE_EMPTY.get(f.getAbsolutePath());
332      throw new InitializationException(message);
333    }
334
335
336    // Make sure that the DN of this entry is equal to the config root DN.
337    try
338    {
339      DN configRootDN = DN.valueOf(DN_CONFIG_ROOT);
340      if (! entry.getName().equals(configRootDN))
341      {
342        throw new InitializationException(ERR_CONFIG_FILE_INVALID_BASE_DN.get(
343            f.getAbsolutePath(), entry.getName(), DN_CONFIG_ROOT));
344      }
345    }
346    catch (InitializationException ie)
347    {
348      logger.traceException(ie);
349
350      close(reader);
351      throw ie;
352    }
353    catch (Exception e)
354    {
355      logger.traceException(e);
356
357      close(reader);
358
359      // This should not happen, so we can use a generic error here.
360      LocalizableMessage message = ERR_CONFIG_FILE_GENERIC_ERROR.get(f.getAbsolutePath(), e);
361      throw new InitializationException(message, e);
362    }
363
364
365    // Convert the entry to a configuration entry and put it in the config
366    // hash.
367    configEntries   = new ConcurrentHashMap<>();
368    configRootEntry = new ConfigEntry(entry, null);
369    configEntries.put(entry.getName(), configRootEntry);
370
371
372    // Iterate through the rest of the configuration file and process the
373    // remaining entries.
374    while (true)
375    {
376      // Read the next entry from the configuration.
377      try
378      {
379        entry = reader.readEntry(checkSchema);
380      }
381      catch (LDIFException le)
382      {
383        logger.traceException(le);
384
385        close(reader);
386
387        LocalizableMessage message = ERR_CONFIG_FILE_INVALID_LDIF_ENTRY.get(
388                               le.getLineNumber(), f.getAbsolutePath(), le);
389        throw new InitializationException(message, le);
390      }
391      catch (Exception e)
392      {
393        logger.traceException(e);
394
395        close(reader);
396
397        LocalizableMessage message = ERR_CONFIG_FILE_READ_ERROR.get(f.getAbsolutePath(), e);
398        throw new InitializationException(message, e);
399      }
400
401
402      // If the entry is null, then we have reached the end of the configuration
403      // file.
404      if (entry == null)
405      {
406        close(reader);
407        break;
408      }
409
410
411      // Make sure that the DN of the entry read doesn't already exist.
412      DN entryDN = entry.getName();
413      if (configEntries.containsKey(entryDN))
414      {
415        close(reader);
416
417        throw new InitializationException(ERR_CONFIG_FILE_DUPLICATE_ENTRY.get(
418            entryDN, reader.getLastEntryLineNumber(), f.getAbsolutePath()));
419      }
420
421
422      // Make sure that the parent DN of the entry read does exist.
423      DN parentDN = entryDN.parent();
424      if (parentDN == null)
425      {
426        close(reader);
427
428        throw new InitializationException(ERR_CONFIG_FILE_UNKNOWN_PARENT.get(
429            entryDN, reader.getLastEntryLineNumber(), f.getAbsolutePath()));
430      }
431
432      ConfigEntry parentEntry = configEntries.get(parentDN);
433      if (parentEntry == null)
434      {
435        close(reader);
436
437        throw new InitializationException(ERR_CONFIG_FILE_NO_PARENT.get(
438            entryDN, reader.getLastEntryLineNumber(), f.getAbsolutePath(), parentDN));
439      }
440
441
442      // Create the new configuration entry, add it as a child of the provided
443      // parent entry, and put it into the entry has.
444      try
445      {
446        ConfigEntry configEntry = new ConfigEntry(entry, parentEntry);
447        parentEntry.addChild(configEntry);
448        configEntries.put(entryDN, configEntry);
449      }
450      catch (Exception e)
451      {
452        // This should not happen.
453        logger.traceException(e);
454
455        close(reader);
456
457        LocalizableMessage message = ERR_CONFIG_FILE_GENERIC_ERROR.get(f.getAbsolutePath(), e);
458        throw new InitializationException(message, e);
459      }
460    }
461
462
463    // Get the server root
464    File rootFile = envConfig.getServerRoot();
465    if (rootFile == null)
466    {
467      throw new InitializationException(ERR_CONFIG_CANNOT_DETERMINE_SERVER_ROOT.get(
468          ENV_VAR_INSTALL_ROOT));
469    }
470    serverRoot = rootFile.getAbsolutePath();
471
472    // Get the server instance root
473    File instanceFile = envConfig.getInstanceRoot();
474    instanceRoot = instanceFile.getAbsolutePath();
475
476    // Register with the Directory Server as an alert generator.
477    DirectoryServer.registerAlertGenerator(this);
478
479    // Register with the Directory Server as the backend that should be used
480    // when accessing the configuration.
481    baseDNs = new DN[] { configRootEntry.getDN() };
482
483    try
484    {
485      // Set a backend ID for the config backend. Try to avoid potential
486      // conflict with user backend identifiers.
487      setBackendID("__config.ldif__");
488
489      DirectoryServer.registerBaseDN(configRootEntry.getDN(), this, true);
490    }
491    catch (Exception e)
492    {
493      logger.traceException(e);
494
495      LocalizableMessage message = ERR_CONFIG_CANNOT_REGISTER_AS_PRIVATE_SUFFIX.get(
496          configRootEntry.getDN(), getExceptionMessage(e));
497      throw new InitializationException(message, e);
498    }
499  }
500
501
502
503  /**
504   * Calculates a SHA-1 digest of the current configuration file.
505   *
506   * @return  The calculated configuration digest.
507   *
508   * @throws  DirectoryException  If a problem occurs while calculating the
509   *                              digest.
510   */
511  private byte[] calculateConfigDigest()
512          throws DirectoryException
513  {
514    InputStream inputStream = null;
515    try
516    {
517      MessageDigest sha1Digest =
518           MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1);
519      inputStream = new FileInputStream(configFile);
520      byte[] buffer = new byte[8192];
521      while (true)
522      {
523        int bytesRead = inputStream.read(buffer);
524        if (bytesRead < 0)
525        {
526          break;
527        }
528
529        sha1Digest.update(buffer, 0, bytesRead);
530      }
531      return sha1Digest.digest();
532    }
533    catch (Exception e)
534    {
535      LocalizableMessage message = ERR_CONFIG_CANNOT_CALCULATE_DIGEST.get(
536          configFile, stackTraceToSingleLineString(e));
537      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
538                                   message, e);
539    }
540    finally
541    {
542      StaticUtils.close(inputStream);
543    }
544  }
545
546
547
548  /**
549   * Looks at the existing archive directory, finds the latest archive file,
550   * and calculates a SHA-1 digest of that file.
551   *
552   * @return  The calculated digest of the most recent archived configuration
553   *          file.
554   *
555   * @throws  DirectoryException  If a problem occurs while calculating the
556   *                              digest.
557   */
558  private byte[] getLastConfigDigest(File archiveDirectory)
559          throws DirectoryException
560  {
561    int    latestCounter   = 0;
562    long   latestTimestamp = -1;
563    String latestFileName  = null;
564    for (String name : archiveDirectory.list())
565    {
566      if (! name.startsWith("config-"))
567      {
568        continue;
569      }
570
571      int dotPos = name.indexOf('.', 7);
572      if (dotPos < 0)
573      {
574        continue;
575      }
576
577      int dashPos = name.indexOf('-', 7);
578      if (dashPos < 0)
579      {
580        try
581        {
582          ByteString ts = ByteString.valueOfUtf8(name.substring(7, dotPos));
583          long timestamp = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(ts);
584          if (timestamp > latestTimestamp)
585          {
586            latestFileName  = name;
587            latestTimestamp = timestamp;
588            latestCounter   = 0;
589            continue;
590          }
591        }
592        catch (Exception e)
593        {
594          continue;
595        }
596      }
597      else
598      {
599        try
600        {
601          ByteString ts = ByteString.valueOfUtf8(name.substring(7, dashPos));
602          long timestamp = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(ts);
603          int counter = Integer.parseInt(name.substring(dashPos+1, dotPos));
604
605          if (timestamp > latestTimestamp)
606          {
607            latestFileName  = name;
608            latestTimestamp = timestamp;
609            latestCounter   = counter;
610            continue;
611          }
612          else if (timestamp == latestTimestamp && counter > latestCounter)
613          {
614            latestFileName  = name;
615            latestTimestamp = timestamp;
616            latestCounter   = counter;
617            continue;
618          }
619        }
620        catch (Exception e)
621        {
622          continue;
623        }
624      }
625    }
626
627    if (latestFileName == null)
628    {
629      return null;
630    }
631    File latestFile = new File(archiveDirectory, latestFileName);
632
633    try
634    {
635      MessageDigest sha1Digest =
636           MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_SHA_1);
637      GZIPInputStream inputStream =
638           new GZIPInputStream(new FileInputStream(latestFile));
639      byte[] buffer = new byte[8192];
640      while (true)
641      {
642        int bytesRead = inputStream.read(buffer);
643        if (bytesRead < 0)
644        {
645          break;
646        }
647
648        sha1Digest.update(buffer, 0, bytesRead);
649      }
650
651      return sha1Digest.digest();
652    }
653    catch (Exception e)
654    {
655      LocalizableMessage message = ERR_CONFIG_CANNOT_CALCULATE_DIGEST.get(
656          latestFile.getAbsolutePath(), stackTraceToSingleLineString(e));
657      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
658                                   message, e);
659    }
660  }
661
662
663
664  /**
665   * Applies the updates in the provided changes file to the content in the
666   * specified source file.  The result will be written to a temporary file, the
667   * current source file will be moved out of place, and then the updated file
668   * will be moved into the place of the original file.  The changes file will
669   * also be renamed so it won't be applied again.
670   * <BR><BR>
671   * If any problems are encountered, then the config initialization process
672   * will be aborted.
673   *
674   * @param  sourceFile   The LDIF file containing the source data.
675   * @param  changesFile  The LDIF file containing the changes to apply.
676   *
677   * @throws  IOException  If a problem occurs while performing disk I/O.
678   *
679   * @throws  LDIFException  If a problem occurs while trying to interpret the
680   *                         data.
681   */
682  private void applyChangesFile(File sourceFile, File changesFile)
683          throws IOException, LDIFException
684  {
685    // Create the appropriate LDIF readers and writer.
686    LDIFImportConfig importConfig =
687         new LDIFImportConfig(sourceFile.getAbsolutePath());
688    importConfig.setValidateSchema(false);
689    LDIFReader sourceReader = new LDIFReader(importConfig);
690
691    importConfig = new LDIFImportConfig(changesFile.getAbsolutePath());
692    importConfig.setValidateSchema(false);
693    LDIFReader changesReader = new LDIFReader(importConfig);
694
695    String tempFile = changesFile.getAbsolutePath() + ".tmp";
696    LDIFExportConfig exportConfig =
697         new LDIFExportConfig(tempFile, ExistingFileBehavior.OVERWRITE);
698    LDIFWriter targetWriter = new LDIFWriter(exportConfig);
699
700
701    // Apply the changes and make sure there were no errors.
702    List<LocalizableMessage> errorList = new LinkedList<>();
703    boolean successful = LDIFModify.modifyLDIF(sourceReader, changesReader,
704                                               targetWriter, errorList);
705
706    StaticUtils.close(sourceReader, changesReader, targetWriter);
707
708    if (! successful)
709    {
710      // FIXME -- Log each error message and throw an exception.
711      for (LocalizableMessage s : errorList)
712      {
713        logger.error(ERR_CONFIG_ERROR_APPLYING_STARTUP_CHANGE, s);
714      }
715
716      LocalizableMessage message = ERR_CONFIG_UNABLE_TO_APPLY_CHANGES_FILE.get();
717      throw new LDIFException(message);
718    }
719
720
721    // Move the current config file out of the way and replace it with the
722    // updated version.
723    File oldSource = new File(sourceFile.getAbsolutePath() + ".prechanges");
724    if (oldSource.exists())
725    {
726      oldSource.delete();
727    }
728    sourceFile.renameTo(oldSource);
729    new File(tempFile).renameTo(sourceFile);
730
731    // Move the changes file out of the way so it doesn't get applied again.
732    File newChanges = new File(changesFile.getAbsolutePath() + ".applied");
733    if (newChanges.exists())
734    {
735      newChanges.delete();
736    }
737    changesFile.renameTo(newChanges);
738  }
739
740  /** {@inheritDoc} */
741  @Override
742  public void finalizeConfigHandler()
743  {
744    finalizeBackend();
745    try
746    {
747      DirectoryServer.deregisterBaseDN(configRootEntry.getDN());
748    }
749    catch (Exception e)
750    {
751      logger.traceException(e);
752    }
753  }
754
755  /** {@inheritDoc} */
756  @Override
757  public ConfigEntry getConfigRootEntry()
758         throws ConfigException
759  {
760    return configRootEntry;
761  }
762
763  /** {@inheritDoc} */
764  @Override
765  public ConfigEntry getConfigEntry(DN entryDN)
766         throws ConfigException
767  {
768    return configEntries.get(entryDN);
769  }
770
771  /** {@inheritDoc} */
772  @Override
773  public String getServerRoot()
774  {
775    return serverRoot;
776  }
777
778  /** {@inheritDoc} */
779  @Override
780  public String getInstanceRoot()
781  {
782    return instanceRoot;
783  }
784
785  /** {@inheritDoc} */
786  @Override
787  public void configureBackend(ConfigFileHandlerBackendCfg cfg, ServerContext serverContext)
788         throws ConfigException
789  {
790    // No action is required.
791  }
792
793  /** {@inheritDoc} */
794  @Override
795  public void openBackend() throws ConfigException, InitializationException
796  {
797    // No action is required, since all initialization was performed in the
798    // initializeConfigHandler method.
799  }
800
801  /** {@inheritDoc} */
802  @Override
803  public DN[] getBaseDNs()
804  {
805    return baseDNs;
806  }
807
808  /** {@inheritDoc} */
809  @Override
810  public long getEntryCount()
811  {
812    return configEntries.size();
813  }
814
815  /** {@inheritDoc} */
816  @Override
817  public boolean isIndexed(AttributeType attributeType, IndexType indexType)
818  {
819    // All searches in this backend will always be considered indexed.
820    return true;
821  }
822
823  /** {@inheritDoc} */
824  @Override
825  public ConditionResult hasSubordinates(DN entryDN)
826         throws DirectoryException
827  {
828    ConfigEntry baseEntry = configEntries.get(entryDN);
829    if (baseEntry != null)
830    {
831      return ConditionResult.valueOf(baseEntry.hasChildren());
832    }
833    return ConditionResult.UNDEFINED;
834  }
835
836  /** {@inheritDoc} */
837  @Override
838  public long getNumberOfEntriesInBaseDN(DN baseDN) throws DirectoryException
839  {
840    checkNotNull(baseDN, "baseDN must not be null");
841    final ConfigEntry baseEntry = configEntries.get(baseDN);
842    if (baseEntry == null)
843    {
844      return -1;
845    }
846
847    long count = 1;
848    for (ConfigEntry child : baseEntry.getChildren().values())
849    {
850      count += getNumberOfEntriesInBaseDN(child.getDN());
851      count++;
852    }
853    return count;
854  }
855
856  /** {@inheritDoc} */
857  @Override
858  public long getNumberOfChildren(DN parentDN) throws DirectoryException
859  {
860    checkNotNull(parentDN, "parentDN must not be null");
861    final ConfigEntry baseEntry = configEntries.get(parentDN);
862    return baseEntry != null ? baseEntry.getChildren().size() : -1;
863  }
864
865  /** {@inheritDoc} */
866  @Override
867  public Entry getEntry(DN entryDN)
868         throws DirectoryException
869  {
870    ConfigEntry configEntry = configEntries.get(entryDN);
871    if (configEntry == null)
872    {
873      return null;
874    }
875
876    return configEntry.getEntry().duplicate(true);
877  }
878
879  /** {@inheritDoc} */
880  @Override
881  public boolean entryExists(DN entryDN)
882         throws DirectoryException
883  {
884    return configEntries.containsKey(entryDN);
885  }
886
887  /** {@inheritDoc} */
888  @Override
889  public void addEntry(Entry entry, AddOperation addOperation)
890         throws DirectoryException
891  {
892    Entry e = entry.duplicate(false);
893
894    // If there is an add operation, then make sure that the associated user has
895    // both the CONFIG_READ and CONFIG_WRITE privileges.
896    if (addOperation != null)
897    {
898      ClientConnection clientConnection = addOperation.getClientConnection();
899      if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE,
900                                             addOperation))
901      {
902        LocalizableMessage message = ERR_CONFIG_FILE_ADD_INSUFFICIENT_PRIVILEGES.get();
903        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
904                                     message);
905      }
906    }
907
908
909    // Grab the config lock to ensure that only one config update may be in
910    // progress at any given time.
911    synchronized (configLock)
912    {
913      // Make sure that the target DN does not already exist.  If it does, then
914      // fail.
915      DN entryDN = e.getName();
916      if (configEntries.containsKey(entryDN))
917      {
918        LocalizableMessage message = ERR_CONFIG_FILE_ADD_ALREADY_EXISTS.get(entryDN);
919        throw new DirectoryException(ResultCode.ENTRY_ALREADY_EXISTS, message);
920      }
921
922
923      // Make sure that the entry's parent exists.  If it does not, then fail.
924      DN parentDN = entryDN.parent();
925      if (parentDN == null)
926      {
927        // The entry DN doesn't have a parent.  This is not allowed.
928        LocalizableMessage message = ERR_CONFIG_FILE_ADD_NO_PARENT_DN.get(entryDN);
929        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message);
930      }
931
932      ConfigEntry parentEntry = configEntries.get(parentDN);
933      if (parentEntry == null)
934      {
935        // The parent entry does not exist.  This is not allowed.
936        DN matchedDN = getMatchedDN(parentDN);
937        LocalizableMessage message = ERR_CONFIG_FILE_ADD_NO_PARENT.get(entryDN, parentDN);
938        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, matchedDN, null);
939      }
940
941
942      // Encapsulate the provided entry in a config entry.
943      ConfigEntry newEntry = new ConfigEntry(e, parentEntry);
944
945
946      // See if the parent entry has any add listeners.  If so, then iterate
947      // through them and make sure the new entry is acceptable.
948      List<ConfigAddListener> addListeners = parentEntry.getAddListeners();
949      LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder();
950      for (ConfigAddListener l : addListeners)
951      {
952        if (! l.configAddIsAcceptable(newEntry, unacceptableReason))
953        {
954          LocalizableMessage message = ERR_CONFIG_FILE_ADD_REJECTED_BY_LISTENER.
955              get(entryDN, parentDN, unacceptableReason);
956          throw new DirectoryException(
957                  ResultCode.UNWILLING_TO_PERFORM, message);
958
959        }
960      }
961
962
963      // At this point, we will assume that everything is OK and proceed with
964      // the add.
965      try
966      {
967        parentEntry.addChild(newEntry);
968        configEntries.put(entryDN, newEntry);
969        writeUpdatedConfig();
970      }
971      catch (org.opends.server.config.ConfigException ce)
972      {
973        logger.traceException(ce);
974
975        LocalizableMessage message = ERR_CONFIG_FILE_ADD_FAILED.get(entryDN, parentDN, getExceptionMessage(ce));
976        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message);
977      }
978
979
980      // Notify all the add listeners that the entry has been added.
981      final ConfigChangeResult aggregatedResult = new ConfigChangeResult();
982      for (ConfigAddListener l : addListeners) // This is an iterator over a COWArrayList
983      {
984        if (addListeners.contains(l))
985        { // ignore listeners that deregistered themselves
986          final ConfigChangeResult result = l.applyConfigurationAdd(newEntry);
987          aggregate(aggregatedResult, result);
988          handleConfigChangeResult(result, newEntry.getDN(), l.getClass().getName(), "applyConfigurationAdd");
989        }
990      }
991
992      throwIfUnsuccessful(aggregatedResult, ERR_CONFIG_FILE_ADD_APPLY_FAILED);
993    }
994  }
995
996  /** {@inheritDoc} */
997  @Override
998  public void deleteEntry(DN entryDN, DeleteOperation deleteOperation)
999         throws DirectoryException
1000  {
1001    // If there is a delete operation, then make sure that the associated user
1002    // has both the CONFIG_READ and CONFIG_WRITE privileges.
1003    if (deleteOperation != null)
1004    {
1005      ClientConnection clientConnection = deleteOperation.getClientConnection();
1006      if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE,
1007                                             deleteOperation))
1008      {
1009        LocalizableMessage message = ERR_CONFIG_FILE_DELETE_INSUFFICIENT_PRIVILEGES.get();
1010        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
1011                                     message);
1012      }
1013    }
1014
1015
1016    // Grab the config lock to ensure that only one config update may be in
1017    // progress at any given time.
1018    synchronized (configLock)
1019    {
1020      // Get the target entry.  If it does not exist, then fail.
1021      ConfigEntry entry = configEntries.get(entryDN);
1022      if (entry == null)
1023      {
1024        DN matchedDN = getMatchedDNForDescendantOfConfig(entryDN);
1025        LocalizableMessage message = ERR_CONFIG_FILE_DELETE_NO_SUCH_ENTRY.get(entryDN);
1026        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, matchedDN, null);
1027      }
1028
1029
1030      // If the entry has children, then fail.
1031      if (entry.hasChildren())
1032      {
1033        LocalizableMessage message = ERR_CONFIG_FILE_DELETE_HAS_CHILDREN.get(entryDN);
1034        throw new DirectoryException(ResultCode.NOT_ALLOWED_ON_NONLEAF, message);
1035      }
1036
1037
1038      // Get the parent entry.  If there isn't one, then it must be the config
1039      // root, which we won't allow.
1040      ConfigEntry parentEntry = entry.getParent();
1041      if (parentEntry == null)
1042      {
1043        LocalizableMessage message = ERR_CONFIG_FILE_DELETE_NO_PARENT.get(entryDN);
1044        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
1045      }
1046
1047
1048      // Get the delete listeners from the parent and make sure that they are
1049      // all OK with the delete.
1050      List<ConfigDeleteListener> deleteListeners =
1051           parentEntry.getDeleteListeners();
1052      LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder();
1053      for (ConfigDeleteListener l : deleteListeners)
1054      {
1055        if (! l.configDeleteIsAcceptable(entry, unacceptableReason))
1056        {
1057          LocalizableMessage message = ERR_CONFIG_FILE_DELETE_REJECTED.
1058              get(entryDN, parentEntry.getDN(), unacceptableReason);
1059          throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM,
1060                  message);
1061        }
1062      }
1063
1064
1065      // At this point, we will assume that everything is OK and proceed with
1066      // the delete.
1067      try
1068      {
1069        parentEntry.removeChild(entryDN);
1070        configEntries.remove(entryDN);
1071        writeUpdatedConfig();
1072      }
1073      catch (org.opends.server.config.ConfigException ce)
1074      {
1075        logger.traceException(ce);
1076
1077        LocalizableMessage message = ERR_CONFIG_FILE_DELETE_FAILED.
1078            get(entryDN, parentEntry.getDN(), getExceptionMessage(ce));
1079        throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message);
1080      }
1081
1082
1083      // Notify all the delete listeners that the entry has been removed.
1084      final ConfigChangeResult aggregatedResult = new ConfigChangeResult();
1085      for (ConfigDeleteListener l : deleteListeners) // This is an iterator over a COWArrayList
1086      {
1087        if (deleteListeners.contains(l))
1088        { // ignore listeners that deregistered themselves
1089          final ConfigChangeResult result = l.applyConfigurationDelete(entry);
1090          aggregate(aggregatedResult, result);
1091          handleConfigChangeResult(result, entry.getDN(), l.getClass().getName(), "applyConfigurationDelete");
1092        }
1093      }
1094
1095      throwIfUnsuccessful(aggregatedResult, ERR_CONFIG_FILE_DELETE_APPLY_FAILED);
1096    }
1097  }
1098
1099  /** {@inheritDoc} */
1100  @Override
1101  public void replaceEntry(Entry oldEntry, Entry newEntry,
1102      ModifyOperation modifyOperation) throws DirectoryException
1103  {
1104    Entry e = newEntry.duplicate(false);
1105
1106    // If there is a modify operation, then make sure that the associated user
1107    // has both the CONFIG_READ and CONFIG_WRITE privileges.  Also, if the
1108    // operation targets the set of root privileges then make sure the user has
1109    // the PRIVILEGE_CHANGE privilege.
1110    if (modifyOperation != null)
1111    {
1112      ClientConnection clientConnection = modifyOperation.getClientConnection();
1113      if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE,
1114                                             modifyOperation))
1115      {
1116        LocalizableMessage message = ERR_CONFIG_FILE_MODIFY_INSUFFICIENT_PRIVILEGES.get();
1117        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
1118                                     message);
1119      }
1120
1121      AttributeType privType =
1122           DirectoryServer.getAttributeType(ATTR_DEFAULT_ROOT_PRIVILEGE_NAME);
1123      for (Modification m : modifyOperation.getModifications())
1124      {
1125        if (m.getAttribute().getAttributeDescription().getAttributeType().equals(privType))
1126        {
1127          if (! clientConnection.hasPrivilege(Privilege.PRIVILEGE_CHANGE,
1128                                              modifyOperation))
1129          {
1130            LocalizableMessage message =
1131                ERR_CONFIG_FILE_MODIFY_PRIVS_INSUFFICIENT_PRIVILEGES.get();
1132            throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message);
1133          }
1134
1135          break;
1136        }
1137      }
1138    }
1139
1140
1141    // Grab the config lock to ensure that only one config update may be in
1142    // progress at any given time.
1143    synchronized (configLock)
1144    {
1145      // Get the DN of the target entry for future reference.
1146      DN entryDN = e.getName();
1147
1148
1149      // Get the target entry.  If it does not exist, then fail.
1150      ConfigEntry currentEntry = configEntries.get(entryDN);
1151      if (currentEntry == null)
1152      {
1153        DN matchedDN = getMatchedDNForDescendantOfConfig(entryDN);
1154        LocalizableMessage message = ERR_CONFIG_FILE_MODIFY_NO_SUCH_ENTRY.get(entryDN);
1155        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, matchedDN, null);
1156      }
1157
1158
1159      // If the structural class is different between the current entry and the
1160      // new entry, then reject the change.
1161      if (! currentEntry.getEntry().getStructuralObjectClass().equals(
1162                 newEntry.getStructuralObjectClass()))
1163      {
1164        LocalizableMessage message = ERR_CONFIG_FILE_MODIFY_STRUCTURAL_CHANGE_NOT_ALLOWED.get(entryDN);
1165        throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message);
1166      }
1167
1168
1169      // Create a new config entry to use for the validation testing.
1170      ConfigEntry newConfigEntry = new ConfigEntry(e, currentEntry.getParent());
1171
1172
1173      // See if there are any config change listeners registered for this entry.
1174      // If there are, then make sure they are all OK with the change.
1175      List<ConfigChangeListener> changeListeners =
1176           currentEntry.getChangeListeners();
1177      LocalizableMessageBuilder unacceptableReason = new LocalizableMessageBuilder();
1178      for (ConfigChangeListener l : changeListeners)
1179      {
1180        if (! l.configChangeIsAcceptable(newConfigEntry, unacceptableReason))
1181        {
1182          LocalizableMessage message = ERR_CONFIG_FILE_MODIFY_REJECTED_BY_CHANGE_LISTENER.
1183              get(entryDN, unacceptableReason);
1184          throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
1185        }
1186      }
1187
1188
1189      // At this point, it looks like the change is acceptable, so apply it.
1190      // We'll just overwrite the core entry in the current config entry so that
1191      // we keep all the registered listeners, references to the parent and
1192      // children, and other metadata.
1193      currentEntry.setEntry(e);
1194      writeUpdatedConfig();
1195
1196
1197      // Notify all the change listeners of the update.
1198      final ConfigChangeResult aggregatedResult = new ConfigChangeResult();
1199      for (ConfigChangeListener l : changeListeners) // This is an iterator over a COWArrayList
1200      {
1201        if (changeListeners.contains(l))
1202        { // ignore listeners that deregistered themselves
1203          final ConfigChangeResult result = l.applyConfigurationChange(currentEntry);
1204          aggregate(aggregatedResult, result);
1205          handleConfigChangeResult(result, currentEntry.getDN(), l.getClass().getName(), "applyConfigurationChange");
1206        }
1207      }
1208
1209      throwIfUnsuccessful(aggregatedResult, ERR_CONFIG_FILE_MODIFY_APPLY_FAILED);
1210    }
1211  }
1212
1213  private void aggregate(final ConfigChangeResult aggregatedResult, ConfigChangeResult newResult)
1214  {
1215    if (newResult.getResultCode() != ResultCode.SUCCESS)
1216    {
1217      if (aggregatedResult.getResultCode() == ResultCode.SUCCESS)
1218      {
1219        aggregatedResult.setResultCode(newResult.getResultCode());
1220      }
1221
1222      aggregatedResult.getMessages().addAll(newResult.getMessages());
1223    }
1224  }
1225
1226  private void throwIfUnsuccessful(final ConfigChangeResult aggregatedResult, Arg1<Object> errMsg)
1227      throws DirectoryException
1228  {
1229    if (aggregatedResult.getResultCode() != ResultCode.SUCCESS)
1230    {
1231      String reasons = Utils.joinAsString(".  ", aggregatedResult.getMessages());
1232      LocalizableMessage message = errMsg.get(reasons);
1233      throw new DirectoryException(aggregatedResult.getResultCode(), message);
1234    }
1235  }
1236
1237  /** {@inheritDoc} */
1238  @Override
1239  public void renameEntry(DN currentDN, Entry entry,
1240                          ModifyDNOperation modifyDNOperation)
1241         throws DirectoryException
1242  {
1243    // If there is a modify DN operation, then make sure that the associated
1244    // user has both the CONFIG_READ and CONFIG_WRITE privileges.
1245    if (modifyDNOperation != null)
1246    {
1247      ClientConnection clientConnection =
1248           modifyDNOperation.getClientConnection();
1249      if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE,
1250                                             modifyDNOperation))
1251      {
1252        LocalizableMessage message = ERR_CONFIG_FILE_MODDN_INSUFFICIENT_PRIVILEGES.get();
1253        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
1254                                     message);
1255      }
1256    }
1257
1258
1259    // Modify DN operations will not be allowed in the configuration, so this
1260    // will always throw an exception.
1261    LocalizableMessage message = ERR_CONFIG_FILE_MODDN_NOT_ALLOWED.get();
1262    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
1263  }
1264
1265  /** {@inheritDoc} */
1266  @Override
1267  public void search(SearchOperation searchOperation)
1268         throws DirectoryException
1269  {
1270    // Make sure that the associated user has the CONFIG_READ privilege.
1271    ClientConnection clientConnection = searchOperation.getClientConnection();
1272    if (! clientConnection.hasPrivilege(Privilege.CONFIG_READ, searchOperation))
1273    {
1274      LocalizableMessage message = ERR_CONFIG_FILE_SEARCH_INSUFFICIENT_PRIVILEGES.get();
1275      throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS,
1276                                   message);
1277    }
1278
1279
1280    // First, get the base DN for the search and make sure that it exists.
1281    DN          baseDN    = searchOperation.getBaseDN();
1282    ConfigEntry baseEntry = configEntries.get(baseDN);
1283    if (baseEntry == null)
1284    {
1285      DN matchedDN = getMatchedDNForDescendantOfConfig(baseDN);
1286      LocalizableMessage message = ERR_CONFIG_FILE_SEARCH_NO_SUCH_BASE.get(baseDN);
1287      throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, message, matchedDN, null);
1288    }
1289
1290
1291    // Get the scope for the search and perform the remainder of the processing
1292    // accordingly.  Also get the filter since we will need it in all cases.
1293    SearchScope  scope  = searchOperation.getScope();
1294    SearchFilter filter = searchOperation.getFilter();
1295    switch (scope.asEnum())
1296    {
1297      case BASE_OBJECT:
1298        // We are only interested in the base entry itself.  See if it matches
1299        // and if so then return the entry.
1300        Entry e = baseEntry.getEntry().duplicate(true);
1301        if (filter.matchesEntry(e))
1302        {
1303          searchOperation.returnEntry(e, null);
1304        }
1305        break;
1306
1307
1308      case SINGLE_LEVEL:
1309        // We are only interested in entries immediately below the base entry.
1310        // Iterate through them and return the ones that match the filter.
1311        for (ConfigEntry child : baseEntry.getChildren().values())
1312        {
1313          e = child.getEntry().duplicate(true);
1314          if (filter.matchesEntry(e) && !searchOperation.returnEntry(e, null))
1315          {
1316            break;
1317          }
1318        }
1319        break;
1320
1321
1322      case WHOLE_SUBTREE:
1323        // We are interested in the base entry and all its children.  Use a
1324        // recursive process to achieve this.
1325        searchSubtree(baseEntry, filter, searchOperation);
1326        break;
1327
1328
1329      case SUBORDINATES:
1330        // We are not interested in the base entry, but we want to check out all
1331        // of its children.  Use a recursive process to achieve this.
1332        for (ConfigEntry child : baseEntry.getChildren().values())
1333        {
1334          if (! searchSubtree(child, filter, searchOperation))
1335          {
1336            break;
1337          }
1338        }
1339        break;
1340
1341
1342      default:
1343        // The user provided an invalid scope.
1344        LocalizableMessage message = ERR_CONFIG_FILE_SEARCH_INVALID_SCOPE.get(scope);
1345        throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message);
1346    }
1347  }
1348
1349  private DN getMatchedDNForDescendantOfConfig(DN dn)
1350  {
1351    if (dn.isSubordinateOrEqualTo(configRootEntry.getDN()))
1352    {
1353      return getMatchedDN(dn);
1354    }
1355    return null;
1356  }
1357
1358  private DN getMatchedDN(DN dn)
1359  {
1360    DN parentDN = dn.parent();
1361    while (parentDN != null)
1362    {
1363      if (configEntries.containsKey(parentDN))
1364      {
1365        return parentDN;
1366      }
1367
1368      parentDN = parentDN.parent();
1369    }
1370    return null;
1371  }
1372
1373  /**
1374   * Performs a subtree search starting at the provided base entry, returning
1375   * all entries anywhere in that subtree that match the provided filter.
1376   *
1377   * @param  baseEntry        The base entry below which to perform the search.
1378   * @param  filter           The filter to use to identify matching entries.
1379   * @param  searchOperation  The search operation to use to return entries to
1380   *                          the client.
1381   *
1382   * @return  <CODE>true</CODE> if the search should continue, or
1383   *          <CODE>false</CODE> if it should stop for some reason (e.g., the
1384   *          time limit or size limit has been reached).
1385   *
1386   * @throws  DirectoryException  If a problem occurs during processing.
1387   */
1388  private boolean searchSubtree(ConfigEntry baseEntry, SearchFilter filter,
1389                                SearchOperation searchOperation)
1390          throws DirectoryException
1391  {
1392    Entry e = baseEntry.getEntry().duplicate(true);
1393    if (filter.matchesEntry(e) && !searchOperation.returnEntry(e, null))
1394    {
1395      return false;
1396    }
1397
1398    for (ConfigEntry child : baseEntry.getChildren().values())
1399    {
1400      if (! searchSubtree(child, filter, searchOperation))
1401      {
1402        return false;
1403      }
1404    }
1405
1406    return true;
1407  }
1408
1409  /** {@inheritDoc} */
1410  @Override
1411  public void writeUpdatedConfig()
1412         throws DirectoryException
1413  {
1414    // FIXME -- This needs support for encryption.
1415
1416
1417    // Calculate an archive for the current server configuration file and see if
1418    // it matches what we expect.  If not, then the file has been manually
1419    // edited with the server online which is a bad thing.  In that case, we'll
1420    // copy the current config off to the side before writing the new config
1421    // so that the manual changes don't get lost but also don't get applied.
1422    // Also, send an admin alert notifying administrators about the problem.
1423    if (maintainConfigArchive)
1424    {
1425      try
1426      {
1427        byte[] currentDigest = calculateConfigDigest();
1428        if (! Arrays.equals(configurationDigest, currentDigest))
1429        {
1430          File existingCfg   = new File(configFile);
1431          File newConfigFile = new File(existingCfg.getParent(),
1432                                        "config.manualedit-" +
1433                                             TimeThread.getGMTTime() + ".ldif");
1434          int counter = 2;
1435          while (newConfigFile.exists())
1436          {
1437            newConfigFile = new File(newConfigFile.getAbsolutePath() + "." +
1438                                     counter++);
1439          }
1440
1441          FileInputStream  inputStream  = new FileInputStream(existingCfg);
1442          FileOutputStream outputStream = new FileOutputStream(newConfigFile);
1443          FilePermission.setSafePermissions(newConfigFile, 0600);
1444          byte[] buffer = new byte[8192];
1445          while (true)
1446          {
1447            int bytesRead = inputStream.read(buffer);
1448            if (bytesRead < 0)
1449            {
1450              break;
1451            }
1452
1453            outputStream.write(buffer, 0, bytesRead);
1454          }
1455
1456          StaticUtils.close(inputStream, outputStream);
1457
1458          LocalizableMessage message =
1459              WARN_CONFIG_MANUAL_CHANGES_DETECTED.get(configFile, newConfigFile
1460                  .getAbsolutePath());
1461          logger.warn(message);
1462
1463          DirectoryServer.sendAlertNotification(this,
1464               ALERT_TYPE_MANUAL_CONFIG_EDIT_HANDLED, message);
1465        }
1466      }
1467      catch (Exception e)
1468      {
1469        logger.traceException(e);
1470
1471        LocalizableMessage message =
1472            ERR_CONFIG_MANUAL_CHANGES_LOST.get(configFile,
1473                stackTraceToSingleLineString(e));
1474        logger.error(message);
1475
1476        DirectoryServer.sendAlertNotification(this,
1477             ALERT_TYPE_MANUAL_CONFIG_EDIT_HANDLED, message);
1478      }
1479    }
1480
1481
1482    // Write the new configuration to a temporary file.
1483    String tempConfig = configFile + ".tmp";
1484    try
1485    {
1486      LDIFExportConfig exportConfig =
1487           new LDIFExportConfig(tempConfig, ExistingFileBehavior.OVERWRITE);
1488
1489      // FIXME -- Add all the appropriate configuration options.
1490      writeLDIF(exportConfig);
1491    }
1492    catch (Exception e)
1493    {
1494      logger.traceException(e);
1495
1496      LocalizableMessage message =
1497          ERR_CONFIG_FILE_WRITE_CANNOT_EXPORT_NEW_CONFIG.get(tempConfig, stackTraceToSingleLineString(e));
1498      logger.error(message);
1499
1500      DirectoryServer.sendAlertNotification(this,
1501           ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message);
1502      return;
1503    }
1504
1505
1506    // Delete the previous version of the configuration and rename the new one.
1507    try
1508    {
1509      File actualConfig = new File(configFile);
1510      File tmpConfig = new File(tempConfig);
1511      renameFile(tmpConfig, actualConfig);
1512    }
1513    catch (Exception e)
1514    {
1515      logger.traceException(e);
1516
1517      LocalizableMessage message =
1518          ERR_CONFIG_FILE_WRITE_CANNOT_RENAME_NEW_CONFIG.get(tempConfig, configFile, stackTraceToSingleLineString(e));
1519      logger.error(message);
1520
1521      DirectoryServer.sendAlertNotification(this,
1522           ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message);
1523      return;
1524    }
1525
1526    configurationDigest = calculateConfigDigest();
1527
1528
1529    // Try to write the archive for the new configuration.
1530    if (maintainConfigArchive)
1531    {
1532      writeConfigArchive();
1533    }
1534  }
1535
1536
1537
1538  /**
1539   * Writes the current configuration to the configuration archive.  This will
1540   * be a best-effort attempt.
1541   */
1542  private void writeConfigArchive()
1543  {
1544    if (! maintainConfigArchive)
1545    {
1546      return;
1547    }
1548
1549    // Determine the path to the directory that will hold the archived
1550    // configuration files.
1551    File configDirectory  = new File(configFile).getParentFile();
1552    File archiveDirectory = new File(configDirectory, CONFIG_ARCHIVE_DIR_NAME);
1553
1554
1555    // If the archive directory doesn't exist, then create it.
1556    if (! archiveDirectory.exists())
1557    {
1558      try
1559      {
1560        if (! archiveDirectory.mkdirs())
1561        {
1562          LocalizableMessage message = ERR_CONFIG_FILE_CANNOT_CREATE_ARCHIVE_DIR_NO_REASON.get(
1563              archiveDirectory.getAbsolutePath());
1564          logger.error(message);
1565
1566          DirectoryServer.sendAlertNotification(this,
1567               ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message);
1568          return;
1569        }
1570      }
1571      catch (Exception e)
1572      {
1573        logger.traceException(e);
1574
1575        LocalizableMessage message =
1576            ERR_CONFIG_FILE_CANNOT_CREATE_ARCHIVE_DIR.get(archiveDirectory
1577                .getAbsolutePath(), stackTraceToSingleLineString(e));
1578        logger.error(message);
1579
1580        DirectoryServer.sendAlertNotification(this,
1581             ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message);
1582        return;
1583      }
1584    }
1585
1586
1587    // Determine the appropriate name to use for the current configuration.
1588    File archiveFile;
1589    try
1590    {
1591      String timestamp = TimeThread.getGMTTime();
1592      archiveFile = new File(archiveDirectory, "config-" + timestamp + ".gz");
1593      if (archiveFile.exists())
1594      {
1595        int counter = 2;
1596        archiveFile = new File(archiveDirectory,
1597                               "config-" + timestamp + "-" + counter + ".gz");
1598
1599        while (archiveFile.exists())
1600        {
1601          counter++;
1602          archiveFile = new File(archiveDirectory,
1603                                 "config-" + timestamp + "-" + counter + ".gz");
1604        }
1605      }
1606    }
1607    catch (Exception e)
1608    {
1609      logger.traceException(e);
1610
1611      LocalizableMessage message =
1612          ERR_CONFIG_FILE_CANNOT_WRITE_CONFIG_ARCHIVE
1613              .get(stackTraceToSingleLineString(e));
1614      logger.error(message);
1615
1616      DirectoryServer.sendAlertNotification(this,
1617           ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message);
1618      return;
1619    }
1620
1621
1622    // Copy the current configuration to the new configuration file.
1623    byte[]           buffer       = new byte[8192];
1624    FileInputStream  inputStream  = null;
1625    GZIPOutputStream outputStream = null;
1626    try
1627    {
1628      inputStream  = new FileInputStream(configFile);
1629      outputStream = new GZIPOutputStream(new FileOutputStream(archiveFile));
1630      FilePermission.setSafePermissions(archiveFile, 0600);
1631      int bytesRead = inputStream.read(buffer);
1632      while (bytesRead > 0)
1633      {
1634        outputStream.write(buffer, 0, bytesRead);
1635        bytesRead = inputStream.read(buffer);
1636      }
1637    }
1638    catch (Exception e)
1639    {
1640      logger.traceException(e);
1641
1642      LocalizableMessage message =
1643          ERR_CONFIG_FILE_CANNOT_WRITE_CONFIG_ARCHIVE
1644              .get(stackTraceToSingleLineString(e));
1645      logger.error(message);
1646
1647      DirectoryServer.sendAlertNotification(this,
1648           ALERT_TYPE_CANNOT_WRITE_CONFIGURATION, message);
1649      return;
1650    }
1651    finally
1652    {
1653      StaticUtils.close(inputStream, outputStream);
1654    }
1655
1656
1657    // If we should enforce a maximum number of archived configurations, then
1658    // see if there are any old ones that we need to delete.
1659    if (maxConfigArchiveSize > 0)
1660    {
1661      String[] archivedFileList = archiveDirectory.list();
1662      int numToDelete = archivedFileList.length - maxConfigArchiveSize;
1663      if (numToDelete > 0)
1664      {
1665        Set<String> archiveSet = new TreeSet<>();
1666        for (String name : archivedFileList)
1667        {
1668          if (! name.startsWith("config-"))
1669          {
1670            continue;
1671          }
1672
1673          // Simply ordering by filename should work, even when there are
1674          // timestamp conflicts, because the dash comes before the period in
1675          // the ASCII character set.
1676          archiveSet.add(name);
1677        }
1678
1679        Iterator<String> iterator = archiveSet.iterator();
1680        for (int i=0; i < numToDelete && iterator.hasNext(); i++)
1681        {
1682          File f = new File(archiveDirectory, iterator.next());
1683          try
1684          {
1685            f.delete();
1686          } catch (Exception e) {}
1687        }
1688      }
1689    }
1690  }
1691
1692  /** {@inheritDoc} */
1693  @Override
1694  public void writeSuccessfulStartupConfig()
1695  {
1696    if (useLastKnownGoodConfig)
1697    {
1698      // The server was started with the "last known good" configuration, so we
1699      // shouldn't overwrite it with something that is probably bad.
1700      return;
1701    }
1702
1703
1704    String startOKFilePath = configFile + ".startok";
1705    String tempFilePath    = startOKFilePath + ".tmp";
1706    String oldFilePath     = startOKFilePath + ".old";
1707
1708
1709    // Copy the current config file to a temporary file.
1710    File tempFile = new File(tempFilePath);
1711    FileInputStream inputStream = null;
1712    try
1713    {
1714      inputStream = new FileInputStream(configFile);
1715
1716      FileOutputStream outputStream = null;
1717      try
1718      {
1719        outputStream = new FileOutputStream(tempFilePath, false);
1720        FilePermission.setSafePermissions(tempFile, 0600);
1721        try
1722        {
1723          byte[] buffer = new byte[8192];
1724          while (true)
1725          {
1726            int bytesRead = inputStream.read(buffer);
1727            if (bytesRead < 0)
1728            {
1729              break;
1730            }
1731
1732            outputStream.write(buffer, 0, bytesRead);
1733          }
1734        }
1735        catch (Exception e)
1736        {
1737          logger.traceException(e);
1738          logger.error(ERR_STARTOK_CANNOT_WRITE, configFile, tempFilePath, getExceptionMessage(e));
1739          return;
1740        }
1741      }
1742      catch (Exception e)
1743      {
1744        logger.traceException(e);
1745        logger.error(ERR_STARTOK_CANNOT_OPEN_FOR_WRITING, tempFilePath, getExceptionMessage(e));
1746        return;
1747      }
1748      finally
1749      {
1750        close(outputStream);
1751      }
1752    }
1753    catch (Exception e)
1754    {
1755      logger.traceException(e);
1756      logger.error(ERR_STARTOK_CANNOT_OPEN_FOR_READING, configFile, getExceptionMessage(e));
1757      return;
1758    }
1759    finally
1760    {
1761      close(inputStream);
1762    }
1763
1764
1765    // If a ".startok" file already exists, then move it to an ".old" file.
1766    File oldFile = new File(oldFilePath);
1767    try
1768    {
1769      if (oldFile.exists())
1770      {
1771        oldFile.delete();
1772      }
1773    }
1774    catch (Exception e)
1775    {
1776      logger.traceException(e);
1777    }
1778
1779    File startOKFile = new File(startOKFilePath);
1780    try
1781    {
1782      if (startOKFile.exists())
1783      {
1784        startOKFile.renameTo(oldFile);
1785      }
1786    }
1787    catch (Exception e)
1788    {
1789      logger.traceException(e);
1790    }
1791
1792
1793    // Rename the temp file to the ".startok" file.
1794    try
1795    {
1796      tempFile.renameTo(startOKFile);
1797    } catch (Exception e)
1798    {
1799      logger.traceException(e);
1800      logger.error(ERR_STARTOK_CANNOT_RENAME, tempFilePath, startOKFilePath, getExceptionMessage(e));
1801      return;
1802    }
1803
1804
1805    // Remove the ".old" file if there is one.
1806    try
1807    {
1808      if (oldFile.exists())
1809      {
1810        oldFile.delete();
1811      }
1812    }
1813    catch (Exception e)
1814    {
1815      logger.traceException(e);
1816    }
1817  }
1818
1819  /** {@inheritDoc} */
1820  @Override
1821  public Set<String> getSupportedControls()
1822  {
1823    return Collections.emptySet();
1824  }
1825
1826  /** {@inheritDoc} */
1827  @Override
1828  public Set<String> getSupportedFeatures()
1829  {
1830    return Collections.emptySet();
1831  }
1832
1833  /** {@inheritDoc} */
1834  @Override
1835  public boolean supports(BackendOperation backendOperation)
1836  {
1837    switch (backendOperation)
1838    {
1839    case BACKUP:
1840    case RESTORE:
1841      return true;
1842
1843    default:
1844      return false;
1845    }
1846  }
1847
1848  /** {@inheritDoc} */
1849  @Override
1850  public void exportLDIF(LDIFExportConfig exportConfig)
1851         throws DirectoryException
1852  {
1853    // TODO We would need export-ldif to initialize this backend.
1854    writeLDIF(exportConfig);
1855  }
1856
1857  /**
1858   * Writes the current configuration to LDIF with the provided export
1859   * configuration.
1860   *
1861   * @param  exportConfig  The configuration to use for the export.
1862   *
1863   * @throws  DirectoryException  If a problem occurs while writing the LDIF.
1864   */
1865  private void writeLDIF(LDIFExportConfig exportConfig)
1866         throws DirectoryException
1867  {
1868    LDIFWriter writer;
1869    try
1870    {
1871      writer = new LDIFWriter(exportConfig);
1872      writer.writeComment(INFO_CONFIG_FILE_HEADER.get(), 80);
1873      writeEntryAndChildren(writer, configRootEntry);
1874    }
1875    catch (Exception e)
1876    {
1877      logger.traceException(e);
1878
1879      LocalizableMessage message = ERR_CONFIG_LDIF_WRITE_ERROR.get(e);
1880      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
1881    }
1882
1883    try
1884    {
1885      writer.close();
1886    }
1887    catch (Exception e)
1888    {
1889      logger.traceException(e);
1890
1891      LocalizableMessage message = ERR_CONFIG_FILE_CLOSE_ERROR.get(e);
1892      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(), message, e);
1893    }
1894  }
1895
1896
1897
1898  /**
1899   * Writes the provided entry and any children that it may have to the provided
1900   * LDIF writer.
1901   *
1902   * @param  writer       The LDIF writer to use to write the entry and its
1903   *                      children.
1904   * @param  configEntry  The configuration entry to write, along with its
1905   *                      children.
1906   *
1907   * @throws  DirectoryException  If a problem occurs while attempting to write
1908   *                              the entry or one of its children.
1909   */
1910  private void writeEntryAndChildren(LDIFWriter writer, ConfigEntry configEntry)
1911          throws DirectoryException
1912  {
1913    try
1914    {
1915      // Write the entry itself to LDIF.
1916      writer.writeEntry(configEntry.getEntry());
1917    }
1918    catch (Exception e)
1919    {
1920      logger.traceException(e);
1921
1922      LocalizableMessage message = ERR_CONFIG_FILE_WRITE_ERROR.get(
1923          configEntry.getDN(), e);
1924      throw new DirectoryException(DirectoryServer.getServerErrorResultCode(),
1925                                   message, e);
1926    }
1927
1928
1929    // See if the entry has any children.  If so, then iterate through them and
1930    // write them and their children.  We'll copy the entries into a tree map
1931    // so that we have a sensible order in the resulting LDIF.
1932    TreeMap<DN,ConfigEntry> childMap = new TreeMap<>(configEntry.getChildren());
1933    for (ConfigEntry childEntry : childMap.values())
1934    {
1935      writeEntryAndChildren(writer, childEntry);
1936    }
1937  }
1938
1939  /** {@inheritDoc} */
1940  @Override
1941  public LDIFImportResult importLDIF(LDIFImportConfig importConfig, ServerContext serverContext)
1942         throws DirectoryException
1943  {
1944    LocalizableMessage message = ERR_CONFIG_FILE_UNWILLING_TO_IMPORT.get();
1945    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
1946  }
1947
1948  /** {@inheritDoc} */
1949  @Override
1950  public void createBackup(BackupConfig backupConfig) throws DirectoryException
1951  {
1952    new BackupManager(getBackendID()).createBackup(this, backupConfig);
1953  }
1954
1955  /** {@inheritDoc} */
1956  @Override
1957  public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException
1958  {
1959    new BackupManager(getBackendID()).removeBackup(backupDirectory, backupID);
1960  }
1961
1962  /** {@inheritDoc} */
1963  @Override
1964  public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException
1965  {
1966    new BackupManager(getBackendID()).restoreBackup(this, restoreConfig);
1967  }
1968
1969  /** {@inheritDoc} */
1970  @Override
1971  public DN getComponentEntryDN()
1972  {
1973    return configRootEntry.getDN();
1974  }
1975
1976  /** {@inheritDoc} */
1977  @Override
1978  public String getClassName()
1979  {
1980    return CLASS_NAME;
1981  }
1982
1983  /** {@inheritDoc} */
1984  @Override
1985  public Map<String,String> getAlerts()
1986  {
1987    Map<String,String> alerts = new LinkedHashMap<>();
1988
1989    alerts.put(ALERT_TYPE_CANNOT_WRITE_CONFIGURATION,
1990               ALERT_DESCRIPTION_CANNOT_WRITE_CONFIGURATION);
1991    alerts.put(ALERT_TYPE_MANUAL_CONFIG_EDIT_HANDLED,
1992               ALERT_DESCRIPTION_MANUAL_CONFIG_EDIT_HANDLED);
1993    alerts.put(ALERT_TYPE_MANUAL_CONFIG_EDIT_LOST,
1994               ALERT_DESCRIPTION_MANUAL_CONFIG_EDIT_LOST);
1995
1996    return alerts;
1997  }
1998
1999
2000
2001  /**
2002   * Examines the provided result and logs a message if appropriate.  If the
2003   * result code is anything other than {@code SUCCESS}, then it will log an
2004   * error message.  If the operation was successful but admin action is
2005   * required, then it will log a warning message.  If no action is required but
2006   * messages were generated, then it will log an informational message.
2007   *
2008   * @param  result      The config change result object that
2009   * @param  entryDN     The DN of the entry that was added, deleted, or
2010   *                     modified.
2011   * @param  className   The name of the class for the object that generated the
2012   *                     provided result.
2013   * @param  methodName  The name of the method that generated the provided
2014   *                     result.
2015   */
2016  public void handleConfigChangeResult(ConfigChangeResult result, DN entryDN,
2017                                       String className, String methodName)
2018  {
2019    if (result == null)
2020    {
2021      logger.error(ERR_CONFIG_CHANGE_NO_RESULT, className, methodName, entryDN);
2022      return;
2023    }
2024
2025    ResultCode    resultCode          = result.getResultCode();
2026    boolean       adminActionRequired = result.adminActionRequired();
2027
2028    String messageBuffer = Utils.joinAsString("  ", result.getMessages());
2029    if (resultCode != ResultCode.SUCCESS)
2030    {
2031      logger.error(ERR_CONFIG_CHANGE_RESULT_ERROR, className, methodName,
2032              entryDN, resultCode, adminActionRequired, messageBuffer);
2033    }
2034    else if (adminActionRequired)
2035    {
2036      logger.warn(WARN_CONFIG_CHANGE_RESULT_ACTION_REQUIRED, className, methodName, entryDN, messageBuffer);
2037    }
2038    else if (messageBuffer.length() > 0)
2039    {
2040      logger.debug(INFO_CONFIG_CHANGE_RESULT_MESSAGES, className, methodName, entryDN, messageBuffer);
2041    }
2042  }
2043
2044  /** {@inheritDoc} */
2045  @Override
2046  public File getDirectory()
2047  {
2048    return getConfigFileInBackendContext().getParentFile();
2049  }
2050
2051  private File getConfigFileInBackendContext()
2052  {
2053    // This may seem a little weird, but in some context, we only have access to
2054    // this class as a backend and not as the config handler.  We need it as a
2055    // config handler to determine the path to the config file, so we can get
2056    // that from the Directory Server object.
2057    return new File(((ConfigFileHandler) DirectoryServer.getConfigHandler()).configFile);
2058  }
2059
2060  /** {@inheritDoc} */
2061  @Override
2062  public ListIterator<Path> getFilesToBackup()
2063  {
2064    final List<Path> files = new ArrayList<>();
2065
2066    // the main config file
2067    File theConfigFile = getConfigFileInBackendContext();
2068    files.add(theConfigFile.toPath());
2069
2070    // the files in archive directory
2071    File archiveDirectory = new File(getDirectory(), CONFIG_ARCHIVE_DIR_NAME);
2072    if (archiveDirectory.exists())
2073    {
2074      for (File archiveFile : archiveDirectory.listFiles())
2075      {
2076        files.add(archiveFile.toPath());
2077      }
2078    }
2079
2080    return files.listIterator();
2081  }
2082
2083  /** {@inheritDoc} */
2084  @Override
2085  public boolean isDirectRestore()
2086  {
2087    return true;
2088  }
2089
2090  /** {@inheritDoc} */
2091  @Override
2092  public Path beforeRestore() throws DirectoryException
2093  {
2094    // save current config files to a save directory
2095    return BackupManager.saveCurrentFilesToDirectory(this, getBackendID());
2096  }
2097
2098  /** {@inheritDoc} */
2099  @Override
2100  public void afterRestore(Path restoreDirectory, Path saveDirectory) throws DirectoryException
2101  {
2102    // restore was successful, delete save directory
2103    StaticUtils.recursiveDelete(saveDirectory.toFile());
2104  }
2105
2106}