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