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 legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2006-2008 Sun Microsystems, Inc.
025 *      Portions Copyright 2012-2015 ForgeRock AS.
026 */
027package org.opends.quicksetup.util;
028
029import java.io.*;
030
031import org.forgerock.i18n.LocalizableMessage;
032import org.forgerock.i18n.slf4j.LocalizedLogger;
033import org.opends.quicksetup.*;
034import org.opends.server.util.StaticUtils;
035
036import static org.opends.messages.QuickSetupMessages.*;
037import static com.forgerock.opendj.util.OperatingSystem.isUnix;
038
039/**
040 * Utility class for use by applications containing methods for managing
041 * file system files.  This class handles application notifications for
042 * interesting events.
043 */
044public class FileManager {
045
046  /**
047   * Describes the approach taken to deleting a file or directory.
048   */
049  public enum DeletionPolicy {
050
051    /**
052     * Delete the file or directory immediately.
053     */
054    DELETE_IMMEDIATELY,
055
056    /**
057     * Mark the file or directory for deletion after the JVM has exited.
058     */
059    DELETE_ON_EXIT,
060
061    /**
062     * First try to delete the file immediately.  If the deletion was
063     * unsuccessful mark the file for deleteion when the JVM has
064     * existed.
065     */
066    DELETE_ON_EXIT_IF_UNSUCCESSFUL
067
068  }
069
070  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
071
072  private Application application;
073
074  /**
075   * Creates a new file manager.
076   */
077  public FileManager() {
078    // do nothing;
079  }
080
081  /**
082   * Creates a new file manager.
083   * @param app Application managing files to which progress notifications
084   * will be sent
085   */
086  public FileManager(Application app) {
087    this.application = app;
088  }
089
090  /**
091   * Recursively copies any files or directories appearing in
092   * <code>source</code> or a subdirectory of <code>source</code>
093   * to the corresponding directory under <code>target</code>.  Files
094   * in under <code>source</code> are not copied to <code>target</code>
095   * if a file by the same name already exists in <code>target</code>.
096   *
097   * @param source source directory
098   * @param target target directory
099   * @throws ApplicationException if there is a problem copying files
100   */
101  public void synchronize(File source, File target)
102          throws ApplicationException
103  {
104    if (source != null && target != null) {
105      String[] sourceFileNames = source.list();
106      if (sourceFileNames != null) {
107        for (String sourceFileName : sourceFileNames) {
108          File sourceFile = new File(source, sourceFileName);
109          copyRecursively(sourceFile, target, null, false);
110        }
111      }
112    }
113  }
114
115  /**
116   * Renames the source file to the target file.  If the target file exists
117   * it is first deleted.  The rename and delete operation return values
118   * are checked for success and if unsuccessful, this method throws an
119   * exception.
120   *
121   * @param fileToRename The file to rename.
122   * @param target       The file to which <code>fileToRename</code> will be
123   *                     moved.
124   * @throws ApplicationException If a problem occurs while attempting to rename
125   *                     the file.  On the Windows platform, this typically
126   *                     indicates that the file is in use by this or another
127   *                     application.
128   */
129  public void rename(File fileToRename, File target)
130          throws ApplicationException {
131    if (fileToRename != null && target != null) {
132      synchronized (target) {
133        if (target.exists() && !target.delete())
134        {
135          throw new ApplicationException(
136                  ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
137                  INFO_ERROR_DELETING_FILE.get(Utils.getPath(target)), null);
138        }
139      }
140      if (!fileToRename.renameTo(target)) {
141        throw new ApplicationException(
142                ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
143                INFO_ERROR_RENAMING_FILE.get(Utils.getPath(fileToRename),
144                        Utils.getPath(target)), null);
145      }
146    }
147  }
148
149
150  /**
151   * Move a file.
152   * @param object File to move
153   * @param newParent File representing new parent directory
154   * @throws ApplicationException if something goes wrong
155   */
156  public void move(File object, File newParent)
157          throws ApplicationException
158  {
159    move(object, newParent, null);
160  }
161
162  /**
163   * Move a file.
164   * @param object File to move
165   * @param newParent File representing new parent directory
166   * @param filter that will be asked whether or not the operation should be
167   *        performed
168   * @throws ApplicationException if something goes wrong
169   */
170  public void move(File object, File newParent, FileFilter filter)
171          throws ApplicationException
172  {
173    // TODO: application notification
174    if (filter == null || filter.accept(object)) {
175      new MoveOperation(object, newParent).apply();
176    }
177  }
178
179  /**
180   * Deletes a single file or directory.
181   * @param object File to delete
182   * @throws ApplicationException if something goes wrong
183   */
184  public void delete(File object)
185          throws ApplicationException
186  {
187    delete(object, null);
188  }
189
190  /**
191   * Deletes a single file or directory.
192   * @param object File to delete
193   * @param filter that will be asked whether or not the operation should be
194   *        performed
195   * @throws ApplicationException if something goes wrong
196   */
197  public void delete(File object, FileFilter filter)
198          throws ApplicationException
199  {
200    if (filter == null || filter.accept(object)) {
201      new DeleteOperation(object, DeletionPolicy.DELETE_IMMEDIATELY).apply();
202    }
203  }
204
205  /**
206   * Deletes the children of a directory.
207   *
208   * @param parentDir the directory whose children is deleted
209   * @throws ApplicationException if there is a problem deleting children
210   */
211  public void deleteChildren(File parentDir) throws ApplicationException {
212    if (parentDir != null && parentDir.exists() && parentDir.isDirectory()) {
213      File[] children = parentDir.listFiles();
214      if (children != null) {
215        for (File child : children) {
216          deleteRecursively(child);
217        }
218      }
219    }
220  }
221
222  /**
223   * Deletes everything below the specified file.
224   *
225   * @param file the path to be deleted.
226   * @throws org.opends.quicksetup.ApplicationException if something goes wrong.
227   */
228  public void deleteRecursively(File file) throws ApplicationException {
229    deleteRecursively(file, null,
230            FileManager.DeletionPolicy.DELETE_IMMEDIATELY);
231  }
232
233  /**
234   * Deletes everything below the specified file.
235   *
236   * @param file   the path to be deleted.
237   * @param filter the filter of the files to know if the file can be deleted
238   *               directly or not.
239   * @param deletePolicy describes how deletions are to be made
240   *        JVM exits rather than deleting the files immediately.
241   * @throws ApplicationException if something goes wrong.
242   */
243  public void deleteRecursively(File file, FileFilter filter,
244                                DeletionPolicy deletePolicy)
245          throws ApplicationException {
246    operateRecursively(new DeleteOperation(file, deletePolicy), filter);
247  }
248
249  /**
250   * Copies everything below the specified file.
251   *
252   * @param objectFile   the file to be copied.
253   * @param destDir      the directory to copy the file to
254   * @return File representing the destination
255   * @throws ApplicationException if something goes wrong.
256   */
257  public File copy(File objectFile, File destDir)
258          throws ApplicationException
259  {
260    CopyOperation co = new CopyOperation(objectFile, destDir, false);
261    co.apply();
262    return co.getDestination();
263  }
264
265  /**
266   * Copies everything below the specified file.
267   *
268   * @param objectFile   the file to be copied.
269   * @param destDir      the directory to copy the file to
270   * @param overwrite    overwrite destination files.
271   * @return File representing the destination
272   * @throws ApplicationException if something goes wrong.
273   */
274  public File copy(File objectFile, File destDir, boolean overwrite)
275          throws ApplicationException
276  {
277    CopyOperation co = new CopyOperation(objectFile, destDir, overwrite);
278    co.apply();
279    return co.getDestination();
280  }
281
282  /**
283   * Copies everything below the specified file.
284   *
285   * @param objectFile   the file to be copied.
286   * @param destDir      the directory to copy the file to
287   * @throws ApplicationException if something goes wrong.
288   */
289  public void copyRecursively(File objectFile, File destDir)
290          throws ApplicationException
291  {
292    copyRecursively(objectFile, destDir, null);
293  }
294
295  /**
296   * Copies everything below the specified file.
297   *
298   * @param objectFile   the file to be copied.
299   * @param destDir      the directory to copy the file to
300   * @param filter the filter of the files to know if the file can be copied
301   *               directly or not.
302   * @throws ApplicationException if something goes wrong.
303   */
304  public void copyRecursively(File objectFile, File destDir, FileFilter filter)
305          throws ApplicationException {
306    copyRecursively(objectFile, destDir, filter, false);
307  }
308
309  /**
310   * Copies everything below the specified file.
311   *
312   * @param objectFile   the file to be copied.
313   * @param destDir      the directory to copy the file to
314   * @param filter the filter of the files to know if the file can be copied
315   *               directly or not.
316   * @param overwrite    overwrite destination files.
317   * @throws ApplicationException if something goes wrong.
318   */
319  public void copyRecursively(File objectFile, File destDir,
320                              FileFilter filter, boolean overwrite)
321          throws ApplicationException {
322    operateRecursively(new CopyOperation(objectFile, destDir, overwrite),
323            filter);
324  }
325
326 /**
327  * Determines whether or not two files differ in content.
328  *
329  * @param f1 file to compare
330  * @param f2 file to compare
331  * @return boolean where true indicates that two files differ
332  * @throws IOException if there is a problem reading the files' conents
333  */
334 public boolean filesDiffer(File f1, File f2) throws IOException {
335   boolean differ = false;
336   FileReader fr1 = new FileReader(f1);
337   FileReader fr2 = new FileReader(f2);
338   try {
339     boolean done = false;
340     while (!differ && !done) {
341       int c1 = fr1.read();
342       int c2 = fr2.read();
343       differ = c1 != c2;
344       done = c1 == -1 || c2 == -1;
345     }
346   } finally {
347     fr1.close();
348     fr2.close();
349   }
350   return differ;
351 }
352
353  private void operateRecursively(FileOperation op, FileFilter filter)
354          throws ApplicationException {
355    File file = op.getObjectFile();
356    if (file.exists()) {
357      if (file.isFile()) {
358        if (filter != null) {
359          if (filter.accept(file)) {
360            op.apply();
361          }
362        } else {
363          op.apply();
364        }
365      } else {
366        File[] children = file.listFiles();
367        if (children != null) {
368          for (File aChildren : children) {
369            FileOperation newOp = op.copyForChild(aChildren);
370            operateRecursively(newOp, filter);
371          }
372        }
373        if (filter != null) {
374          if (filter.accept(file)) {
375            op.apply();
376          }
377        } else {
378          op.apply();
379        }
380      }
381    } else {
382      // Just tell that the file/directory does not exist.
383      if (application != null) {
384        application.notifyListeners(application.getFormattedWarning(
385                INFO_FILE_DOES_NOT_EXIST.get(file)));
386      }
387      logger.info(LocalizableMessage.raw("file '" + file + "' does not exist"));
388    }
389  }
390
391  /**
392   * A file operation.
393   */
394  private abstract class FileOperation {
395
396    private File objectFile;
397
398    /**
399     * Creates a new file operation.
400     * @param objectFile to be operated on
401     */
402    public FileOperation(File objectFile) {
403      this.objectFile = objectFile;
404    }
405
406    /**
407     * Gets the file to be operated on.
408     * @return File to be operated on
409     */
410    protected File getObjectFile() {
411      return objectFile;
412    }
413
414    /**
415     * Make a copy of this class for the child file.
416     * @param child to act as the new file object
417     * @return FileOperation as the same type as this class
418     */
419    public abstract FileOperation copyForChild(File child);
420
421    /**
422     * Execute this operation.
423     * @throws ApplicationException if there is a problem.
424     */
425    public abstract void apply() throws ApplicationException;
426
427  }
428
429  /**
430   * A copy operation.
431   */
432  private class CopyOperation extends FileOperation {
433
434    private File destination;
435
436    private boolean overwrite;
437
438    /**
439     * Create a new copy operation.
440     * @param objectFile to copy
441     * @param destDir to copy to
442     * @param overwrite if true copy should overwrite any existing file
443     */
444    public CopyOperation(File objectFile, File destDir, boolean overwrite) {
445      super(objectFile);
446      this.destination = new File(destDir, objectFile.getName());
447      this.overwrite = overwrite;
448    }
449
450    /** {@inheritDoc} */
451    @Override
452    public FileOperation copyForChild(File child) {
453      return new CopyOperation(child, destination, overwrite);
454    }
455
456    /**
457     * Returns the destination file that is the result of copying
458     * <code>objectFile</code> to <code>destDir</code>.
459     * @return The destination file.
460     */
461    public File getDestination() {
462      return this.destination;
463    }
464
465    /** {@inheritDoc} */
466    @Override
467    public void apply() throws ApplicationException {
468      File objectFile = getObjectFile();
469      if (objectFile.isDirectory()) {
470        if (!destination.exists()) {
471          destination.mkdirs();
472        }
473      } else {
474
475        // If overwriting and the destination exists then kill it
476        if (destination.exists() && overwrite) {
477          deleteRecursively(destination);
478        }
479
480        if (!destination.exists()) {
481          if (Utils.insureParentsExist(destination)) {
482            if (application != null && application.isVerbose()) {
483              application.notifyListeners(application.getFormattedWithPoints(
484                      INFO_PROGRESS_COPYING_FILE.get(
485                              objectFile.getAbsolutePath(),
486                              destination.getAbsolutePath())));
487            }
488            logger.info(LocalizableMessage.raw("copying file '" +
489                    objectFile.getAbsolutePath() + "' to '" +
490                    destination.getAbsolutePath() + "'"));
491            FileInputStream fis = null;
492            FileOutputStream fos = null;
493            try {
494              fis = new FileInputStream(objectFile);
495              fos = new FileOutputStream(destination);
496              byte[] buf = new byte[1024];
497              int i;
498              while ((i = fis.read(buf)) != -1) {
499                fos.write(buf, 0, i);
500              }
501              if (destination.exists() && isUnix()) {
502                // TODO:  set the file's permissions.  This is made easier in
503                // Java 1.6 but until then use the TestUtilities methods
504                String permissions = Utils.getFileSystemPermissions(objectFile);
505                Utils.setPermissionsUnix(Utils.getPath(destination), permissions);
506              }
507
508              if (application != null && application.isVerbose()) {
509                application.notifyListeners(
510                        application.getFormattedDoneWithLineBreak());
511              }
512
513            } catch (Exception e) {
514              LocalizableMessage errMsg = INFO_ERROR_COPYING_FILE.get(
515                      objectFile.getAbsolutePath(),
516                      destination.getAbsolutePath());
517              throw new ApplicationException(
518                      ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
519                      errMsg, null);
520            } finally {
521              StaticUtils.close(fis, fos);
522            }
523          } else {
524            LocalizableMessage errMsg = INFO_ERROR_COPYING_FILE.get(
525                    objectFile.getAbsolutePath(),
526                    destination.getAbsolutePath());
527            throw new ApplicationException(
528                    ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
529                    errMsg, null);
530          }
531        } else {
532          logger.info(LocalizableMessage.raw("Ignoring file '" +
533                  objectFile.getAbsolutePath() + "' since '" +
534                  destination.getAbsolutePath() + "' already exists"));
535          if (application != null && application.isVerbose()) {
536            application.notifyListeners(
537                    INFO_INFO_IGNORING_FILE.get(
538                                    objectFile.getAbsolutePath(),
539                                    destination.getAbsolutePath()));
540            application.notifyListeners(application.getLineBreak());
541          }
542        }
543      }
544    }
545
546  }
547
548  /**
549   * A delete operation.
550   */
551  private class DeleteOperation extends FileOperation {
552
553    private DeletionPolicy deletionPolicy;
554
555    /**
556     * Creates a delete operation.
557     * @param objectFile to delete
558     * @param deletionPolicy describing how files will be deleted
559     * is to take place after this program exists.  This is useful
560     * for cleaning up files that are currently in use.
561     */
562    public DeleteOperation(File objectFile, DeletionPolicy deletionPolicy) {
563      super(objectFile);
564      this.deletionPolicy = deletionPolicy;
565    }
566
567    /** {@inheritDoc} */
568    @Override
569    public FileOperation copyForChild(File child) {
570      return new DeleteOperation(child, deletionPolicy);
571    }
572
573    /** {@inheritDoc} */
574    @Override
575    public void apply() throws ApplicationException {
576      File file = getObjectFile();
577      boolean isFile = file.isFile();
578
579      if (application != null && application.isVerbose()) {
580        if (isFile) {
581          application.notifyListeners(application.getFormattedWithPoints(
582                  INFO_PROGRESS_DELETING_FILE.get(file.getAbsolutePath())));
583        } else {
584          application.notifyListeners(application.getFormattedWithPoints(
585                  INFO_PROGRESS_DELETING_DIRECTORY.get(
586                          file.getAbsolutePath())));
587        }
588      }
589      logger.info(LocalizableMessage.raw("deleting " +
590              (isFile ? " file " : " directory ") +
591              file.getAbsolutePath()));
592
593      boolean delete = false;
594      /*
595       * Sometimes the server keeps some locks on the files.
596       * TODO: remove this code once stop-ds returns properly when server
597       * is stopped.
598       */
599      int nTries = 5;
600      for (int i = 0; i < nTries && !delete; i++) {
601        if (DeletionPolicy.DELETE_ON_EXIT.equals(deletionPolicy)) {
602          file.deleteOnExit();
603          delete = true;
604        } else {
605          delete = file.delete();
606          if (!delete && DeletionPolicy.DELETE_ON_EXIT_IF_UNSUCCESSFUL.
607                  equals(deletionPolicy)) {
608            file.deleteOnExit();
609            delete = true;
610          }
611        }
612        if (!delete) {
613          try {
614            Thread.sleep(1000);
615          }
616          catch (Exception ex) {
617            // do nothing;
618          }
619        }
620      }
621
622      if (!delete) {
623        LocalizableMessage errMsg;
624        if (isFile) {
625          errMsg = INFO_ERROR_DELETING_FILE.get(file.getAbsolutePath());
626        } else {
627          errMsg = INFO_ERROR_DELETING_DIRECTORY.get(file.getAbsolutePath());
628        }
629        throw new ApplicationException(
630                ReturnCode.FILE_SYSTEM_ACCESS_ERROR,
631                errMsg, null);
632      }
633
634      if (application != null && application.isVerbose()) {
635        application.notifyListeners(
636                application.getFormattedDoneWithLineBreak());
637      }
638    }
639  }
640
641  /**
642   * A delete operation.
643   */
644  private class MoveOperation extends FileOperation {
645
646    File destination;
647
648    /**
649     * Creates a delete operation.
650     * @param objectFile to delete
651     * @param newParent Filr where <code>objectFile</code> will be copied.
652     */
653    public MoveOperation(File objectFile, File newParent) {
654      super(objectFile);
655      this.destination = new File(newParent, objectFile.getName());
656    }
657
658    /** {@inheritDoc} */
659    @Override
660    public FileOperation copyForChild(File child) {
661      return new MoveOperation(child, destination);
662    }
663
664    /** {@inheritDoc} */
665    @Override
666    public void apply() throws ApplicationException {
667      File objectFile = getObjectFile();
668      if (destination.exists()) {
669        deleteRecursively(destination);
670      }
671      if (!objectFile.renameTo(destination)) {
672        throw ApplicationException.createFileSystemException(
673                INFO_ERROR_FAILED_MOVING_FILE.get(Utils.getPath(objectFile),
674                        Utils.getPath(destination)),
675                null);
676      }
677    }
678  }
679
680}