/* * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * 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 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. 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. */ package org.opends.server.types; import org.opends.messages.Message; 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 org.opends.server.config.ConfigException; import static org.opends.server.loggers.debug.DebugLogger.*; import org.opends.server.loggers.debug.DebugTracer; 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 { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); /** * 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 DN configEntryDN; // The set of backups in the specified directory. The iteration // order will be the order in which the backups were created. private LinkedHashMap<String,BackupInfo> backups; // The filesystem path to the backup directory. private 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 = path; this.configEntryDN = configEntryDN; backups = new LinkedHashMap<String,BackupInfo>(); } /** * 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 = new LinkedHashMap<String,BackupInfo>(); } else { this.backups = backups; } } /** * 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 LinkedHashMap<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) { latestBackup = backup; } else { if (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)) { Message message = ERR_BACKUPDIRECTORY_ADD_DUPLICATE_ID.get(backupID, path); throw new ConfigException(message); } 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)) { Message message = ERR_BACKUPDIRECTORY_NO_SUCH_BACKUP.get(backupID, path); throw new ConfigException(message); } for (BackupInfo backup : backups.values()) { if (backup.dependsOn(backupID)) { Message message = ERR_BACKUPDIRECTORY_UNRESOLVED_DEPENDENCY. get(backupID, path, backup.getBackupID()); throw new ConfigException(message); } } 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. File dir = new File(path); if (! dir.exists()) { try { dir.mkdirs(); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_BACKUPDIRECTORY_CANNOT_CREATE_DIRECTORY. get(path, getExceptionMessage(e)); throw new IOException(message.toString()); } } else if (! dir.isDirectory()) { Message message = ERR_BACKUPDIRECTORY_NOT_DIRECTORY.get(path); throw new IOException(message.toString()); } // 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); 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.toString()); writer.newLine(); writer.newLine(); // Iterate through all of the backups and add them to the file. for (BackupInfo backup : backups.values()) { LinkedList<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(); writer.close(); // If previous backup descriptor file exists, then rename it. String descriptorFilePath = path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE; File descriptorFile = new File(descriptorFilePath); if (descriptorFile.exists()) { String savedDescriptorFilePath = descriptorFilePath + ".save"; File savedDescriptorFile = new File(savedDescriptorFilePath); if (savedDescriptorFile.exists()) { try { savedDescriptorFile.delete(); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_BACKUPDIRECTORY_CANNOT_DELETE_SAVED_DESCRIPTOR. get(savedDescriptorFilePath, getExceptionMessage(e), newDescriptorFilePath, descriptorFilePath); throw new IOException(message.toString()); } } try { descriptorFile.renameTo(savedDescriptorFile); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_BACKUPDIRECTORY_CANNOT_RENAME_CURRENT_DESCRIPTOR. get(descriptorFilePath, savedDescriptorFilePath, getExceptionMessage(e), newDescriptorFilePath, descriptorFilePath); throw new IOException(message.toString()); } } // Rename the new descriptor file to match the previous one. try { newDescriptorFile.renameTo(descriptorFile); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_BACKUPDIRECTORY_CANNOT_RENAME_NEW_DESCRIPTOR. get(newDescriptorFilePath, descriptorFilePath, getExceptionMessage(e)); 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; File descriptorFile = new File(descriptorFilePath); if (! descriptorFile.exists()) { Message message = ERR_BACKUPDIRECTORY_NO_DESCRIPTOR_FILE.get( descriptorFilePath); throw new ConfigException(message); } // Open the file for reading. The first line should be the DN of // the associated configuration entry. BufferedReader reader = new BufferedReader(new FileReader(descriptorFile)); String line = reader.readLine(); if ((line == null) || (line.length() == 0)) { Message message = ERR_BACKUPDIRECTORY_CANNOT_READ_CONFIG_ENTRY_DN. get(descriptorFilePath); throw new ConfigException(message); } else if (! line.startsWith(PROPERTY_BACKEND_CONFIG_DN)) { Message message = ERR_BACKUPDIRECTORY_FIRST_LINE_NOT_DN.get( descriptorFilePath, line); throw new ConfigException(message); } String dnString = line.substring(PROPERTY_BACKEND_CONFIG_DN.length() + 1); DN configEntryDN; try { configEntryDN = DN.decode(dnString); } catch (DirectoryException de) { Message message = ERR_BACKUPDIRECTORY_CANNOT_DECODE_DN.get( dnString, descriptorFilePath, de.getMessageObject()); throw new ConfigException(message, de); } catch (Exception e) { Message 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. LinkedList<String> lines = new LinkedList<String>(); while (true) { line = reader.readLine(); if ((line == null) || (line.length() == 0)) { // It's a blank line or the end of the file. If we have lines // to process then do so. Otherwise, move on. if (lines.isEmpty()) { if (line == null) { break; } else { continue; } } // Parse the lines that we read and add the backup info to the // directory structure. BackupInfo backupInfo = BackupInfo.decode(backupDirectory, lines); backupDirectory.addBackup(backupInfo); lines.clear(); // If it was the end of the file, then break out of the loop. if (line == null) { break; } } else { lines.add(line); } } // Close the reader and return the backup directory structure. reader.close(); return backupDirectory; } }