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