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