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.time.Duration;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.List;
24 import java.util.Optional;
25 import java.util.concurrent.CopyOnWriteArrayList;
26 import java.util.concurrent.ThreadFactory;
27 import java.util.stream.Stream;
28
29 import org.apache.commons.io.ThreadUtils;
30
31 /**
32 * A runnable that spawns a monitoring thread triggering any
33 * registered {@link FileAlterationObserver} at a specified interval.
34 *
35 * @see FileAlterationObserver
36 * @since 2.0
37 */
38 public final class FileAlterationMonitor implements Runnable {
39
40 private static final FileAlterationObserver[] EMPTY_ARRAY = {};
41
42 private final long intervalMillis;
43 private final List<FileAlterationObserver> observers = new CopyOnWriteArrayList<>();
44 private Thread thread;
45 private ThreadFactory threadFactory;
46 private volatile boolean running;
47
48 /**
49 * Constructs a monitor with a default interval of 10 seconds.
50 */
51 public FileAlterationMonitor() {
52 this(10_000);
53 }
54
55 /**
56 * Constructs a monitor with the specified interval.
57 *
58 * @param intervalMillis The amount of time in milliseconds to wait between
59 * checks of the file system.
60 */
61 public FileAlterationMonitor(final long intervalMillis) {
62 this.intervalMillis = intervalMillis;
63 }
64
65 /**
66 * Constructs a monitor with the specified interval and collection of observers.
67 *
68 * @param interval The amount of time in milliseconds to wait between
69 * checks of the file system.
70 * @param observers The collection of observers to add to the monitor.
71 * @since 2.9.0
72 */
73 public FileAlterationMonitor(final long interval, final Collection<FileAlterationObserver> observers) {
74 // @formatter:off
75 this(interval,
76 Optional
77 .ofNullable(observers)
78 .orElse(Collections.emptyList())
79 .toArray(EMPTY_ARRAY)
80 );
81 // @formatter:on
82 }
83
84 /**
85 * Constructs a monitor with the specified interval and set of observers.
86 *
87 * @param interval The amount of time in milliseconds to wait between
88 * checks of the file system.
89 * @param observers The set of observers to add to the monitor.
90 */
91 public FileAlterationMonitor(final long interval, final FileAlterationObserver... observers) {
92 this(interval);
93 if (observers != null) {
94 Stream.of(observers).forEach(this::addObserver);
95 }
96 }
97
98 /**
99 * Adds a file system observer to this monitor.
100 *
101 * @param observer The file system observer to add
102 */
103 public void addObserver(final FileAlterationObserver observer) {
104 if (observer != null) {
105 observers.add(observer);
106 }
107 }
108
109 /**
110 * Returns the interval.
111 *
112 * @return the interval
113 */
114 public long getInterval() {
115 return intervalMillis;
116 }
117
118 /**
119 * Returns the set of {@link FileAlterationObserver} registered with
120 * this monitor.
121 *
122 * @return The set of {@link FileAlterationObserver}
123 */
124 public Iterable<FileAlterationObserver> getObservers() {
125 return new ArrayList<>(observers);
126 }
127
128 /**
129 * Removes a file system observer from this monitor.
130 *
131 * @param observer The file system observer to remove
132 */
133 public void removeObserver(final FileAlterationObserver observer) {
134 if (observer != null) {
135 observers.removeIf(observer::equals);
136 }
137 }
138
139 /**
140 * Runs this monitor.
141 */
142 @Override
143 public void run() {
144 while (running) {
145 observers.forEach(FileAlterationObserver::checkAndNotify);
146 if (!running) {
147 break;
148 }
149 try {
150 ThreadUtils.sleep(Duration.ofMillis(intervalMillis));
151 } catch (final InterruptedException ignored) {
152 // ignore
153 }
154 }
155 }
156
157 /**
158 * Sets the thread factory.
159 *
160 * @param threadFactory the thread factory
161 */
162 public synchronized void setThreadFactory(final ThreadFactory threadFactory) {
163 this.threadFactory = threadFactory;
164 }
165
166 /**
167 * Starts monitoring.
168 *
169 * @throws Exception if an error occurs initializing the observer
170 */
171 public synchronized void start() throws Exception {
172 if (running) {
173 throw new IllegalStateException("Monitor is already running");
174 }
175 for (final FileAlterationObserver observer : observers) {
176 observer.initialize();
177 }
178 running = true;
179 if (threadFactory != null) {
180 thread = threadFactory.newThread(this);
181 } else {
182 thread = new Thread(this);
183 }
184 thread.start();
185 }
186
187 /**
188 * Stops monitoring.
189 *
190 * @throws Exception if an error occurs initializing the observer
191 */
192 public synchronized void stop() throws Exception {
193 stop(intervalMillis);
194 }
195
196 /**
197 * Stops monitoring.
198 *
199 * @param stopInterval the amount of time in milliseconds to wait for the thread to finish.
200 * A value of zero will wait until the thread is finished (see {@link Thread#join(long)}).
201 * @throws Exception if an error occurs initializing the observer
202 * @since 2.1
203 */
204 public synchronized void stop(final long stopInterval) throws Exception {
205 if (!running) {
206 throw new IllegalStateException("Monitor is not running");
207 }
208 running = false;
209 try {
210 thread.interrupt();
211 thread.join(stopInterval);
212 } catch (final InterruptedException e) {
213 Thread.currentThread().interrupt();
214 }
215 for (final FileAlterationObserver observer : observers) {
216 observer.destroy();
217 }
218 }
219 }