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