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 java.io.BufferedReader; 020import java.io.BufferedWriter; 021import java.io.File; 022import java.io.FileReader; 023import java.io.FileWriter; 024import java.io.IOException; 025import java.util.LinkedHashMap; 026import java.util.LinkedList; 027import java.util.List; 028import java.util.Map; 029 030import org.forgerock.i18n.LocalizableMessage; 031import org.forgerock.i18n.LocalizedIllegalArgumentException; 032import org.forgerock.i18n.slf4j.LocalizedLogger; 033import org.forgerock.opendj.config.server.ConfigException; 034import org.forgerock.opendj.ldap.DN; 035 036import static org.opends.messages.CoreMessages.*; 037import static org.opends.server.util.ServerConstants.*; 038import static org.opends.server.util.StaticUtils.*; 039 040/** 041 * This class defines a data structure for holding information about a 042 * filesystem directory that contains data for one or more backups associated 043 * with a backend. Only backups for a single backend may be placed in any given 044 * directory. 045 */ 046@org.opends.server.types.PublicAPI( 047 stability = org.opends.server.types.StabilityLevel.VOLATILE, 048 mayInstantiate = true, 049 mayExtend = false, 050 mayInvoke = true) 051public final class BackupDirectory 052{ 053 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 054 055 /** 056 * The name of the property that will be used to provide the DN of 057 * the configuration entry for the backend associated with the 058 * backups in this directory. 059 */ 060 private static final String PROPERTY_BACKEND_CONFIG_DN = "backend_dn"; 061 062 /** 063 * The DN of the configuration entry for the backend with which this 064 * backup directory is associated. 065 */ 066 private final DN configEntryDN; 067 068 /** 069 * The set of backups in the specified directory. The iteration 070 * order will be the order in which the backups were created. 071 */ 072 private final Map<String, BackupInfo> backups; 073 074 /** The filesystem path to the backup directory. */ 075 private final String path; 076 077 /** 078 * Creates a new backup directory object with the provided information. 079 * 080 * @param path 081 * The path to the directory containing the backup file(s). 082 * @param configEntryDN 083 * The DN of the configuration entry for the backend with which this 084 * backup directory is associated. 085 */ 086 public BackupDirectory(String path, DN configEntryDN) 087 { 088 this(path, configEntryDN, null); 089 } 090 091 /** 092 * Creates a new backup directory object with the provided information. 093 * 094 * @param path 095 * The path to the directory containing the backup file(s). 096 * @param configEntryDN 097 * The DN of the configuration entry for the backend with which this 098 * backup directory is associated. 099 * @param backups 100 * Information about the set of backups available within the 101 * specified directory. 102 */ 103 private BackupDirectory(String path, DN configEntryDN, LinkedHashMap<String, BackupInfo> backups) 104 { 105 this.path = path; 106 this.configEntryDN = configEntryDN; 107 108 if (backups != null) 109 { 110 this.backups = backups; 111 } 112 else 113 { 114 this.backups = new LinkedHashMap<>(); 115 } 116 } 117 118 /** 119 * Retrieves the path to the directory containing the backup file(s). 120 * 121 * @return The path to the directory containing the backup file(s). 122 */ 123 public String getPath() 124 { 125 return path; 126 } 127 128 /** 129 * Retrieves the DN of the configuration entry for the backend with which this 130 * backup directory is associated. 131 * 132 * @return The DN of the configuration entry for the backend with which this 133 * backup directory is associated. 134 */ 135 public DN getConfigEntryDN() 136 { 137 return configEntryDN; 138 } 139 140 /** 141 * Retrieves the set of backups in this backup directory, as a mapping between 142 * the backup ID and the associated backup info. The iteration order for the 143 * map will be the order in which the backups were created. 144 * 145 * @return The set of backups in this backup directory. 146 */ 147 public Map<String, BackupInfo> getBackups() 148 { 149 return backups; 150 } 151 152 /** 153 * Retrieves the backup info structure for the backup with the specified ID. 154 * 155 * @param backupID 156 * The backup ID for the structure to retrieve. 157 * @return The requested backup info structure, or <CODE>null</CODE> if no such 158 * structure exists. 159 */ 160 public BackupInfo getBackupInfo(String backupID) 161 { 162 return backups.get(backupID); 163 } 164 165 /** 166 * Retrieves the most recent backup for this backup directory, according to 167 * the backup date. 168 * 169 * @return The most recent backup for this backup directory, according to the 170 * backup date, or <CODE>null</CODE> if there are no backups in the 171 * backup directory. 172 */ 173 public BackupInfo getLatestBackup() 174 { 175 BackupInfo latestBackup = null; 176 for (BackupInfo backup : backups.values()) 177 { 178 if (latestBackup == null 179 || backup.getBackupDate().getTime() > latestBackup.getBackupDate().getTime()) 180 { 181 latestBackup = backup; 182 } 183 } 184 185 return latestBackup; 186 } 187 188 /** 189 * Adds information about the provided backup to this backup directory. 190 * 191 * @param backupInfo 192 * The backup info structure for the backup to be added. 193 * @throws ConfigException 194 * If another backup already exists with the same backup ID. 195 */ 196 public void addBackup(BackupInfo backupInfo) throws ConfigException 197 { 198 String backupID = backupInfo.getBackupID(); 199 if (backups.containsKey(backupID)) 200 { 201 throw new ConfigException(ERR_BACKUPDIRECTORY_ADD_DUPLICATE_ID.get(backupID, path)); 202 } 203 backups.put(backupID, backupInfo); 204 } 205 206 /** 207 * Removes the backup with the specified backup ID from this backup directory. 208 * 209 * @param backupID 210 * The backup ID for the backup to remove from this backup directory. 211 * @throws ConfigException 212 * If it is not possible to remove the requested backup for some 213 * reason (e.g., no such backup exists, or another backup is 214 * dependent on it). 215 */ 216 public void removeBackup(String backupID) throws ConfigException 217 { 218 if (!backups.containsKey(backupID)) 219 { 220 throw new ConfigException(ERR_BACKUPDIRECTORY_NO_SUCH_BACKUP.get(backupID, path)); 221 } 222 223 for (BackupInfo backup : backups.values()) 224 { 225 if (backup.dependsOn(backupID)) 226 { 227 throw new ConfigException(ERR_BACKUPDIRECTORY_UNRESOLVED_DEPENDENCY.get(backupID, path, backup.getBackupID())); 228 } 229 } 230 231 backups.remove(backupID); 232 } 233 234 /** 235 * Retrieves a path to the backup descriptor file that should be used for this 236 * backup directory. 237 * 238 * @return A path to the backup descriptor file that should be used for this 239 * backup directory. 240 */ 241 public String getDescriptorPath() 242 { 243 return path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE; 244 } 245 246 /** 247 * Writes the descriptor with the information contained in this structure to 248 * disk in the appropriate directory. 249 * 250 * @throws IOException 251 * If a problem occurs while writing to disk. 252 */ 253 public void writeBackupDirectoryDescriptor() throws IOException 254 { 255 // First make sure that the target directory exists. If it doesn't, then try to create it. 256 createDirectoryIfNotExists(); 257 258 // We'll write to a temporary file so that we won't destroy the live copy if a problem occurs. 259 String newDescriptorFilePath = path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE + ".new"; 260 File newDescriptorFile = new File(newDescriptorFilePath); 261 try (BufferedWriter writer = new BufferedWriter(new FileWriter(newDescriptorFile, false))) 262 { 263 // The first line in the file will only contain the DN of the configuration entry for the associated backend. 264 writer.write(PROPERTY_BACKEND_CONFIG_DN + "=" + configEntryDN); 265 writer.newLine(); 266 writer.newLine(); 267 268 // Iterate through all of the backups and add them to the file. 269 for (BackupInfo backup : backups.values()) 270 { 271 for (String line : backup.encode()) 272 { 273 writer.write(line); 274 writer.newLine(); 275 } 276 277 writer.newLine(); 278 } 279 280 // At this point, the file should be complete so flush and close it. 281 writer.flush(); 282 } 283 284 // If previous backup descriptor file exists, then rename it. 285 String descriptorFilePath = path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE; 286 File descriptorFile = new File(descriptorFilePath); 287 renameOldBackupDescriptorFile(descriptorFile, descriptorFilePath); 288 289 // Rename the new descriptor file to match the previous one. 290 try 291 { 292 newDescriptorFile.renameTo(descriptorFile); 293 } 294 catch (Exception e) 295 { 296 logger.traceException(e); 297 LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_RENAME_NEW_DESCRIPTOR.get( 298 newDescriptorFilePath, descriptorFilePath, getExceptionMessage(e)); 299 throw new IOException(message.toString()); 300 } 301 } 302 303 private void createDirectoryIfNotExists() throws IOException 304 { 305 File dir = new File(path); 306 if (!dir.exists()) 307 { 308 try 309 { 310 dir.mkdirs(); 311 } 312 catch (Exception e) 313 { 314 logger.traceException(e); 315 LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_CREATE_DIRECTORY.get(path, getExceptionMessage(e)); 316 throw new IOException(message.toString()); 317 } 318 } 319 else if (!dir.isDirectory()) 320 { 321 throw new IOException(ERR_BACKUPDIRECTORY_NOT_DIRECTORY.get(path).toString()); 322 } 323 } 324 325 private void renameOldBackupDescriptorFile(File descriptorFile, String descriptorFilePath) throws IOException 326 { 327 if (descriptorFile.exists()) 328 { 329 String savedDescriptorFilePath = descriptorFilePath + ".save"; 330 File savedDescriptorFile = new File(savedDescriptorFilePath); 331 if (savedDescriptorFile.exists()) 332 { 333 try 334 { 335 savedDescriptorFile.delete(); 336 } 337 catch (Exception e) 338 { 339 logger.traceException(e); 340 LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_DELETE_SAVED_DESCRIPTOR.get( 341 savedDescriptorFilePath, getExceptionMessage(e), descriptorFilePath, descriptorFilePath); 342 throw new IOException(message.toString()); 343 } 344 } 345 346 try 347 { 348 descriptorFile.renameTo(savedDescriptorFile); 349 } 350 catch (Exception e) 351 { 352 logger.traceException(e); 353 LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_RENAME_CURRENT_DESCRIPTOR.get(descriptorFilePath, 354 savedDescriptorFilePath, getExceptionMessage(e), descriptorFilePath, descriptorFilePath); 355 throw new IOException(message.toString()); 356 } 357 } 358 } 359 360 /** 361 * Reads the backup descriptor file in the specified path and uses the 362 * information it contains to create a new backup directory structure. 363 * 364 * @param path 365 * The path to the directory containing the backup descriptor file to 366 * read. 367 * @return The backup directory structure created from the contents of the 368 * descriptor file. 369 * @throws IOException 370 * If a problem occurs while trying to read the contents of the 371 * descriptor file. 372 * @throws ConfigException 373 * If the contents of the descriptor file cannot be parsed to create 374 * a backup directory structure. 375 */ 376 public static BackupDirectory readBackupDirectoryDescriptor(String path) throws IOException, ConfigException 377 { 378 // Make sure that the descriptor file exists. 379 String descriptorFilePath = path + File.separator + BACKUP_DIRECTORY_DESCRIPTOR_FILE; 380 if (!new File(descriptorFilePath).exists()) 381 { 382 throw new ConfigException(ERR_BACKUPDIRECTORY_NO_DESCRIPTOR_FILE.get(descriptorFilePath)); 383 } 384 385 // Open the file for reading. 386 // The first line should be the DN of the associated configuration entry. 387 try (BufferedReader reader = new BufferedReader(new FileReader(descriptorFilePath))) 388 { 389 String line = reader.readLine(); 390 if (line == null || line.length() == 0) 391 { 392 throw new ConfigException(ERR_BACKUPDIRECTORY_CANNOT_READ_CONFIG_ENTRY_DN.get(descriptorFilePath)); 393 } 394 else if (!line.startsWith(PROPERTY_BACKEND_CONFIG_DN)) 395 { 396 throw new ConfigException(ERR_BACKUPDIRECTORY_FIRST_LINE_NOT_DN.get(descriptorFilePath, line)); 397 } 398 399 String dnString = line.substring(PROPERTY_BACKEND_CONFIG_DN.length() + 1); 400 DN configEntryDN; 401 try 402 { 403 configEntryDN = DN.valueOf(dnString); 404 } 405 catch (LocalizedIllegalArgumentException e) 406 { 407 LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_DECODE_DN.get( 408 dnString, descriptorFilePath, e.getMessageObject()); 409 throw new ConfigException(message, e); 410 } 411 catch (Exception e) 412 { 413 LocalizableMessage message = ERR_BACKUPDIRECTORY_CANNOT_DECODE_DN.get( 414 dnString, descriptorFilePath, getExceptionMessage(e)); 415 throw new ConfigException(message, e); 416 } 417 418 // Create the backup directory structure from what we know so far. 419 BackupDirectory backupDirectory = new BackupDirectory(path, configEntryDN); 420 421 // Iterate through the rest of the file and create the backup info structures. 422 // Blank lines will be considered delimiters. 423 List<String> lines = new LinkedList<>(); 424 while ((line = reader.readLine()) != null) 425 { 426 if (!line.isEmpty()) 427 { 428 lines.add(line); 429 continue; 430 } 431 432 // We are on a delimiter blank line. 433 readBackupFromLines(backupDirectory, lines); 434 } 435 readBackupFromLines(backupDirectory, lines); 436 437 return backupDirectory; 438 } 439 } 440 441 private static void readBackupFromLines(BackupDirectory backupDirectory, List<String> lines) throws ConfigException 442 { 443 if (!lines.isEmpty()) 444 { 445 backupDirectory.addBackup(BackupInfo.decode(backupDirectory, lines)); 446 lines.clear(); 447 } 448 } 449}