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