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