FileAlterationObserver.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.io.monitor;

  18. import java.io.File;
  19. import java.io.FileFilter;
  20. import java.io.IOException;
  21. import java.io.Serializable;
  22. import java.util.ArrayList;
  23. import java.util.Arrays;
  24. import java.util.Comparator;
  25. import java.util.List;
  26. import java.util.Objects;
  27. import java.util.concurrent.CopyOnWriteArrayList;
  28. import java.util.stream.Stream;

  29. import org.apache.commons.io.FileUtils;
  30. import org.apache.commons.io.IOCase;
  31. import org.apache.commons.io.build.AbstractOrigin;
  32. import org.apache.commons.io.build.AbstractOriginSupplier;
  33. import org.apache.commons.io.comparator.NameFileComparator;
  34. import org.apache.commons.io.filefilter.TrueFileFilter;

  35. /**
  36.  * FileAlterationObserver represents the state of files below a root directory, checking the file system and notifying listeners of create, change or delete
  37.  * events.
  38.  * <p>
  39.  * To use this implementation:
  40.  * </p>
  41.  * <ul>
  42.  * <li>Create {@link FileAlterationListener} implementation(s) that process the file/directory create, change and delete events</li>
  43.  * <li>Register the listener(s) with a {@link FileAlterationObserver} for the appropriate directory.</li>
  44.  * <li>Either register the observer(s) with a {@link FileAlterationMonitor} or run manually.</li>
  45.  * </ul>
  46.  * <h2>Basic Usage</h2> Create a {@link FileAlterationObserver} for the directory and register the listeners:
  47.  *
  48.  * <pre>
  49.  *      File directory = new File(FileUtils.current(), "src");
  50.  *      FileAlterationObserver observer = new FileAlterationObserver(directory);
  51.  *      observer.addListener(...);
  52.  *      observer.addListener(...);
  53.  * </pre>
  54.  * <p>
  55.  * To manually observe a directory, initialize the observer and invoked the {@link #checkAndNotify()} method as required:
  56.  * </p>
  57.  *
  58.  * <pre>
  59.  *      // initialize
  60.  *      observer.init();
  61.  *      ...
  62.  *      // invoke as required
  63.  *      observer.checkAndNotify();
  64.  *      ...
  65.  *      observer.checkAndNotify();
  66.  *      ...
  67.  *      // finished
  68.  *      observer.finish();
  69.  * </pre>
  70.  * <p>
  71.  * Alternatively, register the observer(s) with a {@link FileAlterationMonitor}, which creates a new thread, invoking the observer at the specified interval:
  72.  * </p>
  73.  *
  74.  * <pre>
  75.  *      long interval = ...
  76.  *      FileAlterationMonitor monitor = new FileAlterationMonitor(interval);
  77.  *      monitor.addObserver(observer);
  78.  *      monitor.start();
  79.  *      ...
  80.  *      monitor.stop();
  81.  * </pre>
  82.  *
  83.  * <h2>File Filters</h2> This implementation can monitor portions of the file system by using {@link FileFilter}s to observe only the files and/or directories
  84.  * that are of interest. This makes it more efficient and reduces the noise from <em>unwanted</em> file system events.
  85.  * <p>
  86.  * <a href="https://commons.apache.org/io/">Commons IO</a> has a good range of useful, ready-made <a href="../filefilter/package-summary.html">File Filter</a>
  87.  * implementations for this purpose.
  88.  * </p>
  89.  * <p>
  90.  * For example, to only observe 1) visible directories and 2) files with a ".java" suffix in a root directory called "src" you could set up a
  91.  * {@link FileAlterationObserver} in the following way:
  92.  * </p>
  93.  *
  94.  * <pre>
  95.  *      // Create a FileFilter
  96.  *      IOFileFilter directories = FileFilterUtils.and(
  97.  *                                      FileFilterUtils.directoryFileFilter(),
  98.  *                                      HiddenFileFilter.VISIBLE);
  99.  *      IOFileFilter files       = FileFilterUtils.and(
  100.  *                                      FileFilterUtils.fileFileFilter(),
  101.  *                                      FileFilterUtils.suffixFileFilter(".java"));
  102.  *      IOFileFilter filter = FileFilterUtils.or(directories, files);
  103.  *
  104.  *      // Create the File system observer and register File Listeners
  105.  *      FileAlterationObserver observer = new FileAlterationObserver(new File("src"), filter);
  106.  *      observer.addListener(...);
  107.  *      observer.addListener(...);
  108.  * </pre>
  109.  *
  110.  * <h2>FileEntry</h2>
  111.  * <p>
  112.  * {@link FileEntry} represents the state of a file or directory, capturing {@link File} attributes at a point in time. Custom implementations of
  113.  * {@link FileEntry} can be used to capture additional properties that the basic implementation does not support. The {@link FileEntry#refresh(File)} method is
  114.  * used to determine if a file or directory has changed since the last check and stores the current state of the {@link File}'s properties.
  115.  * </p>
  116.  * <h2>Deprecating Serialization</h2>
  117.  * <p>
  118.  * <em>Serialization is deprecated and will be removed in 3.0.</em>
  119.  * </p>
  120.  *
  121.  * @see FileAlterationListener
  122.  * @see FileAlterationMonitor
  123.  * @since 2.0
  124.  */
  125. public class FileAlterationObserver implements Serializable {

  126.     /**
  127.      * Builds instances of {@link FileAlterationObserver}.
  128.      *
  129.      * @since 2.18.0
  130.      */
  131.     public static final class Builder extends AbstractOriginSupplier<FileAlterationObserver, Builder> {

  132.         private FileEntry rootEntry;
  133.         private FileFilter fileFilter;
  134.         private IOCase ioCase;

  135.         private Builder() {
  136.             // empty
  137.         }

  138.         /**
  139.          * Gets a new {@link FileAlterationObserver} instance.
  140.          *
  141.          * @throws IOException                   if an I/O error occurs converting to an {@link File} using {@link AbstractOrigin#getFile()}.
  142.          * @see #getUnchecked()
  143.          */
  144.         @Override
  145.         public FileAlterationObserver get() throws IOException {
  146.             return new FileAlterationObserver(rootEntry != null ? rootEntry : new FileEntry(checkOrigin().getFile()), fileFilter, toComparator(ioCase));
  147.         }

  148.         /**
  149.          * Sets the file filter or null if none.
  150.          *
  151.          * @param fileFilter file filter or null if none.
  152.          * @return This instance.
  153.          */
  154.         public Builder setFileFilter(final FileFilter fileFilter) {
  155.             this.fileFilter = fileFilter;
  156.             return asThis();
  157.         }

  158.         /**
  159.          * Sets what case sensitivity to use comparing file names, null means system sensitive.
  160.          *
  161.          * @param ioCase what case sensitivity to use comparing file names, null means system sensitive.
  162.          * @return This instance.
  163.          */
  164.         public Builder setIOCase(final IOCase ioCase) {
  165.             this.ioCase = ioCase;
  166.             return asThis();
  167.         }

  168.         /**
  169.          * Sets the root directory to observe.
  170.          *
  171.          * @param rootEntry the root directory to observe.
  172.          * @return This instance.
  173.          */
  174.         public Builder setRootEntry(final FileEntry rootEntry) {
  175.             this.rootEntry = rootEntry;
  176.             return asThis();
  177.         }

  178.     }

  179.     private static final long serialVersionUID = 1185122225658782848L;

  180.     /**
  181.      * Creates a new builder.
  182.      *
  183.      * @return a new builder.
  184.      * @since 2.18.0
  185.      */
  186.     public static Builder builder() {
  187.         return new Builder();
  188.     }

  189.     private static Comparator<File> toComparator(final IOCase ioCase) {
  190.         switch (IOCase.value(ioCase, IOCase.SYSTEM)) {
  191.         case SYSTEM:
  192.             return NameFileComparator.NAME_SYSTEM_COMPARATOR;
  193.         case INSENSITIVE:
  194.             return NameFileComparator.NAME_INSENSITIVE_COMPARATOR;
  195.         default:
  196.             return NameFileComparator.NAME_COMPARATOR;
  197.         }
  198.     }

  199.     /**
  200.      * List of listeners.
  201.      */
  202.     private final transient List<FileAlterationListener> listeners = new CopyOnWriteArrayList<>();

  203.     /**
  204.      * The root directory to observe.
  205.      */
  206.     private final FileEntry rootEntry;

  207.     /**
  208.      * The file filter or null if none.
  209.      */
  210.     private final transient FileFilter fileFilter;

  211.     /**
  212.      * Compares file names.
  213.      */
  214.     private final Comparator<File> comparator;

  215.     /**
  216.      * Constructs an observer for the specified directory.
  217.      *
  218.      * @param directory the directory to observe.
  219.      * @deprecated Use {@link #builder()}.
  220.      */
  221.     @Deprecated
  222.     public FileAlterationObserver(final File directory) {
  223.         this(directory, null);
  224.     }

  225.     /**
  226.      * Constructs an observer for the specified directory and file filter.
  227.      *
  228.      * @param directory  The directory to observe.
  229.      * @param fileFilter The file filter or null if none.
  230.      * @deprecated Use {@link #builder()}.
  231.      */
  232.     @Deprecated
  233.     public FileAlterationObserver(final File directory, final FileFilter fileFilter) {
  234.         this(directory, fileFilter, null);
  235.     }

  236.     /**
  237.      * Constructs an observer for the specified directory, file filter and file comparator.
  238.      *
  239.      * @param directory  The directory to observe.
  240.      * @param fileFilter The file filter or null if none.
  241.      * @param ioCase     What case sensitivity to use comparing file names, null means system sensitive.
  242.      * @deprecated Use {@link #builder()}.
  243.      */
  244.     @Deprecated
  245.     public FileAlterationObserver(final File directory, final FileFilter fileFilter, final IOCase ioCase) {
  246.         this(new FileEntry(directory), fileFilter, ioCase);
  247.     }

  248.     /**
  249.      * Constructs an observer for the specified directory, file filter and file comparator.
  250.      *
  251.      * @param rootEntry  The root directory to observe.
  252.      * @param fileFilter The file filter or null if none.
  253.      * @param comparator How to compare files.
  254.      */
  255.     private FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final Comparator<File> comparator) {
  256.         Objects.requireNonNull(rootEntry, "rootEntry");
  257.         Objects.requireNonNull(rootEntry.getFile(), "rootEntry.getFile()");
  258.         this.rootEntry = rootEntry;
  259.         this.fileFilter = fileFilter != null ? fileFilter : TrueFileFilter.INSTANCE;
  260.         this.comparator = Objects.requireNonNull(comparator, "comparator");
  261.     }

  262.     /**
  263.      * Constructs an observer for the specified directory, file filter and file comparator.
  264.      *
  265.      * @param rootEntry  The root directory to observe.
  266.      * @param fileFilter The file filter or null if none.
  267.      * @param ioCase     What case sensitivity to use comparing file names, null means system sensitive.
  268.      */
  269.     protected FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final IOCase ioCase) {
  270.         this(rootEntry, fileFilter, toComparator(ioCase));
  271.     }

  272.     /**
  273.      * Constructs an observer for the specified directory.
  274.      *
  275.      * @param directoryName the name of the directory to observe.
  276.      * @deprecated Use {@link #builder()}.
  277.      */
  278.     @Deprecated
  279.     public FileAlterationObserver(final String directoryName) {
  280.         this(new File(directoryName));
  281.     }

  282.     /**
  283.      * Constructs an observer for the specified directory and file filter.
  284.      *
  285.      * @param directoryName the name of the directory to observe.
  286.      * @param fileFilter    The file filter or null if none.
  287.      * @deprecated Use {@link #builder()}.
  288.      */
  289.     @Deprecated
  290.     public FileAlterationObserver(final String directoryName, final FileFilter fileFilter) {
  291.         this(new File(directoryName), fileFilter);
  292.     }

  293.     /**
  294.      * Constructs an observer for the specified directory, file filter and file comparator.
  295.      *
  296.      * @param directoryName the name of the directory to observe.
  297.      * @param fileFilter    The file filter or null if none.
  298.      * @param ioCase        what case sensitivity to use comparing file names, null means system sensitive.
  299.      * @deprecated Use {@link #builder()}.
  300.      */
  301.     @Deprecated
  302.     public FileAlterationObserver(final String directoryName, final FileFilter fileFilter, final IOCase ioCase) {
  303.         this(new File(directoryName), fileFilter, ioCase);
  304.     }

  305.     /**
  306.      * Adds a file system listener.
  307.      *
  308.      * @param listener The file system listener.
  309.      */
  310.     public void addListener(final FileAlterationListener listener) {
  311.         if (listener != null) {
  312.             listeners.add(listener);
  313.         }
  314.     }

  315.     /**
  316.      * Compares two file lists for files which have been created, modified or deleted.
  317.      *
  318.      * @param parentEntry     The parent entry.
  319.      * @param previousEntries The original list of file entries.
  320.      * @param currentEntries  The current list of files entries.
  321.      */
  322.     private void checkAndFire(final FileEntry parentEntry, final FileEntry[] previousEntries, final File[] currentEntries) {
  323.         int c = 0;
  324.         final FileEntry[] actualEntries = currentEntries.length > 0 ? new FileEntry[currentEntries.length] : FileEntry.EMPTY_FILE_ENTRY_ARRAY;
  325.         for (final FileEntry previousEntry : previousEntries) {
  326.             while (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) > 0) {
  327.                 actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]);
  328.                 fireOnCreate(actualEntries[c]);
  329.                 c++;
  330.             }
  331.             if (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) == 0) {
  332.                 fireOnChange(previousEntry, currentEntries[c]);
  333.                 checkAndFire(previousEntry, previousEntry.getChildren(), listFiles(currentEntries[c]));
  334.                 actualEntries[c] = previousEntry;
  335.                 c++;
  336.             } else {
  337.                 checkAndFire(previousEntry, previousEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
  338.                 fireOnDelete(previousEntry);
  339.             }
  340.         }
  341.         for (; c < currentEntries.length; c++) {
  342.             actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]);
  343.             fireOnCreate(actualEntries[c]);
  344.         }
  345.         parentEntry.setChildren(actualEntries);
  346.     }

  347.     /**
  348.      * Checks whether the file and its children have been created, modified or deleted.
  349.      */
  350.     public void checkAndNotify() {

  351.         // fire onStart()
  352.         listeners.forEach(listener -> listener.onStart(this));

  353.         // fire directory/file events
  354.         final File rootFile = rootEntry.getFile();
  355.         if (rootFile.exists()) {
  356.             checkAndFire(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
  357.         } else if (rootEntry.isExists()) {
  358.             checkAndFire(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
  359.         }
  360.         // Else: Didn't exist and still doesn't

  361.         // fire onStop()
  362.         listeners.forEach(listener -> listener.onStop(this));
  363.     }

  364.     /**
  365.      * Creates a new file entry for the specified file.
  366.      *
  367.      * @param parent The parent file entry.
  368.      * @param file   The file to wrap.
  369.      * @return A new file entry.
  370.      */
  371.     private FileEntry createFileEntry(final FileEntry parent, final File file) {
  372.         final FileEntry entry = parent.newChildInstance(file);
  373.         entry.refresh(file);
  374.         entry.setChildren(listFileEntries(file, entry));
  375.         return entry;
  376.     }

  377.     /**
  378.      * Final processing.
  379.      *
  380.      * @throws Exception if an error occurs.
  381.      */
  382.     @SuppressWarnings("unused") // Possibly thrown from subclasses.
  383.     public void destroy() throws Exception {
  384.         // noop
  385.     }

  386.     /**
  387.      * Fires directory/file change events to the registered listeners.
  388.      *
  389.      * @param entry The previous file system entry.
  390.      * @param file  The current file.
  391.      */
  392.     private void fireOnChange(final FileEntry entry, final File file) {
  393.         if (entry.refresh(file)) {
  394.             listeners.forEach(listener -> {
  395.                 if (entry.isDirectory()) {
  396.                     listener.onDirectoryChange(file);
  397.                 } else {
  398.                     listener.onFileChange(file);
  399.                 }
  400.             });
  401.         }
  402.     }

  403.     /**
  404.      * Fires directory/file created events to the registered listeners.
  405.      *
  406.      * @param entry The file entry.
  407.      */
  408.     private void fireOnCreate(final FileEntry entry) {
  409.         listeners.forEach(listener -> {
  410.             if (entry.isDirectory()) {
  411.                 listener.onDirectoryCreate(entry.getFile());
  412.             } else {
  413.                 listener.onFileCreate(entry.getFile());
  414.             }
  415.         });
  416.         Stream.of(entry.getChildren()).forEach(this::fireOnCreate);
  417.     }

  418.     /**
  419.      * Fires directory/file delete events to the registered listeners.
  420.      *
  421.      * @param entry The file entry.
  422.      */
  423.     private void fireOnDelete(final FileEntry entry) {
  424.         listeners.forEach(listener -> {
  425.             if (entry.isDirectory()) {
  426.                 listener.onDirectoryDelete(entry.getFile());
  427.             } else {
  428.                 listener.onFileDelete(entry.getFile());
  429.             }
  430.         });
  431.     }

  432.     Comparator<File> getComparator() {
  433.         return comparator;
  434.     }

  435.     /**
  436.      * Returns the directory being observed.
  437.      *
  438.      * @return the directory being observed.
  439.      */
  440.     public File getDirectory() {
  441.         return rootEntry.getFile();
  442.     }

  443.     /**
  444.      * Returns the fileFilter.
  445.      *
  446.      * @return the fileFilter.
  447.      * @since 2.1
  448.      */
  449.     public FileFilter getFileFilter() {
  450.         return fileFilter;
  451.     }

  452.     /**
  453.      * Returns the set of registered file system listeners.
  454.      *
  455.      * @return The file system listeners
  456.      */
  457.     public Iterable<FileAlterationListener> getListeners() {
  458.         return new ArrayList<>(listeners);
  459.     }

  460.     /**
  461.      * Initializes the observer.
  462.      *
  463.      * @throws Exception if an error occurs.
  464.      */
  465.     @SuppressWarnings("unused") // Possibly thrown from subclasses.
  466.     public void initialize() throws Exception {
  467.         rootEntry.refresh(rootEntry.getFile());
  468.         rootEntry.setChildren(listFileEntries(rootEntry.getFile(), rootEntry));
  469.     }

  470.     /**
  471.      * Lists the file entries in {@code file}.
  472.      *
  473.      * @param file  The directory to list.
  474.      * @param entry the parent entry.
  475.      * @return The child file entries.
  476.      */
  477.     private FileEntry[] listFileEntries(final File file, final FileEntry entry) {
  478.         return Stream.of(listFiles(file)).map(f -> createFileEntry(entry, f)).toArray(FileEntry[]::new);
  479.     }

  480.     /**
  481.      * Lists the contents of a directory.
  482.      *
  483.      * @param directory The directory to list.
  484.      * @return the directory contents or a zero length array if the empty or the file is not a directory
  485.      */
  486.     private File[] listFiles(final File directory) {
  487.         return directory.isDirectory() ? sort(directory.listFiles(fileFilter)) : FileUtils.EMPTY_FILE_ARRAY;
  488.     }

  489.     /**
  490.      * Removes a file system listener.
  491.      *
  492.      * @param listener The file system listener.
  493.      */
  494.     public void removeListener(final FileAlterationListener listener) {
  495.         if (listener != null) {
  496.             listeners.removeIf(listener::equals);
  497.         }
  498.     }

  499.     private File[] sort(final File[] files) {
  500.         if (files == null) {
  501.             return FileUtils.EMPTY_FILE_ARRAY;
  502.         }
  503.         if (files.length > 1) {
  504.             Arrays.sort(files, comparator);
  505.         }
  506.         return files;
  507.     }

  508.     /**
  509.      * Returns a String representation of this observer.
  510.      *
  511.      * @return a String representation of this observer.
  512.      */
  513.     @Override
  514.     public String toString() {
  515.         final StringBuilder builder = new StringBuilder();
  516.         builder.append(getClass().getSimpleName());
  517.         builder.append("[file='");
  518.         builder.append(getDirectory().getPath());
  519.         builder.append('\'');
  520.         builder.append(", ");
  521.         builder.append(fileFilter.toString());
  522.         builder.append(", listeners=");
  523.         builder.append(listeners.size());
  524.         builder.append("]");
  525.         return builder.toString();
  526.     }

  527. }