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.ArrayList;
021import java.util.Collection;
022import java.util.Collections;
023import java.util.List;
024import java.util.Optional;
025import java.util.concurrent.CopyOnWriteArrayList;
026import java.util.concurrent.ThreadFactory;
027import java.util.stream.Stream;
028
029import org.apache.commons.io.ThreadUtils;
030
031/**
032 * A runnable that spawns a monitoring thread triggering any
033 * registered {@link FileAlterationObserver} at a specified interval.
034 *
035 * @see FileAlterationObserver
036 * @since 2.0
037 */
038public final class FileAlterationMonitor implements Runnable {
039
040    private static final FileAlterationObserver[] EMPTY_ARRAY = {};
041
042    private final long intervalMillis;
043    private final List<FileAlterationObserver> observers = new CopyOnWriteArrayList<>();
044    private Thread thread;
045    private ThreadFactory threadFactory;
046    private volatile boolean running;
047
048    /**
049     * Constructs a monitor with a default interval of 10 seconds.
050     */
051    public FileAlterationMonitor() {
052        this(10_000);
053    }
054
055    /**
056     * Constructs a monitor with the specified interval.
057     *
058     * @param intervalMillis The amount of time in milliseconds to wait between
059     * checks of the file system.
060     */
061    public FileAlterationMonitor(final long intervalMillis) {
062        this.intervalMillis = intervalMillis;
063    }
064
065    /**
066     * Constructs a monitor with the specified interval and collection of observers.
067     *
068     * @param interval The amount of time in milliseconds to wait between
069     * checks of the file system.
070     * @param observers The collection of observers to add to the monitor.
071     * @since 2.9.0
072     */
073    public FileAlterationMonitor(final long interval, final Collection<FileAlterationObserver> observers) {
074        // @formatter:off
075        this(interval,
076            Optional
077                .ofNullable(observers)
078                .orElse(Collections.emptyList())
079                .toArray(EMPTY_ARRAY)
080        );
081        // @formatter:on
082    }
083
084    /**
085     * Constructs a monitor with the specified interval and set of observers.
086     *
087     * @param interval The amount of time in milliseconds to wait between
088     * checks of the file system.
089     * @param observers The set of observers to add to the monitor.
090     */
091    public FileAlterationMonitor(final long interval, final FileAlterationObserver... observers) {
092        this(interval);
093        if (observers != null) {
094            Stream.of(observers).forEach(this::addObserver);
095        }
096    }
097
098    /**
099     * 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}