/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at legal-notices/CDDLv1_0.txt. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2006-2008 Sun Microsystems, Inc. * Portions Copyright 2014-2015 ForgeRock AS */ package org.opends.server.types; import org.forgerock.i18n.LocalizableMessage; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import org.forgerock.opendj.config.server.ConfigException; import org.forgerock.i18n.slf4j.LocalizedLogger; import static org.opends.messages.CoreMessages.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.*; /** * This class defines a data structure for holding information about a * filesystem directory that contains data for one or more backups associated * with a backend. Only backups for a single backend may be placed in any given * directory. */ @org.opends.server.types.PublicAPI( stability = org.opends.server.types.StabilityLevel.VOLATILE, mayInstantiate = true, mayExtend = false, mayInvoke = true) public final class BackupDirectory { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** * The name of the property that will be used to provide the DN of * the configuration entry for the backend associated with the * backups in this directory. */ public static final String PROPERTY_BACKEND_CONFIG_DN = "backend_dn"; /** * The DN of the configuration entry for the backend with which this * backup directory is associated. */ private final DN configEntryDN; /** * The set of backups in the specified directory. The iteration * order will be the order in which the backups were created. */ private final Map<String, BackupInfo> backups; /** The filesystem path to the backup directory. */ private final String path; /** * Creates a new backup directory object with the provided information. * * @param path * The path to the directory containing the backup file(s). * @param configEntryDN * The DN of the configuration entry for the backend with which this * backup directory is associated. */ public BackupDirectory(String path, DN configEntryDN) { this(path, configEntryDN, null); } /** * Creates a new backup directory object with the provided information. * * @param path * The path to the directory containing the backup file(s). * @param configEntryDN * The DN of the configuration entry for the backend with which this * backup directory is associated. * @param backups * Information about the set of backups available within the * specified directory. */ public BackupDirectory(String path, DN configEntryDN, LinkedHashMap<String, BackupInfo> backups) { this.path = path; this.configEntryDN = configEntryDN; if (backups != null) { this.backups = backups; } else { this.backups = new LinkedHashMap<>(); } } /** * Retrieves the path to the directory containing the backup file(s). * * @return The path to the directory containing the backup file(s). */ public String getPath() { return path; } /** * Retrieves the DN of the configuration entry for the backend with which this * backup directory is associated. * * @return The DN of the configuration entry for the backend with which this * backup directory is associated. */ public DN getConfigEntryDN() { return configEntryDN; } /** * Retrieves the set of backups in this backup directory, as a mapping between * the backup ID and the associated backup info. The iteration order for the * map will be the order in which the backups were created. * * @return The set of backups in this backup directory. */ public Map<String, BackupInfo> getBackups() { return backups; } /** * Retrieves the backup info structure for the backup with the specified ID. * * @param backupID * The backup ID for the structure to retrieve. * @return The requested backup info structure, or <CODE>null</CODE> if no such * structure exists. */ public BackupInfo getBackupInfo(String backupID) { return backups.get(backupID); } /** * Retrieves the most recent backup for this backup directory, according to * the backup date. * * @return The most recent backup for this backup directory, according to the * backup date, or <CODE>null</CODE> if there are no backups in the * backup directory. */ public BackupInfo getLatestBackup() { BackupInfo latestBackup = null; for (BackupInfo backup : backups.values()) { if (latestBackup == null || backup.getBackupDate().getTime() > latestBackup.getBackupDate().getTime()) { latestBackup = backup; } } return latestBackup; } /** * Adds information about the provided backup to this backup directory. * * @param backupInfo * The backup info structure for the backup to be added. * @throws ConfigException * If another backup already exists with the same backup ID. */ public void addBackup(BackupInfo backupInfo) throws ConfigException { String backupID = backupInfo.getBackupID(); if (backups.containsKey(backupID)) { throw new ConfigException(ERR_BACKUPDIRECTORY_ADD_DUPLICATE_ID.get(backupID, path)); } backups.put(backupID, backupInfo); } /** * Removes the backup with the specified backup ID from this backup directory. * * @param backupID * The backup ID for the backup to remove from this backup directory. * @throws ConfigException * If it is not possible to remove the requested backup for some * reason (e.g., no such backup exists, or another backup is * dependent on it). */ public void removeBackup(String backupID) throws ConfigException { if (!backups.containsKey(backupID)) { throw new ConfigException(ERR_BACKUPDIRECTORY_NO_SUCH_BACKUP.get(backupID, path)); } for (BackupInfo backup : backups.values()) { if (backup.dependsOn(backupID)) { throw new ConfigException(ERR_BACKUPDIRECTORY_UNRESOLVED_DEPENDENCY.get(backupID, path, backup.getBackupID())); } } backups.remove(backupID); } /** * Retrieves a path to the backup descriptor file that should be used for this * backup directory. * * @return A path to the backup descriptor file that should be used for this * backup directory. */ public String getDescriptorPath() { return path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE; } /** * Writes the descriptor with the information contained in this structure to * disk in the appropriate directory. * * @throws IOException * If a problem occurs while writing to disk. */ public void writeBackupDirectoryDescriptor() throws IOException { // First make sure that the target directory exists. If it doesn't, then try to create it. createDirectoryIfNotExists(); // We'll write to a temporary file so that we won't destroy the live copy if a problem occurs. String newDescriptorFilePath = path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE + ".new"; File newDescriptorFile = new File(newDescriptorFilePath); try (BufferedWriter writer = new BufferedWriter(new FileWriter(newDescriptorFile, false))) { // The first line in the file will only contain the DN of the configuration entry for the associated backend. writer.write(PROPERTY_BACKEND_CONFIG_DN + "=" + configEntryDN); writer.newLine(); writer.newLine(); // Iterate through all of the backups and add them to the file. for (BackupInfo backup : backups.values()) { List<String> backupLines = backup.encode(); for (String line : backupLines) { writer.write(line); writer.newLine(); } writer.newLine(); } // At this point, the file should be complete so flush and close it. writer.flush(); } // If previous backup descriptor file exists, then rename it. String descriptorFilePath = path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE; File descriptorFile = new File(descriptorFilePath); renameOldBackupDescriptorFile(descriptorFile, descriptorFilePath); // Rename the new descriptor file to match the previous one. try { newDescriptorFile.renameTo(descriptorFile); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_RENAME_NEW_DESCRIPTOR.get( newDescriptorFilePath, descriptorFilePath, getExceptionMessage(e)); throw new IOException(message.toString()); } } private void createDirectoryIfNotExists() throws IOException { File dir = new File(path); if (!dir.exists()) { try { dir.mkdirs(); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_CREATE_DIRECTORY.get(path, getExceptionMessage(e)); throw new IOException(message.toString()); } } else if (!dir.isDirectory()) { throw new IOException(ERR_BACKUPDIRECTORY_NOT_DIRECTORY.get(path).toString()); } } private void renameOldBackupDescriptorFile(File descriptorFile, String descriptorFilePath) throws IOException { if (descriptorFile.exists()) { String savedDescriptorFilePath = descriptorFilePath + ".save"; File savedDescriptorFile = new File(savedDescriptorFilePath); if (savedDescriptorFile.exists()) { try { savedDescriptorFile.delete(); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_DELETE_SAVED_DESCRIPTOR.get( savedDescriptorFilePath, getExceptionMessage(e), descriptorFilePath, descriptorFilePath); throw new IOException(message.toString()); } } try { descriptorFile.renameTo(savedDescriptorFile); } catch (Exception e) { logger.traceException(e); LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_RENAME_CURRENT_DESCRIPTOR.get(descriptorFilePath, savedDescriptorFilePath, getExceptionMessage(e), descriptorFilePath, descriptorFilePath); throw new IOException(message.toString()); } } } /** * Reads the backup descriptor file in the specified path and uses the * information it contains to create a new backup directory structure. * * @param path * The path to the directory containing the backup descriptor file to * read. * @return The backup directory structure created from the contents of the * descriptor file. * @throws IOException * If a problem occurs while trying to read the contents of the * descriptor file. * @throws ConfigException * If the contents of the descriptor file cannot be parsed to create * a backup directory structure. */ public static BackupDirectory readBackupDirectoryDescriptor(String path) throws IOException, ConfigException { // Make sure that the descriptor file exists. String descriptorFilePath = path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE; if (!new File(descriptorFilePath).exists()) { throw new ConfigException(ERR_BACKUPDIRECTORY_NO_DESCRIPTOR_FILE.get(descriptorFilePath)); } // Open the file for reading. // The first line should be the DN of the associated configuration entry. try (BufferedReader reader = new BufferedReader(new FileReader(descriptorFilePath))) { String line = reader.readLine(); if (line == null || line.length() == 0) { throw new ConfigException(ERR_BACKUPDIRECTORY_CANNOT_READ_CONFIG_ENTRY_DN.get(descriptorFilePath)); } else if (!line.startsWith(PROPERTY_BACKEND_CONFIG_DN)) { throw new ConfigException(ERR_BACKUPDIRECTORY_FIRST_LINE_NOT_DN.get(descriptorFilePath, line)); } String dnString = line.substring(PROPERTY_BACKEND_CONFIG_DN.length() + 1); DN configEntryDN; try { configEntryDN = DN.valueOf(dnString); } catch (DirectoryException de) { LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_DECODE_DN.get( dnString, descriptorFilePath, de.getMessageObject()); throw new ConfigException(message, de); } catch (Exception e) { LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_DECODE_DN.get( dnString, descriptorFilePath, getExceptionMessage(e)); throw new ConfigException(message, e); } // Create the backup directory structure from what we know so far. BackupDirectory backupDirectory = new BackupDirectory(path, configEntryDN); // Iterate through the rest of the file and create the backup info structures. // Blank lines will be considered delimiters. List<String> lines = new LinkedList<>(); while ((line = reader.readLine()) != null) { if (!line.isEmpty()) { lines.add(line); continue; } // We are on a delimiter blank line. readBackupFromLines(backupDirectory, lines); } readBackupFromLines(backupDirectory, lines); return backupDirectory; } } private static void readBackupFromLines(BackupDirectory backupDirectory, List<String> lines) throws ConfigException { if (!lines.isEmpty()) { backupDirectory.addBackup(BackupInfo.decode(backupDirectory, lines)); lines.clear(); } } }