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}