View Javadoc
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    *      http://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.vfs2.impl;
18  
19  import static org.apache.commons.vfs2.VfsTestUtils.getTestDirectoryFile;
20  import static org.junit.jupiter.api.Assertions.assertEquals;
21  import static org.junit.jupiter.api.Assertions.assertNotNull;
22  import static org.junit.jupiter.api.Assertions.assertNull;
23  import static org.junit.jupiter.api.Assertions.assertTrue;
24  
25  import java.io.File;
26  import java.io.IOException;
27  import java.nio.charset.StandardCharsets;
28  import java.nio.file.Files;
29  import java.util.ArrayDeque;
30  import java.util.Deque;
31  import java.util.Objects;
32  import java.util.concurrent.atomic.AtomicLong;
33  
34  import org.apache.commons.vfs2.FileChangeEvent;
35  import org.apache.commons.vfs2.FileListener;
36  import org.apache.commons.vfs2.FileObject;
37  import org.apache.commons.vfs2.FileSystemManager;
38  import org.apache.commons.vfs2.VFS;
39  import org.junit.jupiter.api.AfterEach;
40  import org.junit.jupiter.api.BeforeEach;
41  import org.junit.jupiter.api.Disabled;
42  import org.junit.jupiter.api.Test;
43  
44  /**
45   * Tests {@link DefaultFileMonitor}.
46   */
47  public class DefaultFileMonitorTest {
48  
49      private static class CountingListener implements FileListener {
50          private final AtomicLong changed = new AtomicLong();
51          private final AtomicLong created = new AtomicLong();
52          private final AtomicLong deleted = new AtomicLong();
53  
54          @Override
55          public void fileChanged(final FileChangeEvent event) {
56              changed.incrementAndGet();
57          }
58  
59          @Override
60          public void fileCreated(final FileChangeEvent event) {
61              created.incrementAndGet();
62          }
63  
64          @Override
65          public void fileDeleted(final FileChangeEvent event) {
66              deleted.incrementAndGet();
67          }
68      }
69  
70      private enum PeekLocation {
71          FIRST, LAST
72      }
73  
74      private enum Status {
75          CHANGED, CREATED, DELETED
76      }
77  
78      private class TestFileListener implements FileListener {
79  
80          @Override
81          public void fileChanged(final FileChangeEvent event) throws Exception {
82              status.add(Status.CHANGED);
83          }
84  
85          @Override
86          public void fileCreated(final FileChangeEvent event) throws Exception {
87              status.add(Status.CREATED);
88          }
89  
90          @Override
91          public void fileDeleted(final FileChangeEvent event) throws Exception {
92              status.add(Status.DELETED);
93          }
94      }
95  
96      private static final int DELAY_MILLIS = 100;
97  
98      private FileSystemManager fileSystemManager;
99  
100     private final Deque<Status> status = new ArrayDeque<>();
101 
102     private File testDir;
103 
104     private File testFile;
105 
106     private void deleteTestFileIfPresent() {
107         if (testFile != null && testFile.exists()) {
108             final boolean deleted = testFile.delete();
109             assertTrue(deleted, testFile.toString());
110         }
111     }
112 
113     private Status getStatus(final PeekLocation peekLocation) {
114         switch (Objects.requireNonNull(peekLocation, "peekLocation")) {
115         case FIRST:
116             return status.peekFirst();
117         case LAST:
118             return status.peekLast();
119         }
120         throw new IllegalStateException();
121     }
122 
123     private void resetStatus() {
124         status.clear();
125     }
126 
127     @BeforeEach
128     public void setUp() throws Exception {
129         fileSystemManager = VFS.getManager();
130         testDir = getTestDirectoryFile();
131         resetStatus();
132         testFile = new File(testDir, "testReload.properties");
133         deleteTestFileIfPresent();
134     }
135 
136     @AfterEach
137     public void tearDown() {
138         deleteTestFileIfPresent();
139     }
140 
141     @Test
142     public void testChildFileDeletedWithoutRecursiveChecking() throws Exception {
143         writeToFile(testFile);
144         try (FileObject fileObject = fileSystemManager.resolveFile(testDir.toURI().toURL().toString())) {
145             try (DefaultFileMonitor monitor = new DefaultFileMonitor(new TestFileListener())) {
146                 monitor.setDelay(2000);
147                 monitor.setRecursive(false);
148                 monitor.addFile(fileObject);
149                 monitor.start();
150                 resetStatus();
151                 Thread.sleep(DELAY_MILLIS * 5);
152                 testFile.delete();
153                 Thread.sleep(DELAY_MILLIS * 30);
154                 assertNull(getStatus(PeekLocation.LAST), "Event should not have occurred");
155             }
156         }
157     }
158 
159     @Test
160     public void testChildFileRecreated() throws Exception {
161         writeToFile(testFile);
162         try (FileObject fileObj = fileSystemManager.resolveFile(testDir.toURI().toURL().toString())) {
163             try (DefaultFileMonitor monitor = new DefaultFileMonitor(new TestFileListener())) {
164                 monitor.setDelay(2000);
165                 monitor.setRecursive(true);
166                 monitor.addFile(fileObj);
167                 monitor.start();
168                 resetStatus();
169                 Thread.sleep(DELAY_MILLIS * 5);
170                 testFile.delete();
171                 waitFor(Status.DELETED, DELAY_MILLIS * 30, PeekLocation.LAST);
172                 resetStatus();
173                 Thread.sleep(DELAY_MILLIS * 5);
174                 writeToFile(testFile);
175                 waitFor(Status.CREATED, DELAY_MILLIS * 30, PeekLocation.LAST);
176             }
177         }
178     }
179 
180     @Test
181     public void testFileCreated() throws Exception {
182         try (FileObject fileObject = fileSystemManager.resolveFile(testFile.toURI().toURL().toString())) {
183             try (DefaultFileMonitor monitor = new DefaultFileMonitor(new TestFileListener())) {
184                 // TestFileListener manipulates status
185                 monitor.setDelay(DELAY_MILLIS);
186                 monitor.addFile(fileObject);
187                 monitor.start();
188                 writeToFile(testFile);
189                 Thread.sleep(DELAY_MILLIS * 5);
190                 waitFor(Status.CREATED, DELAY_MILLIS * 5, PeekLocation.FIRST);
191             }
192         }
193     }
194 
195     @Test
196     public void testFileDeleted() throws Exception {
197         writeToFile(testFile);
198         try (FileObject fileObject = fileSystemManager.resolveFile(testFile.toURI().toString())) {
199             try (DefaultFileMonitor monitor = new DefaultFileMonitor(new TestFileListener())) {
200                 // TestFileListener manipulates status
201                 monitor.setDelay(DELAY_MILLIS);
202                 monitor.addFile(fileObject);
203                 monitor.start();
204                 testFile.delete();
205                 waitFor(Status.DELETED, DELAY_MILLIS * 5, PeekLocation.LAST);
206             }
207         }
208     }
209 
210     @Test
211     public void testFileModified() throws Exception {
212         writeToFile(testFile);
213         try (FileObject fileObject = fileSystemManager.resolveFile(testFile.toURI().toURL().toString())) {
214             try (DefaultFileMonitor monitor = new DefaultFileMonitor(new TestFileListener())) {
215                 // TestFileListener manipulates status
216                 monitor.setDelay(DELAY_MILLIS);
217                 monitor.addFile(fileObject);
218                 monitor.start();
219                 // Need a long delay to insure the new timestamp doesn't truncate to be the same as
220                 // the current timestamp. Java only guarantees the timestamp will be to 1 second.
221                 Thread.sleep(DELAY_MILLIS * 10);
222                 final long valueMillis = System.currentTimeMillis();
223                 final boolean rcMillis = testFile.setLastModified(valueMillis);
224                 assertTrue(rcMillis, "setLastModified succeeded");
225                 waitFor(Status.CHANGED, DELAY_MILLIS * 5, PeekLocation.LAST);
226             }
227         }
228     }
229 
230     @Test
231     public void testFileMonitorRestarted() throws Exception {
232         try (FileObject fileObject = fileSystemManager.resolveFile(testFile.toURI().toString())) {
233             final DefaultFileMonitor monitor = new DefaultFileMonitor(new TestFileListener());
234             try {
235                 // TestFileListener manipulates status
236                 monitor.setDelay(DELAY_MILLIS);
237                 monitor.addFile(fileObject);
238 
239                 monitor.start();
240                 writeToFile(testFile);
241                 Thread.sleep(DELAY_MILLIS * 5);
242             } finally {
243                 monitor.stop();
244             }
245 
246             monitor.start();
247             try {
248                 testFile.delete();
249                 waitFor(Status.DELETED, DELAY_MILLIS * 5, PeekLocation.LAST);
250             } finally {
251                 monitor.stop();
252             }
253         }
254     }
255 
256     @Test
257     public void testFileRecreated() throws Exception {
258         try (FileObject fileObject = fileSystemManager.resolveFile(testFile.toURI())) {
259             try (DefaultFileMonitor monitor = new DefaultFileMonitor(new TestFileListener())) {
260                 // TestFileListener manipulates status
261                 monitor.setDelay(DELAY_MILLIS);
262                 monitor.addFile(fileObject);
263                 monitor.start();
264                 writeToFile(testFile);
265                 waitFor(Status.CREATED, DELAY_MILLIS * 10, PeekLocation.LAST);
266                 resetStatus();
267                 testFile.delete();
268                 waitFor(Status.DELETED, DELAY_MILLIS * 10, PeekLocation.LAST);
269                 resetStatus();
270                 Thread.sleep(DELAY_MILLIS * 5);
271                 monitor.addFile(fileObject);
272                 writeToFile(testFile);
273                 waitFor(Status.CREATED, DELAY_MILLIS * 10, PeekLocation.LAST);
274             }
275         }
276     }
277 
278     /**
279      * VFS-299: Handlers are not removed. One instance is {@link DefaultFileMonitor#removeFile(FileObject)}.
280      *
281      * As a result, the file monitor will fire two created events.
282      */
283     @Disabled("VFS-299")
284     @Test
285     public void testIgnoreTestAddRemove() throws Exception {
286         try (FileObject fileObject = fileSystemManager.resolveFile(testFile.toURI().toString())) {
287             final CountingListener listener = new CountingListener();
288             try (DefaultFileMonitor monitor = new DefaultFileMonitor(listener)) {
289                 monitor.setDelay(DELAY_MILLIS);
290                 monitor.addFile(fileObject);
291                 monitor.removeFile(fileObject);
292                 monitor.addFile(fileObject);
293                 monitor.start();
294                 writeToFile(testFile);
295                 Thread.sleep(DELAY_MILLIS * 3);
296                 assertEquals(1, listener.created.get(), "Created event is only fired once");
297             }
298         }
299     }
300 
301     /**
302      * VFS-299: Handlers are not removed. There is no API for properly decommissioning a file monitor.
303      *
304      * As a result, listeners of stopped monitors still receive events.
305      */
306     @Disabled("VFS-299")
307     @Test
308     public void testIgnoreTestStartStop() throws Exception {
309         try (FileObject fileObject = fileSystemManager.resolveFile(testFile.toURI().toString())) {
310             final CountingListener stoppedListener = new CountingListener();
311             try (DefaultFileMonitor stoppedMonitor = new DefaultFileMonitor(stoppedListener)) {
312                 stoppedMonitor.start();
313                 stoppedMonitor.addFile(fileObject);
314             }
315 
316             // Variant 1: it becomes documented behavior to manually remove all files after stop() such that all
317             // listeners
318             // are removed
319             // This currently does not work, see DefaultFileMonitorTests#testAddRemove above.
320             // stoppedMonitor.removeFile(file);
321 
322             // Variant 2: change behavior of stop(), which then removes all handlers.
323             // This would remove the possibility to pause watching files. Resuming watching for the same files via
324             // start();
325             // stop(); start(); would not work.
326 
327             // Variant 3: introduce new method DefaultFileMonitor#close which definitely removes all resources held by
328             // DefaultFileMonitor.
329 
330             final CountingListener activeListener = new CountingListener();
331             try (DefaultFileMonitor activeMonitor = new DefaultFileMonitor(activeListener)) {
332                 activeMonitor.setDelay(DELAY_MILLIS);
333                 activeMonitor.addFile(fileObject);
334                 activeMonitor.start();
335                 writeToFile(testFile);
336                 Thread.sleep(DELAY_MILLIS * 10);
337 
338                 assertEquals(1, activeListener.created.get(), "The listener of the active monitor received one created event");
339                 assertEquals(0, stoppedListener.created.get(), "The listener of the stopped monitor received no events");
340             }
341         }
342     }
343 
344     private void waitFor(final Status expected, final long timeoutMillis, final PeekLocation peekLocation) throws InterruptedException {
345         if (expected == getStatus(peekLocation)) {
346             return;
347         }
348         long remaining = timeoutMillis;
349         final long interval = timeoutMillis / 10;
350         while (remaining > 0) {
351             Thread.sleep(interval);
352             remaining -= interval;
353             if (expected == getStatus(peekLocation)) {
354                 return;
355             }
356         }
357         assertNotNull(getStatus(peekLocation), "No event occurred");
358         assertEquals(expected, getStatus(peekLocation), "Incorrect event " + getStatus(peekLocation));
359     }
360 
361     private void writeToFile(final File file) throws IOException {
362         Files.write(file.toPath(), "string=value1".getBytes(StandardCharsets.UTF_8));
363     }
364 
365 }