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 010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE 011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE. 012 * See the License for the specific language governing permissions 013 * and limitations under the License. 014 * 015 * When distributing Covered Code, include this CDDL HEADER in each 016 * file and include the License file at 017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, 018 * add the following below this CDDL HEADER, with the fields enclosed 019 * by brackets "[]" replaced with your own identifying information: 020 * Portions Copyright [yyyy] [name of copyright owner] 021 * 022 * CDDL HEADER END 023 * 024 * 025 * Copyright 2006-2008 Sun Microsystems, Inc. 026 */ 027 package org.opends.server.types; 028 import org.opends.messages.Message; 029 030 031 032 import java.io.BufferedReader; 033 import java.io.BufferedWriter; 034 import java.io.File; 035 import java.io.FileReader; 036 import java.io.FileWriter; 037 import java.io.IOException; 038 import java.util.LinkedHashMap; 039 import java.util.LinkedList; 040 041 import org.opends.server.config.ConfigException; 042 043 import static org.opends.server.loggers.debug.DebugLogger.*; 044 import org.opends.server.loggers.debug.DebugTracer; 045 import static org.opends.messages.CoreMessages.*; 046 import static org.opends.server.util.ServerConstants.*; 047 import static org.opends.server.util.StaticUtils.*; 048 049 050 051 /** 052 * This class defines a data structure for holding information about a 053 * filesystem directory that contains data for one or more backups 054 * associated with a backend. Only backups for a single backend may 055 * be placed in any given directory. 056 */ 057 @org.opends.server.types.PublicAPI( 058 stability=org.opends.server.types.StabilityLevel.VOLATILE, 059 mayInstantiate=true, 060 mayExtend=false, 061 mayInvoke=true) 062 public final class BackupDirectory 063 { 064 /** 065 * The tracer object for the debug logger. 066 */ 067 private static final DebugTracer TRACER = getTracer(); 068 069 070 071 072 /** 073 * The name of the property that will be used to provide the DN of 074 * the configuration entry for the backend associated with the 075 * backups in this directory. 076 */ 077 public static final String PROPERTY_BACKEND_CONFIG_DN = 078 "backend_dn"; 079 080 081 082 // The DN of the configuration entry for the backend with which this 083 // backup directory is associated. 084 private DN configEntryDN; 085 086 // The set of backups in the specified directory. The iteration 087 // order will be the order in which the backups were created. 088 private LinkedHashMap<String,BackupInfo> backups; 089 090 // The filesystem path to the backup directory. 091 private String path; 092 093 094 095 /** 096 * Creates a new backup directory object with the provided 097 * information. 098 * 099 * @param path The path to the directory containing the 100 * backup file(s). 101 * @param configEntryDN The DN of the configuration entry for the 102 * backend with which this backup directory 103 * is associated. 104 */ 105 public BackupDirectory(String path, DN configEntryDN) 106 { 107 this.path = path; 108 this.configEntryDN = configEntryDN; 109 110 backups = new LinkedHashMap<String,BackupInfo>(); 111 } 112 113 114 115 /** 116 * Creates a new backup directory object with the provided 117 * information. 118 * 119 * @param path The path to the directory containing the 120 * backup file(s). 121 * @param configEntryDN The DN of the configuration entry for the 122 * backend with which this backup directory 123 * is associated. 124 * @param backups Information about the set of backups 125 * available within the specified directory. 126 */ 127 public BackupDirectory(String path, DN configEntryDN, 128 LinkedHashMap<String,BackupInfo> backups) 129 { 130 this.path = path; 131 this.configEntryDN = configEntryDN; 132 133 if (backups == null) 134 { 135 this.backups = new LinkedHashMap<String,BackupInfo>(); 136 } 137 else 138 { 139 this.backups = backups; 140 } 141 } 142 143 144 145 /** 146 * Retrieves the path to the directory containing the backup 147 * file(s). 148 * 149 * @return The path to the directory containing the backup file(s). 150 */ 151 public String getPath() 152 { 153 return path; 154 } 155 156 157 158 /** 159 * Retrieves the DN of the configuration entry for the backend with 160 * which this backup directory is associated. 161 * 162 * @return The DN of the configuration entry for the backend with 163 * which this backup directory is associated. 164 */ 165 public DN getConfigEntryDN() 166 { 167 return configEntryDN; 168 } 169 170 171 172 /** 173 * Retrieves the set of backups in this backup directory, as a 174 * mapping between the backup ID and the associated backup info. 175 * The iteration order for the map will be the order in which the 176 * backups were created. 177 * 178 * @return The set of backups in this backup directory. 179 */ 180 public LinkedHashMap<String,BackupInfo> getBackups() 181 { 182 return backups; 183 } 184 185 186 187 /** 188 * Retrieves the backup info structure for the backup with the 189 * specified ID. 190 * 191 * @param backupID The backup ID for the structure to retrieve. 192 * 193 * @return The requested backup info structure, or 194 * <CODE>null</CODE> if no such structure exists. 195 */ 196 public BackupInfo getBackupInfo(String backupID) 197 { 198 return backups.get(backupID); 199 } 200 201 202 203 /** 204 * Retrieves the most recent backup for this backup directory, 205 * according to the backup date. 206 * 207 * @return The most recent backup for this backup directory, 208 * according to the backup date, or <CODE>null</CODE> if 209 * there are no backups in the backup directory. 210 */ 211 public BackupInfo getLatestBackup() 212 { 213 BackupInfo latestBackup = null; 214 for (BackupInfo backup : backups.values()) 215 { 216 if (latestBackup == null) 217 { 218 latestBackup = backup; 219 } 220 else 221 { 222 if (backup.getBackupDate().getTime() > 223 latestBackup.getBackupDate().getTime()) 224 { 225 latestBackup = backup; 226 } 227 } 228 } 229 230 return latestBackup; 231 } 232 233 234 235 /** 236 * Adds information about the provided backup to this backup 237 * directory. 238 * 239 * @param backupInfo The backup info structure for the backup to 240 * be added. 241 * 242 * @throws ConfigException If another backup already exists with 243 * the same backup ID. 244 */ 245 public void addBackup(BackupInfo backupInfo) 246 throws ConfigException 247 { 248 String backupID = backupInfo.getBackupID(); 249 if (backups.containsKey(backupID)) 250 { 251 Message message = 252 ERR_BACKUPDIRECTORY_ADD_DUPLICATE_ID.get(backupID, path); 253 throw new ConfigException(message); 254 } 255 256 backups.put(backupID, backupInfo); 257 } 258 259 260 261 /** 262 * Removes the backup with the specified backup ID from this backup 263 * directory. 264 * 265 * @param backupID The backup ID for the backup to remove from 266 * this backup directory. 267 * 268 * @throws ConfigException If it is not possible to remove the 269 * requested backup for some reason (e.g., 270 * no such backup exists, or another 271 * backup is dependent on it). 272 */ 273 public void removeBackup(String backupID) 274 throws ConfigException 275 { 276 if (! backups.containsKey(backupID)) 277 { 278 Message message = 279 ERR_BACKUPDIRECTORY_NO_SUCH_BACKUP.get(backupID, path); 280 throw new ConfigException(message); 281 } 282 283 for (BackupInfo backup : backups.values()) 284 { 285 if (backup.dependsOn(backupID)) 286 { 287 Message message = ERR_BACKUPDIRECTORY_UNRESOLVED_DEPENDENCY. 288 get(backupID, path, backup.getBackupID()); 289 throw new ConfigException(message); 290 } 291 } 292 293 backups.remove(backupID); 294 } 295 296 297 298 /** 299 * Retrieves a path to the backup descriptor file that should be 300 * used for this backup directory. 301 * 302 * @return A path to the backup descriptor file that should be used 303 * for this backup directory. 304 */ 305 public String getDescriptorPath() 306 { 307 return path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE; 308 } 309 310 311 312 /** 313 * Writes the descriptor with the information contained in this 314 * structure to disk in the appropriate directory. 315 * 316 * @throws IOException If a problem occurs while writing to disk. 317 */ 318 public void writeBackupDirectoryDescriptor() 319 throws IOException 320 { 321 // First make sure that the target directory exists. If it 322 // doesn't, then try to create it. 323 File dir = new File(path); 324 if (! dir.exists()) 325 { 326 try 327 { 328 dir.mkdirs(); 329 } 330 catch (Exception e) 331 { 332 if (debugEnabled()) 333 { 334 TRACER.debugCaught(DebugLogLevel.ERROR, e); 335 } 336 337 Message message = ERR_BACKUPDIRECTORY_CANNOT_CREATE_DIRECTORY. 338 get(path, getExceptionMessage(e)); 339 throw new IOException(message.toString()); 340 } 341 } 342 else if (! dir.isDirectory()) 343 { 344 Message message = ERR_BACKUPDIRECTORY_NOT_DIRECTORY.get(path); 345 throw new IOException(message.toString()); 346 } 347 348 349 // We'll write to a temporary file so that we won't destroy the 350 // live copy if a problem occurs. 351 String newDescriptorFilePath = path + File.separator + 352 BACKUP_DIRECTORY_DESCRIPTOR_FILE + 353 ".new"; 354 File newDescriptorFile = new File(newDescriptorFilePath); 355 BufferedWriter writer = 356 new BufferedWriter(new FileWriter(newDescriptorFile, false)); 357 358 359 // The first line in the file will only contain the DN of the 360 // configuration entry for the associated backend. 361 writer.write(PROPERTY_BACKEND_CONFIG_DN + "=" + 362 configEntryDN.toString()); 363 writer.newLine(); 364 writer.newLine(); 365 366 367 // Iterate through all of the backups and add them to the file. 368 for (BackupInfo backup : backups.values()) 369 { 370 LinkedList<String> backupLines = backup.encode(); 371 372 for (String line : backupLines) 373 { 374 writer.write(line); 375 writer.newLine(); 376 } 377 378 writer.newLine(); 379 } 380 381 382 // At this point, the file should be complete so flush and close 383 // it. 384 writer.flush(); 385 writer.close(); 386 387 388 // If previous backup descriptor file exists, then rename it. 389 String descriptorFilePath = path + File.separator + 390 BACKUP_DIRECTORY_DESCRIPTOR_FILE; 391 File descriptorFile = new File(descriptorFilePath); 392 if (descriptorFile.exists()) 393 { 394 String savedDescriptorFilePath = descriptorFilePath + ".save"; 395 File savedDescriptorFile = new File(savedDescriptorFilePath); 396 if (savedDescriptorFile.exists()) 397 { 398 try 399 { 400 savedDescriptorFile.delete(); 401 } 402 catch (Exception e) 403 { 404 if (debugEnabled()) 405 { 406 TRACER.debugCaught(DebugLogLevel.ERROR, e); 407 } 408 409 Message message = 410 ERR_BACKUPDIRECTORY_CANNOT_DELETE_SAVED_DESCRIPTOR. 411 get(savedDescriptorFilePath, getExceptionMessage(e), 412 newDescriptorFilePath, descriptorFilePath); 413 throw new IOException(message.toString()); 414 } 415 } 416 417 try 418 { 419 descriptorFile.renameTo(savedDescriptorFile); 420 } 421 catch (Exception e) 422 { 423 if (debugEnabled()) 424 { 425 TRACER.debugCaught(DebugLogLevel.ERROR, e); 426 } 427 428 Message message = 429 ERR_BACKUPDIRECTORY_CANNOT_RENAME_CURRENT_DESCRIPTOR. 430 get(descriptorFilePath, savedDescriptorFilePath, 431 getExceptionMessage(e), newDescriptorFilePath, 432 descriptorFilePath); 433 throw new IOException(message.toString()); 434 } 435 } 436 437 438 // Rename the new descriptor file to match the previous one. 439 try 440 { 441 newDescriptorFile.renameTo(descriptorFile); 442 } 443 catch (Exception e) 444 { 445 if (debugEnabled()) 446 { 447 TRACER.debugCaught(DebugLogLevel.ERROR, e); 448 } 449 450 Message message = 451 ERR_BACKUPDIRECTORY_CANNOT_RENAME_NEW_DESCRIPTOR. 452 get(newDescriptorFilePath, descriptorFilePath, 453 getExceptionMessage(e)); 454 throw new IOException(message.toString()); 455 } 456 } 457 458 459 460 /** 461 * Reads the backup descriptor file in the specified path and uses 462 * the information it contains to create a new backup directory 463 * structure. 464 * 465 * @param path The path to the directory containing the backup 466 * descriptor file to read. 467 * 468 * @return The backup directory structure created from the contents 469 * of the descriptor file. 470 * 471 * @throws IOException If a problem occurs while trying to read 472 * the contents of the descriptor file. 473 * 474 * @throws ConfigException If the contents of the descriptor file 475 * cannot be parsed to create a backup 476 * directory structure. 477 */ 478 public static BackupDirectory 479 readBackupDirectoryDescriptor(String path) 480 throws IOException, ConfigException 481 { 482 // Make sure that the descriptor file exists. 483 String descriptorFilePath = path + File.separator + 484 BACKUP_DIRECTORY_DESCRIPTOR_FILE; 485 File descriptorFile = new File(descriptorFilePath); 486 if (! descriptorFile.exists()) 487 { 488 Message message = ERR_BACKUPDIRECTORY_NO_DESCRIPTOR_FILE.get( 489 descriptorFilePath); 490 throw new ConfigException(message); 491 } 492 493 494 // Open the file for reading. The first line should be the DN of 495 // the associated configuration entry. 496 BufferedReader reader = 497 new BufferedReader(new FileReader(descriptorFile)); 498 String line = reader.readLine(); 499 if ((line == null) || (line.length() == 0)) 500 { 501 Message message = 502 ERR_BACKUPDIRECTORY_CANNOT_READ_CONFIG_ENTRY_DN. 503 get(descriptorFilePath); 504 throw new ConfigException(message); 505 } 506 else if (! line.startsWith(PROPERTY_BACKEND_CONFIG_DN)) 507 { 508 Message message = ERR_BACKUPDIRECTORY_FIRST_LINE_NOT_DN.get( 509 descriptorFilePath, line); 510 throw new ConfigException(message); 511 } 512 513 String dnString = 514 line.substring(PROPERTY_BACKEND_CONFIG_DN.length() + 1); 515 DN configEntryDN; 516 try 517 { 518 configEntryDN = DN.decode(dnString); 519 } 520 catch (DirectoryException de) 521 { 522 Message message = ERR_BACKUPDIRECTORY_CANNOT_DECODE_DN.get( 523 dnString, descriptorFilePath, de.getMessageObject()); 524 throw new ConfigException(message, de); 525 } 526 catch (Exception e) 527 { 528 Message message = ERR_BACKUPDIRECTORY_CANNOT_DECODE_DN.get( 529 dnString, descriptorFilePath, getExceptionMessage(e)); 530 throw new ConfigException(message, e); 531 } 532 533 534 // Create the backup directory structure from what we know so far. 535 BackupDirectory backupDirectory = 536 new BackupDirectory(path, configEntryDN); 537 538 539 // Iterate through the rest of the file and create the backup info 540 // structures. Blank lines will be considered delimiters. 541 LinkedList<String> lines = new LinkedList<String>(); 542 while (true) 543 { 544 line = reader.readLine(); 545 if ((line == null) || (line.length() == 0)) 546 { 547 // It's a blank line or the end of the file. If we have lines 548 // to process then do so. Otherwise, move on. 549 if (lines.isEmpty()) 550 { 551 if (line == null) 552 { 553 break; 554 } 555 else 556 { 557 continue; 558 } 559 } 560 561 562 // Parse the lines that we read and add the backup info to the 563 // directory structure. 564 BackupInfo backupInfo = BackupInfo.decode(backupDirectory, 565 lines); 566 backupDirectory.addBackup(backupInfo); 567 lines.clear(); 568 569 570 // If it was the end of the file, then break out of the loop. 571 if (line == null) 572 { 573 break; 574 } 575 } 576 else 577 { 578 lines.add(line); 579 } 580 } 581 582 583 // Close the reader and return the backup directory structure. 584 reader.close(); 585 return backupDirectory; 586 } 587 } 588