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