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.  *      https://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.         private File checkOriginFile() {
  139.             return checkOrigin().getFile();
  140.         }

  141.         /**
  142.          * Gets a new {@link FileAlterationObserver} instance.
  143.          *
  144.          * @throws IOException                   if an I/O error occurs converting to an {@link File} using {@link AbstractOrigin#getFile()}.
  145.          * @see #getUnchecked()
  146.          */
  147.         @Override
  148.         public FileAlterationObserver get() throws IOException {
  149.             return new FileAlterationObserver(this);
  150.         }

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

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

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

  181.     }

  182.     private static final long serialVersionUID = 1185122225658782848L;

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

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

  202.     /**
  203.      * List of listeners.
  204.      */
  205.     private final transient List<FileAlterationListener> listeners = new CopyOnWriteArrayList<>();

  206.     /**
  207.      * The root directory to observe.
  208.      */
  209.     private final FileEntry rootEntry;

  210.     /**
  211.      * The file filter or null if none.
  212.      */
  213.     private final transient FileFilter fileFilter;

  214.     /**
  215.      * Compares file names.
  216.      */
  217.     private final Comparator<File> comparator;

  218.     private FileAlterationObserver(final Builder builder) {
  219.         this(builder.rootEntry != null ? builder.rootEntry : new FileEntry(builder.checkOriginFile()), builder.fileFilter, toComparator(builder.ioCase));
  220.     }

  221.     /**
  222.      * Constructs an observer for the specified directory.
  223.      *
  224.      * @param directory the directory to observe.
  225.      * @deprecated Use {@link #builder()}.
  226.      */
  227.     @Deprecated
  228.     public FileAlterationObserver(final File directory) {
  229.         this(directory, null);
  230.     }

  231.     /**
  232.      * Constructs an observer for the specified directory and file filter.
  233.      *
  234.      * @param directory  The directory to observe.
  235.      * @param fileFilter The file filter or null if none.
  236.      * @deprecated Use {@link #builder()}.
  237.      */
  238.     @Deprecated
  239.     public FileAlterationObserver(final File directory, final FileFilter fileFilter) {
  240.         this(directory, fileFilter, null);
  241.     }

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

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

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

  278.     /**
  279.      * Constructs an observer for the specified directory.
  280.      *
  281.      * @param directoryName the name of the directory to observe.
  282.      * @deprecated Use {@link #builder()}.
  283.      */
  284.     @Deprecated
  285.     public FileAlterationObserver(final String directoryName) {
  286.         this(new File(directoryName));
  287.     }

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

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

  311.     /**
  312.      * Adds a file system listener.
  313.      *
  314.      * @param listener The file system listener.
  315.      */
  316.     public void addListener(final FileAlterationListener listener) {
  317.         if (listener != null) {
  318.             listeners.add(listener);
  319.         }
  320.     }

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

  353.     /**
  354.      * Checks whether the file and its children have been created, modified or deleted.
  355.      */
  356.     public void checkAndNotify() {

  357.         // fire onStart()
  358.         listeners.forEach(listener -> listener.onStart(this));

  359.         // fire directory/file events
  360.         final File rootFile = rootEntry.getFile();
  361.         if (rootFile.exists()) {
  362.             checkAndFire(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
  363.         } else if (rootEntry.isExists()) {
  364.             checkAndFire(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
  365.         }
  366.         // Else: Didn't exist and still doesn't

  367.         // fire onStop()
  368.         listeners.forEach(listener -> listener.onStop(this));
  369.     }

  370.     /**
  371.      * Creates a new file entry for the specified file.
  372.      *
  373.      * @param parent The parent file entry.
  374.      * @param file   The file to wrap.
  375.      * @return A new file entry.
  376.      */
  377.     private FileEntry createFileEntry(final FileEntry parent, final File file) {
  378.         final FileEntry entry = parent.newChildInstance(file);
  379.         entry.refresh(file);
  380.         entry.setChildren(listFileEntries(file, entry));
  381.         return entry;
  382.     }

  383.     /**
  384.      * Final processing.
  385.      *
  386.      * @throws Exception if an error occurs.
  387.      */
  388.     @SuppressWarnings("unused") // Possibly thrown from subclasses.
  389.     public void destroy() throws Exception {
  390.         // noop
  391.     }

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

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

  424.     /**
  425.      * Fires directory/file delete events to the registered listeners.
  426.      *
  427.      * @param entry The file entry.
  428.      */
  429.     private void fireOnDelete(final FileEntry entry) {
  430.         listeners.forEach(listener -> {
  431.             if (entry.isDirectory()) {
  432.                 listener.onDirectoryDelete(entry.getFile());
  433.             } else {
  434.                 listener.onFileDelete(entry.getFile());
  435.             }
  436.         });
  437.     }

  438.     Comparator<File> getComparator() {
  439.         return comparator;
  440.     }

  441.     /**
  442.      * Returns the directory being observed.
  443.      *
  444.      * @return the directory being observed.
  445.      */
  446.     public File getDirectory() {
  447.         return rootEntry.getFile();
  448.     }

  449.     /**
  450.      * Returns the fileFilter.
  451.      *
  452.      * @return the fileFilter.
  453.      * @since 2.1
  454.      */
  455.     public FileFilter getFileFilter() {
  456.         return fileFilter;
  457.     }

  458.     /**
  459.      * Returns the set of registered file system listeners.
  460.      *
  461.      * @return The file system listeners
  462.      */
  463.     public Iterable<FileAlterationListener> getListeners() {
  464.         return new ArrayList<>(listeners);
  465.     }

  466.     /**
  467.      * Initializes the observer.
  468.      *
  469.      * @throws Exception if an error occurs.
  470.      */
  471.     @SuppressWarnings("unused") // Possibly thrown from subclasses.
  472.     public void initialize() throws Exception {
  473.         rootEntry.refresh(rootEntry.getFile());
  474.         rootEntry.setChildren(listFileEntries(rootEntry.getFile(), rootEntry));
  475.     }

  476.     /**
  477.      * Lists the file entries in {@code file}.
  478.      *
  479.      * @param file  The directory to list.
  480.      * @param entry the parent entry.
  481.      * @return The child file entries.
  482.      */
  483.     private FileEntry[] listFileEntries(final File file, final FileEntry entry) {
  484.         return Stream.of(listFiles(file)).map(f -> createFileEntry(entry, f)).toArray(FileEntry[]::new);
  485.     }

  486.     /**
  487.      * Lists the contents of a directory.
  488.      *
  489.      * @param directory The directory to list.
  490.      * @return the directory contents or a zero length array if the empty or the file is not a directory
  491.      */
  492.     private File[] listFiles(final File directory) {
  493.         return directory.isDirectory() ? sort(directory.listFiles(fileFilter)) : FileUtils.EMPTY_FILE_ARRAY;
  494.     }

  495.     /**
  496.      * Removes a file system listener.
  497.      *
  498.      * @param listener The file system listener.
  499.      */
  500.     public void removeListener(final FileAlterationListener listener) {
  501.         if (listener != null) {
  502.             listeners.removeIf(listener::equals);
  503.         }
  504.     }

  505.     private File[] sort(final File[] files) {
  506.         if (files == null) {
  507.             return FileUtils.EMPTY_FILE_ARRAY;
  508.         }
  509.         if (files.length > 1) {
  510.             Arrays.sort(files, comparator);
  511.         }
  512.         return files;
  513.     }

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

  533. }