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-2008 Sun Microsystems, Inc.
025 *      Portions Copyright 2014-2015 ForgeRock AS
026 */
027package org.opends.server.types;
028
029import org.forgerock.i18n.LocalizableMessage;
030
031import java.io.BufferedReader;
032import java.io.BufferedWriter;
033import java.io.File;
034import java.io.FileReader;
035import java.io.FileWriter;
036import java.io.IOException;
037import java.util.LinkedHashMap;
038import java.util.LinkedList;
039import java.util.List;
040import java.util.Map;
041
042import org.forgerock.opendj.config.server.ConfigException;
043import org.forgerock.i18n.slf4j.LocalizedLogger;
044
045import static org.opends.messages.CoreMessages.*;
046import static org.opends.server.util.ServerConstants.*;
047import static org.opends.server.util.StaticUtils.*;
048
049/**
050 * This class defines a data structure for holding information about a
051 * filesystem directory that contains data for one or more backups associated
052 * with a backend. Only backups for a single backend may be placed in any given
053 * directory.
054 */
055@org.opends.server.types.PublicAPI(
056    stability = org.opends.server.types.StabilityLevel.VOLATILE,
057    mayInstantiate = true,
058    mayExtend = false,
059    mayInvoke = true)
060public final class BackupDirectory
061{
062  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
063
064  /**
065   * The name of the property that will be used to provide the DN of
066   * the configuration entry for the backend associated with the
067   * backups in this directory.
068   */
069  public static final String PROPERTY_BACKEND_CONFIG_DN = "backend_dn";
070
071  /**
072   * The DN of the configuration entry for the backend with which this
073   * backup directory is associated.
074   */
075  private final DN configEntryDN;
076
077  /**
078   * The set of backups in the specified directory.  The iteration
079   * order will be the order in which the backups were created.
080   */
081  private final Map<String, BackupInfo> backups;
082
083  /** The filesystem path to the backup directory. */
084  private final String path;
085
086  /**
087   * Creates a new backup directory object with the provided information.
088   *
089   * @param path
090   *          The path to the directory containing the backup file(s).
091   * @param configEntryDN
092   *          The DN of the configuration entry for the backend with which this
093   *          backup directory is associated.
094   */
095  public BackupDirectory(String path, DN configEntryDN)
096  {
097    this(path, configEntryDN, null);
098  }
099
100  /**
101   * Creates a new backup directory object with the provided information.
102   *
103   * @param path
104   *          The path to the directory containing the backup file(s).
105   * @param configEntryDN
106   *          The DN of the configuration entry for the backend with which this
107   *          backup directory is associated.
108   * @param backups
109   *          Information about the set of backups available within the
110   *          specified directory.
111   */
112  public BackupDirectory(String path, DN configEntryDN, LinkedHashMap<String, BackupInfo> backups)
113  {
114    this.path = path;
115    this.configEntryDN = configEntryDN;
116
117    if (backups != null)
118    {
119      this.backups = backups;
120    }
121    else
122    {
123      this.backups = new LinkedHashMap<>();
124    }
125  }
126
127  /**
128   * Retrieves the path to the directory containing the backup file(s).
129   *
130   * @return The path to the directory containing the backup file(s).
131   */
132  public String getPath()
133  {
134    return path;
135  }
136
137  /**
138   * Retrieves the DN of the configuration entry for the backend with which this
139   * backup directory is associated.
140   *
141   * @return The DN of the configuration entry for the backend with which this
142   *         backup directory is associated.
143   */
144  public DN getConfigEntryDN()
145  {
146    return configEntryDN;
147  }
148
149  /**
150   * Retrieves the set of backups in this backup directory, as a mapping between
151   * the backup ID and the associated backup info. The iteration order for the
152   * map will be the order in which the backups were created.
153   *
154   * @return The set of backups in this backup directory.
155   */
156  public Map<String, BackupInfo> getBackups()
157  {
158    return backups;
159  }
160
161  /**
162   * Retrieves the backup info structure for the backup with the specified ID.
163   *
164   * @param backupID
165   *          The backup ID for the structure to retrieve.
166   * @return The requested backup info structure, or <CODE>null</CODE> if no such
167   *         structure exists.
168   */
169  public BackupInfo getBackupInfo(String backupID)
170  {
171    return backups.get(backupID);
172  }
173
174  /**
175   * Retrieves the most recent backup for this backup directory, according to
176   * the backup date.
177   *
178   * @return The most recent backup for this backup directory, according to the
179   *         backup date, or <CODE>null</CODE> if there are no backups in the
180   *         backup directory.
181   */
182  public BackupInfo getLatestBackup()
183  {
184    BackupInfo latestBackup = null;
185    for (BackupInfo backup : backups.values())
186    {
187      if (latestBackup == null
188          || backup.getBackupDate().getTime() > latestBackup.getBackupDate().getTime())
189      {
190        latestBackup = backup;
191      }
192    }
193
194    return latestBackup;
195  }
196
197  /**
198   * Adds information about the provided backup to this backup directory.
199   *
200   * @param backupInfo
201   *          The backup info structure for the backup to be added.
202   * @throws ConfigException
203   *           If another backup already exists with the same backup ID.
204   */
205  public void addBackup(BackupInfo backupInfo) throws ConfigException
206  {
207    String backupID = backupInfo.getBackupID();
208    if (backups.containsKey(backupID))
209    {
210      throw new ConfigException(ERR_BACKUPDIRECTORY_ADD_DUPLICATE_ID.get(backupID, path));
211    }
212    backups.put(backupID, backupInfo);
213  }
214
215  /**
216   * Removes the backup with the specified backup ID from this backup directory.
217   *
218   * @param backupID
219   *          The backup ID for the backup to remove from this backup directory.
220   * @throws ConfigException
221   *           If it is not possible to remove the requested backup for some
222   *           reason (e.g., no such backup exists, or another backup is
223   *           dependent on it).
224   */
225  public void removeBackup(String backupID) throws ConfigException
226  {
227    if (!backups.containsKey(backupID))
228    {
229      throw new ConfigException(ERR_BACKUPDIRECTORY_NO_SUCH_BACKUP.get(backupID, path));
230    }
231
232    for (BackupInfo backup : backups.values())
233    {
234      if (backup.dependsOn(backupID))
235      {
236        throw new ConfigException(ERR_BACKUPDIRECTORY_UNRESOLVED_DEPENDENCY.get(backupID, path, backup.getBackupID()));
237      }
238    }
239
240    backups.remove(backupID);
241  }
242
243  /**
244   * Retrieves a path to the backup descriptor file that should be used for this
245   * backup directory.
246   *
247   * @return A path to the backup descriptor file that should be used for this
248   *         backup directory.
249   */
250  public String getDescriptorPath()
251  {
252    return path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE;
253  }
254
255  /**
256   * Writes the descriptor with the information contained in this structure to
257   * disk in the appropriate directory.
258   *
259   * @throws IOException
260   *           If a problem occurs while writing to disk.
261   */
262  public void writeBackupDirectoryDescriptor() throws IOException
263  {
264    // First make sure that the target directory exists.  If it doesn't, then try to create it.
265    createDirectoryIfNotExists();
266
267    // We'll write to a temporary file so that we won't destroy the live copy if a problem occurs.
268    String newDescriptorFilePath = path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE + ".new";
269    File newDescriptorFile = new File(newDescriptorFilePath);
270    try (BufferedWriter writer = new BufferedWriter(new FileWriter(newDescriptorFile, false)))
271    {
272      // The first line in the file will only contain the DN of the configuration entry for the associated backend.
273      writer.write(PROPERTY_BACKEND_CONFIG_DN + "=" + configEntryDN);
274      writer.newLine();
275      writer.newLine();
276
277      // Iterate through all of the backups and add them to the file.
278      for (BackupInfo backup : backups.values())
279      {
280        List<String> backupLines = backup.encode();
281        for (String line : backupLines)
282        {
283          writer.write(line);
284          writer.newLine();
285        }
286
287        writer.newLine();
288      }
289
290      // At this point, the file should be complete so flush and close it.
291      writer.flush();
292    }
293
294    // If previous backup descriptor file exists, then rename it.
295    String descriptorFilePath = path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE;
296    File descriptorFile = new File(descriptorFilePath);
297    renameOldBackupDescriptorFile(descriptorFile, descriptorFilePath);
298
299    // Rename the new descriptor file to match the previous one.
300    try
301    {
302      newDescriptorFile.renameTo(descriptorFile);
303    }
304    catch (Exception e)
305    {
306      logger.traceException(e);
307      LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_RENAME_NEW_DESCRIPTOR.get(
308          newDescriptorFilePath, descriptorFilePath, getExceptionMessage(e));
309      throw new IOException(message.toString());
310    }
311  }
312
313  private void createDirectoryIfNotExists() throws IOException
314  {
315    File dir = new File(path);
316    if (!dir.exists())
317    {
318      try
319      {
320        dir.mkdirs();
321      }
322      catch (Exception e)
323      {
324        logger.traceException(e);
325        LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_CREATE_DIRECTORY.get(path, getExceptionMessage(e));
326        throw new IOException(message.toString());
327      }
328    }
329    else if (!dir.isDirectory())
330    {
331      throw new IOException(ERR_BACKUPDIRECTORY_NOT_DIRECTORY.get(path).toString());
332    }
333  }
334
335  private void renameOldBackupDescriptorFile(File descriptorFile, String descriptorFilePath) throws IOException
336  {
337    if (descriptorFile.exists())
338    {
339      String savedDescriptorFilePath = descriptorFilePath + ".save";
340      File savedDescriptorFile = new File(savedDescriptorFilePath);
341      if (savedDescriptorFile.exists())
342      {
343        try
344        {
345          savedDescriptorFile.delete();
346        }
347        catch (Exception e)
348        {
349          logger.traceException(e);
350          LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_DELETE_SAVED_DESCRIPTOR.get(
351              savedDescriptorFilePath, getExceptionMessage(e), descriptorFilePath, descriptorFilePath);
352          throw new IOException(message.toString());
353        }
354      }
355
356      try
357      {
358        descriptorFile.renameTo(savedDescriptorFile);
359      }
360      catch (Exception e)
361      {
362        logger.traceException(e);
363        LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_RENAME_CURRENT_DESCRIPTOR.get(descriptorFilePath,
364            savedDescriptorFilePath, getExceptionMessage(e), descriptorFilePath, descriptorFilePath);
365        throw new IOException(message.toString());
366      }
367    }
368  }
369
370  /**
371   * Reads the backup descriptor file in the specified path and uses the
372   * information it contains to create a new backup directory structure.
373   *
374   * @param path
375   *          The path to the directory containing the backup descriptor file to
376   *          read.
377   * @return The backup directory structure created from the contents of the
378   *         descriptor file.
379   * @throws IOException
380   *           If a problem occurs while trying to read the contents of the
381   *           descriptor file.
382   * @throws ConfigException
383   *           If the contents of the descriptor file cannot be parsed to create
384   *           a backup directory structure.
385   */
386  public static BackupDirectory readBackupDirectoryDescriptor(String path) throws IOException, ConfigException
387  {
388    // Make sure that the descriptor file exists.
389    String descriptorFilePath = path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE;
390    if (!new File(descriptorFilePath).exists())
391    {
392      throw new ConfigException(ERR_BACKUPDIRECTORY_NO_DESCRIPTOR_FILE.get(descriptorFilePath));
393    }
394
395    // Open the file for reading.
396    // The first line should be the DN of the associated configuration entry.
397    try (BufferedReader reader = new BufferedReader(new FileReader(descriptorFilePath)))
398    {
399      String line = reader.readLine();
400      if (line == null || line.length() == 0)
401      {
402        throw new ConfigException(ERR_BACKUPDIRECTORY_CANNOT_READ_CONFIG_ENTRY_DN.get(descriptorFilePath));
403      }
404      else if (!line.startsWith(PROPERTY_BACKEND_CONFIG_DN))
405      {
406        throw new ConfigException(ERR_BACKUPDIRECTORY_FIRST_LINE_NOT_DN.get(descriptorFilePath, line));
407      }
408
409      String dnString = line.substring(PROPERTY_BACKEND_CONFIG_DN.length() + 1);
410      DN configEntryDN;
411      try
412      {
413        configEntryDN = DN.valueOf(dnString);
414      }
415      catch (DirectoryException de)
416      {
417        LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_DECODE_DN.get(
418            dnString, descriptorFilePath, de.getMessageObject());
419        throw new ConfigException(message, de);
420      }
421      catch (Exception e)
422      {
423        LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_DECODE_DN.get(
424            dnString, descriptorFilePath, getExceptionMessage(e));
425        throw new ConfigException(message, e);
426      }
427
428      // Create the backup directory structure from what we know so far.
429      BackupDirectory backupDirectory = new BackupDirectory(path, configEntryDN);
430
431      // Iterate through the rest of the file and create the backup info structures.
432      // Blank lines will be considered delimiters.
433      List<String> lines = new LinkedList<>();
434      while ((line = reader.readLine()) != null)
435      {
436        if (!line.isEmpty())
437        {
438          lines.add(line);
439          continue;
440        }
441
442        // We are on a delimiter blank line.
443        readBackupFromLines(backupDirectory, lines);
444      }
445      readBackupFromLines(backupDirectory, lines);
446
447      return backupDirectory;
448    }
449  }
450
451  private static void readBackupFromLines(BackupDirectory backupDirectory, List<String> lines) throws ConfigException
452  {
453    if (!lines.isEmpty())
454    {
455      backupDirectory.addBackup(BackupInfo.decode(backupDirectory, lines));
456      lines.clear();
457    }
458  }
459}