001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.io.monitor;
018
019import java.time.Duration;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.List;
023import java.util.Optional;
024import java.util.concurrent.CopyOnWriteArrayList;
025import java.util.concurrent.ThreadFactory;
026import java.util.stream.Stream;
027
028import org.apache.commons.io.ThreadUtils;
029
030/**
031 * A runnable that spawns a monitoring thread triggering any
032 * registered {@link FileAlterationObserver} at a specified interval.
033 *
034 * @see FileAlterationObserver
035 * @since 2.0
036 */
037public final class FileAlterationMonitor implements Runnable {
038
039    private static final FileAlterationObserver[] EMPTY_ARRAY = {};
040
041    private final long intervalMillis;
042    private final List<FileAlterationObserver> observers = new CopyOnWriteArrayList<>();
043    private Thread thread;
044    private ThreadFactory threadFactory;
045    private volatile boolean running;
046
047    /**
048     * Constructs a monitor with a default interval of 10 seconds.
049     */
050    public FileAlterationMonitor() {
051        this(10_000);
052    }
053
054    /**
055     * Constructs a monitor with the specified interval.
056     *
057     * @param intervalMillis The amount of time in milliseconds to wait between
058     * checks of the file system.
059     */
060    public FileAlterationMonitor(final long intervalMillis) {
061        this.intervalMillis = intervalMillis;
062    }
063
064    /**
065     * Constructs a monitor with the specified interval and collection of observers.
066     *
067     * @param interval The amount of time in milliseconds to wait between
068     * checks of the file system.
069     * @param observers The collection of observers to add to the monitor.
070     * @since 2.9.0
071     */
072    public FileAlterationMonitor(final long interval, final Collection<FileAlterationObserver> observers) {
073        // @formatter:off
074        this(interval,
075            Optional
076                .ofNullable(observers)
077                .orElse(Collections.emptyList())
078                .toArray(EMPTY_ARRAY)
079        );
080        // @formatter:on
081    }
082
083    /**
084     * Constructs a monitor with the specified interval and set of observers.
085     *
086     * @param interval The amount of time in milliseconds to wait between
087     * checks of the file system.
088     * @param observers The set of observers to add to the monitor.
089     */
090    public FileAlterationMonitor(final long interval, final FileAlterationObserver... observers) {
091        this(interval);
092        if (observers != null) {
093            Stream.of(observers).forEach(this::addObserver);
094        }
095    }
096
097    /**
098     * Adds a file system observer to this monitor.
099     *
100     * @param observer The file system observer to add
101     */
102    public void addObserver(final FileAlterationObserver observer) {
103        if (observer != null) {
104            observers.add(observer);
105        }
106    }
107
108    /**
109     * Returns the interval.
110     *
111     * @return the interval
112     */
113    public long getInterval() {
114        return intervalMillis;
115    }
116
117    /**
118     * Returns the set of {@link FileAlterationObserver} registered with
119     * this monitor.
120     *
121     * @return The set of {@link FileAlterationObserver}
122     */
123    public Iterable<FileAlterationObserver> getObservers() {
124        return observers;
125    }
126
127    /**
128     * Removes a file system observer from this monitor.
129     *
130     * @param observer The file system observer to remove
131     */
132    public void removeObserver(final FileAlterationObserver observer) {
133        if (observer != null) {
134            observers.removeIf(observer::equals);
135        }
136    }
137
138    /**
139     * Runs this monitor.
140     */
141    @Override
142    public void run() {
143        while (running) {
144            observers.forEach(FileAlterationObserver::checkAndNotify);
145            if (!running) {
146                break;
147            }
148            try {
149                ThreadUtils.sleep(Duration.ofMillis(intervalMillis));
150            } catch (final InterruptedException ignored) {
151                // ignore
152            }
153        }
154    }
155
156    /**
157     * Sets the thread factory.
158     *
159     * @param threadFactory the thread factory
160     */
161    public synchronized void setThreadFactory(final ThreadFactory threadFactory) {
162        this.threadFactory = threadFactory;
163    }
164
165    /**
166     * Starts monitoring.
167     *
168     * @throws Exception if an error occurs initializing the observer
169     */
170    public synchronized void start() throws Exception {
171        if (running) {
172            throw new IllegalStateException("Monitor is already running");
173        }
174        for (final FileAlterationObserver observer : observers) {
175            observer.initialize();
176        }
177        running = true;
178        if (threadFactory != null) {
179            thread = threadFactory.newThread(this);
180        } else {
181            thread = new Thread(this);
182        }
183        thread.start();
184    }
185
186    /**
187     * Stops monitoring.
188     *
189     * @throws Exception if an error occurs initializing the observer
190     */
191    public synchronized void stop() throws Exception {
192        stop(intervalMillis);
193    }
194
195    /**
196     * Stops monitoring.
197     *
198     * @param stopInterval the amount of time in milliseconds to wait for the thread to finish.
199     * A value of zero will wait until the thread is finished (see {@link Thread#join(long)}).
200     * @throws Exception if an error occurs initializing the observer
201     * @since 2.1
202     */
203    public synchronized void stop(final long stopInterval) throws Exception {
204        if (!running) {
205            throw new IllegalStateException("Monitor is not running");
206        }
207        running = false;
208        try {
209            thread.interrupt();
210            thread.join(stopInterval);
211        } catch (final InterruptedException e) {
212            Thread.currentThread().interrupt();
213        }
214        for (final FileAlterationObserver observer : observers) {
215            observer.destroy();
216        }
217    }
218}