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}