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
19 import java.io.File;
20 import java.io.FileFilter;
21 import java.io.IOException;
22 import java.io.Serializable;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Comparator;
26 import java.util.List;
27 import java.util.Objects;
28 import java.util.concurrent.CopyOnWriteArrayList;
29 import java.util.stream.Stream;
30
31 import org.apache.commons.io.FileUtils;
32 import org.apache.commons.io.IOCase;
33 import org.apache.commons.io.build.AbstractOrigin;
34 import org.apache.commons.io.build.AbstractOriginSupplier;
35 import org.apache.commons.io.comparator.NameFileComparator;
36 import org.apache.commons.io.filefilter.TrueFileFilter;
37
38 /**
39 * FileAlterationObserver represents the state of files below a root directory, checking the file system and notifying listeners of create, change or delete
40 * events.
41 * <p>
42 * To use this implementation:
43 * </p>
44 * <ul>
45 * <li>Create {@link FileAlterationListener} implementation(s) that process the file/directory create, change and delete events</li>
46 * <li>Register the listener(s) with a {@link FileAlterationObserver} for the appropriate directory.</li>
47 * <li>Either register the observer(s) with a {@link FileAlterationMonitor} or run manually.</li>
48 * </ul>
49 * <h2>Basic Usage</h2> Create a {@link FileAlterationObserver} for the directory and register the listeners:
50 *
51 * <pre>
52 * File directory = new File(FileUtils.current(), "src");
53 * FileAlterationObserver observer = new FileAlterationObserver(directory);
54 * observer.addListener(...);
55 * observer.addListener(...);
56 * </pre>
57 * <p>
58 * To manually observe a directory, initialize the observer and invoked the {@link #checkAndNotify()} method as required:
59 * </p>
60 *
61 * <pre>
62 * // initialize
63 * observer.init();
64 * ...
65 * // invoke as required
66 * observer.checkAndNotify();
67 * ...
68 * observer.checkAndNotify();
69 * ...
70 * // finished
71 * observer.finish();
72 * </pre>
73 * <p>
74 * Alternatively, register the observer(s) with a {@link FileAlterationMonitor}, which creates a new thread, invoking the observer at the specified interval:
75 * </p>
76 *
77 * <pre>
78 * long interval = ...
79 * FileAlterationMonitor monitor = new FileAlterationMonitor(interval);
80 * monitor.addObserver(observer);
81 * monitor.start();
82 * ...
83 * monitor.stop();
84 * </pre>
85 *
86 * <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
87 * that are of interest. This makes it more efficient and reduces the noise from <em>unwanted</em> file system events.
88 * <p>
89 * <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>
90 * implementations for this purpose.
91 * </p>
92 * <p>
93 * 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
94 * {@link FileAlterationObserver} in the following way:
95 * </p>
96 *
97 * <pre>
98 * // Create a FileFilter
99 * IOFileFilter directories = FileFilterUtils.and(
100 * FileFilterUtils.directoryFileFilter(),
101 * HiddenFileFilter.VISIBLE);
102 * IOFileFilter files = FileFilterUtils.and(
103 * FileFilterUtils.fileFileFilter(),
104 * FileFilterUtils.suffixFileFilter(".java"));
105 * IOFileFilter filter = FileFilterUtils.or(directories, files);
106 *
107 * // Create the File system observer and register File Listeners
108 * FileAlterationObserver observer = new FileAlterationObserver(new File("src"), filter);
109 * observer.addListener(...);
110 * observer.addListener(...);
111 * </pre>
112 *
113 * <h2>FileEntry</h2>
114 * <p>
115 * {@link FileEntry} represents the state of a file or directory, capturing {@link File} attributes at a point in time. Custom implementations of
116 * {@link FileEntry} can be used to capture additional properties that the basic implementation does not support. The {@link FileEntry#refresh(File)} method is
117 * 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.
118 * </p>
119 * <h2>Deprecating Serialization</h2>
120 * <p>
121 * <em>Serialization is deprecated and will be removed in 3.0.</em>
122 * </p>
123 *
124 * @see FileAlterationListener
125 * @see FileAlterationMonitor
126 * @since 2.0
127 */
128 public class FileAlterationObserver implements Serializable {
129
130 /**
131 * Builds instances of {@link FileAlterationObserver}.
132 *
133 * @since 2.18.0
134 */
135 public static final class Builder extends AbstractOriginSupplier<FileAlterationObserver, Builder> {
136
137 private FileEntry rootEntry;
138 private FileFilter fileFilter;
139 private IOCase ioCase;
140
141 private Builder() {
142 // empty
143 }
144
145 private File checkOriginFile() {
146 return checkOrigin().getFile();
147 }
148
149 /**
150 * Gets a new {@link FileAlterationObserver} instance.
151 *
152 * @throws IOException if an I/O error occurs converting to an {@link File} using {@link AbstractOrigin#getFile()}.
153 * @see #getUnchecked()
154 */
155 @Override
156 public FileAlterationObserver get() throws IOException {
157 return new FileAlterationObserver(this);
158 }
159
160 /**
161 * Sets the file filter or null if none.
162 *
163 * @param fileFilter file filter or null if none.
164 * @return This instance.
165 */
166 public Builder setFileFilter(final FileFilter fileFilter) {
167 this.fileFilter = fileFilter;
168 return asThis();
169 }
170
171 /**
172 * Sets what case sensitivity to use comparing file names, null means system sensitive.
173 *
174 * @param ioCase what case sensitivity to use comparing file names, null means system sensitive.
175 * @return This instance.
176 */
177 public Builder setIOCase(final IOCase ioCase) {
178 this.ioCase = ioCase;
179 return asThis();
180 }
181
182 /**
183 * Sets the root directory to observe.
184 *
185 * @param rootEntry the root directory to observe.
186 * @return This instance.
187 */
188 public Builder setRootEntry(final FileEntry rootEntry) {
189 this.rootEntry = rootEntry;
190 return asThis();
191 }
192
193 }
194
195 private static final long serialVersionUID = 1185122225658782848L;
196
197 /**
198 * Creates a new builder.
199 *
200 * @return a new builder.
201 * @since 2.18.0
202 */
203 public static Builder builder() {
204 return new Builder();
205 }
206
207 private static Comparator<File> toComparator(final IOCase ioCase) {
208 switch (IOCase.value(ioCase, IOCase.SYSTEM)) {
209 case SYSTEM:
210 return NameFileComparator.NAME_SYSTEM_COMPARATOR;
211 case INSENSITIVE:
212 return NameFileComparator.NAME_INSENSITIVE_COMPARATOR;
213 default:
214 return NameFileComparator.NAME_COMPARATOR;
215 }
216 }
217
218 /**
219 * List of listeners.
220 */
221 private final transient List<FileAlterationListener> listeners = new CopyOnWriteArrayList<>();
222
223 /**
224 * The root directory to observe.
225 */
226 private final FileEntry rootEntry;
227
228 /**
229 * The file filter or null if none.
230 */
231 private final transient FileFilter fileFilter;
232
233 /**
234 * Compares file names.
235 */
236 private final Comparator<File> comparator;
237
238 private FileAlterationObserver(final Builder builder) {
239 this(builder.rootEntry != null ? builder.rootEntry : new FileEntry(builder.checkOriginFile()), builder.fileFilter, toComparator(builder.ioCase));
240 }
241
242 /**
243 * Constructs an observer for the specified directory.
244 *
245 * @param directory the directory to observe.
246 * @deprecated Use {@link #builder()}.
247 */
248 @Deprecated
249 public FileAlterationObserver(final File directory) {
250 this(directory, null);
251 }
252
253 /**
254 * Constructs an observer for the specified directory and file filter.
255 *
256 * @param directory The directory to observe.
257 * @param fileFilter The file filter or null if none.
258 * @deprecated Use {@link #builder()}.
259 */
260 @Deprecated
261 public FileAlterationObserver(final File directory, final FileFilter fileFilter) {
262 this(directory, fileFilter, null);
263 }
264
265 /**
266 * Constructs an observer for the specified directory, file filter and file comparator.
267 *
268 * @param directory The directory to observe.
269 * @param fileFilter The file filter or null if none.
270 * @param ioCase What case sensitivity to use comparing file names, null means system sensitive.
271 * @deprecated Use {@link #builder()}.
272 */
273 @Deprecated
274 public FileAlterationObserver(final File directory, final FileFilter fileFilter, final IOCase ioCase) {
275 this(new FileEntry(directory), fileFilter, ioCase);
276 }
277
278 /**
279 * Constructs an observer for the specified directory, file filter and file comparator.
280 *
281 * @param rootEntry The root directory to observe.
282 * @param fileFilter The file filter or null if none.
283 * @param comparator How to compare files.
284 */
285 private FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final Comparator<File> comparator) {
286 Objects.requireNonNull(rootEntry, "rootEntry");
287 Objects.requireNonNull(rootEntry.getFile(), "rootEntry.getFile()");
288 this.rootEntry = rootEntry;
289 this.fileFilter = fileFilter != null ? fileFilter : TrueFileFilter.INSTANCE;
290 this.comparator = Objects.requireNonNull(comparator, "comparator");
291 }
292
293 /**
294 * Constructs an observer for the specified directory, file filter and file comparator.
295 *
296 * @param rootEntry The root 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 */
300 protected FileAlterationObserver(final FileEntry rootEntry, final FileFilter fileFilter, final IOCase ioCase) {
301 this(rootEntry, fileFilter, toComparator(ioCase));
302 }
303
304 /**
305 * Constructs an observer for the specified directory.
306 *
307 * @param directoryName the name of the directory to observe.
308 * @deprecated Use {@link #builder()}.
309 */
310 @Deprecated
311 public FileAlterationObserver(final String directoryName) {
312 this(new File(directoryName));
313 }
314
315 /**
316 * Constructs an observer for the specified directory and file filter.
317 *
318 * @param directoryName the name of the directory to observe.
319 * @param fileFilter The file filter or null if none.
320 * @deprecated Use {@link #builder()}.
321 */
322 @Deprecated
323 public FileAlterationObserver(final String directoryName, final FileFilter fileFilter) {
324 this(new File(directoryName), fileFilter);
325 }
326
327 /**
328 * Constructs an observer for the specified directory, file filter and file comparator.
329 *
330 * @param directoryName the name of the directory to observe.
331 * @param fileFilter The file filter or null if none.
332 * @param ioCase what case sensitivity to use comparing file names, null means system sensitive.
333 * @deprecated Use {@link #builder()}.
334 */
335 @Deprecated
336 public FileAlterationObserver(final String directoryName, final FileFilter fileFilter, final IOCase ioCase) {
337 this(new File(directoryName), fileFilter, ioCase);
338 }
339
340 /**
341 * Adds a file system listener.
342 *
343 * @param listener The file system listener.
344 */
345 public void addListener(final FileAlterationListener listener) {
346 if (listener != null) {
347 listeners.add(listener);
348 }
349 }
350
351 /**
352 * Compares two file lists for files which have been created, modified or deleted.
353 *
354 * @param parentEntry The parent entry.
355 * @param previousEntries The original list of file entries.
356 * @param currentEntries The current list of files entries.
357 */
358 private void checkAndFire(final FileEntry parentEntry, final FileEntry[] previousEntries, final File[] currentEntries) {
359 int c = 0;
360 final FileEntry[] actualEntries = currentEntries.length > 0 ? new FileEntry[currentEntries.length] : FileEntry.EMPTY_FILE_ENTRY_ARRAY;
361 for (final FileEntry previousEntry : previousEntries) {
362 while (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) > 0) {
363 actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]);
364 fireOnCreate(actualEntries[c]);
365 c++;
366 }
367 if (c < currentEntries.length && comparator.compare(previousEntry.getFile(), currentEntries[c]) == 0) {
368 fireOnChange(previousEntry, currentEntries[c]);
369 checkAndFire(previousEntry, previousEntry.getChildren(), listFiles(currentEntries[c]));
370 actualEntries[c] = previousEntry;
371 c++;
372 } else {
373 checkAndFire(previousEntry, previousEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
374 fireOnDelete(previousEntry);
375 }
376 }
377 for (; c < currentEntries.length; c++) {
378 actualEntries[c] = createFileEntry(parentEntry, currentEntries[c]);
379 fireOnCreate(actualEntries[c]);
380 }
381 parentEntry.setChildren(actualEntries);
382 }
383
384 /**
385 * Checks whether the file and its children have been created, modified or deleted.
386 */
387 public void checkAndNotify() {
388
389 // fire onStart()
390 listeners.forEach(listener -> listener.onStart(this));
391
392 // fire directory/file events
393 final File rootFile = rootEntry.getFile();
394 if (rootFile.exists()) {
395 checkAndFire(rootEntry, rootEntry.getChildren(), listFiles(rootFile));
396 } else if (rootEntry.isExists()) {
397 checkAndFire(rootEntry, rootEntry.getChildren(), FileUtils.EMPTY_FILE_ARRAY);
398 }
399 // Else: Didn't exist and still doesn't
400
401 // fire onStop()
402 listeners.forEach(listener -> listener.onStop(this));
403 }
404
405 /**
406 * Creates a new file entry for the specified file.
407 *
408 * @param parent The parent file entry.
409 * @param file The file to wrap.
410 * @return A new file entry.
411 */
412 private FileEntry createFileEntry(final FileEntry parent, final File file) {
413 final FileEntry entry = parent.newChildInstance(file);
414 entry.refresh(file);
415 entry.setChildren(listFileEntries(file, entry));
416 return entry;
417 }
418
419 /**
420 * Final processing.
421 *
422 * @throws Exception if an error occurs.
423 */
424 @SuppressWarnings("unused") // Possibly thrown from subclasses.
425 public void destroy() throws Exception {
426 // noop
427 }
428
429 /**
430 * Fires directory/file change events to the registered listeners.
431 *
432 * @param entry The previous file system entry.
433 * @param file The current file.
434 */
435 private void fireOnChange(final FileEntry entry, final File file) {
436 if (entry.refresh(file)) {
437 listeners.forEach(listener -> {
438 if (entry.isDirectory()) {
439 listener.onDirectoryChange(file);
440 } else {
441 listener.onFileChange(file);
442 }
443 });
444 }
445 }
446
447 /**
448 * Fires directory/file created events to the registered listeners.
449 *
450 * @param entry The file entry.
451 */
452 private void fireOnCreate(final FileEntry entry) {
453 listeners.forEach(listener -> {
454 if (entry.isDirectory()) {
455 listener.onDirectoryCreate(entry.getFile());
456 } else {
457 listener.onFileCreate(entry.getFile());
458 }
459 });
460 Stream.of(entry.getChildren()).forEach(this::fireOnCreate);
461 }
462
463 /**
464 * Fires directory/file delete events to the registered listeners.
465 *
466 * @param entry The file entry.
467 */
468 private void fireOnDelete(final FileEntry entry) {
469 listeners.forEach(listener -> {
470 if (entry.isDirectory()) {
471 listener.onDirectoryDelete(entry.getFile());
472 } else {
473 listener.onFileDelete(entry.getFile());
474 }
475 });
476 }
477
478 Comparator<File> getComparator() {
479 return comparator;
480 }
481
482 /**
483 * Returns the directory being observed.
484 *
485 * @return the directory being observed.
486 */
487 public File getDirectory() {
488 return rootEntry.getFile();
489 }
490
491 /**
492 * Returns the fileFilter.
493 *
494 * @return the fileFilter.
495 * @since 2.1
496 */
497 public FileFilter getFileFilter() {
498 return fileFilter;
499 }
500
501 /**
502 * Returns the set of registered file system listeners.
503 *
504 * @return The file system listeners
505 */
506 public Iterable<FileAlterationListener> getListeners() {
507 return new ArrayList<>(listeners);
508 }
509
510 /**
511 * Initializes the observer.
512 *
513 * @throws Exception if an error occurs.
514 */
515 @SuppressWarnings("unused") // Possibly thrown from subclasses.
516 public void initialize() throws Exception {
517 rootEntry.refresh(rootEntry.getFile());
518 rootEntry.setChildren(listFileEntries(rootEntry.getFile(), rootEntry));
519 }
520
521 /**
522 * Lists the file entries in {@code file}.
523 *
524 * @param file The directory to list.
525 * @param entry the parent entry.
526 * @return The child file entries.
527 */
528 private FileEntry[] listFileEntries(final File file, final FileEntry entry) {
529 return Stream.of(listFiles(file)).map(f -> createFileEntry(entry, f)).toArray(FileEntry[]::new);
530 }
531
532 /**
533 * Lists the contents of a directory.
534 *
535 * @param directory The directory to list.
536 * @return the directory contents or a zero length array if the empty or the file is not a directory
537 */
538 private File[] listFiles(final File directory) {
539 return directory.isDirectory() ? sort(directory.listFiles(fileFilter)) : FileUtils.EMPTY_FILE_ARRAY;
540 }
541
542 /**
543 * Removes a file system listener.
544 *
545 * @param listener The file system listener.
546 */
547 public void removeListener(final FileAlterationListener listener) {
548 if (listener != null) {
549 listeners.removeIf(listener::equals);
550 }
551 }
552
553 private File[] sort(final File[] files) {
554 if (files == null) {
555 return FileUtils.EMPTY_FILE_ARRAY;
556 }
557 if (files.length > 1) {
558 Arrays.sort(files, comparator);
559 }
560 return files;
561 }
562
563 /**
564 * Returns a String representation of this observer.
565 *
566 * @return a String representation of this observer.
567 */
568 @Override
569 public String toString() {
570 final StringBuilder builder = new StringBuilder();
571 builder.append(getClass().getSimpleName());
572 builder.append("[file='");
573 builder.append(getDirectory().getPath());
574 builder.append('\'');
575 builder.append(", ");
576 builder.append(fileFilter.toString());
577 builder.append(", listeners=");
578 builder.append(listeners.size());
579 builder.append("]");
580 return builder.toString();
581 }
582
583 }