View Javadoc
1   /**
2    * This file Copyright (c) 2016-2018 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.resourceloader.file;
35  
36  import static info.magnolia.resourceloader.ResourceMatcher.file;
37  import static info.magnolia.resourceloader.ResourceOriginChange.Type.*;
38  import static info.magnolia.resourceloader.ResourceOriginChangeMatcher.resourceChange;
39  import static info.magnolia.resourceloader.file.FileSystemResourceOrigin.RESOURCES_DIR_PROPERTY;
40  import static info.magnolia.test.hamcrest.ExceptionMatcher.instanceOf;
41  import static info.magnolia.test.hamcrest.ExecutionMatcher.throwsAnException;
42  import static java.nio.file.Files.exists;
43  import static org.hamcrest.Matchers.is;
44  import static org.junit.Assert.assertThat;
45  import static org.mockito.Mockito.*;
46  
47  import info.magnolia.dirwatch.DirectoryWatcher;
48  import info.magnolia.dirwatch.DirectoryWatcherService;
49  import info.magnolia.init.MagnoliaConfigurationProperties;
50  import info.magnolia.resourceloader.ResourceChangeHandler;
51  import info.magnolia.resourceloader.ResourceOrigin;
52  
53  import java.io.File;
54  import java.io.FileOutputStream;
55  import java.nio.file.Files;
56  import java.nio.file.Path;
57  import java.nio.file.Paths;
58  import java.nio.file.StandardCopyOption;
59  import java.util.function.Predicate;
60  
61  import org.apache.commons.io.FileUtils;
62  import org.apache.commons.io.IOUtils;
63  import org.junit.Before;
64  import org.junit.Rule;
65  import org.junit.Test;
66  import org.junit.rules.TemporaryFolder;
67  
68  public class FileWatcherCallbackTest {
69  
70      private static final int FILE_OPERATION_TIMEOUT = 30 * 1000;
71      private static final String IGNORED_DIRECTORY = "rsyncstagingdir";
72  
73      @Rule
74      public TemporaryFolder watchedDir = new TemporaryFolder();
75  
76      @Rule
77      public TemporaryFolder nonWatchedDir = new TemporaryFolder();
78  
79      private Predicate<Path> watchedPathFilter = path -> !path.endsWith(IGNORED_DIRECTORY);
80  
81      private FileSystemResourceOrigin fileSystemResourceOrigin;
82      private DirectoryWatcher watcher;
83      private FileWatcherCallback fileWatcherCallback;
84  
85      @Before
86      public void setUp() throws Exception {
87          final MagnoliaConfigurationProperties mcp = mock(MagnoliaConfigurationProperties.class);
88          doReturn(watchedDir.getRoot().toString()).when(mcp).getProperty(RESOURCES_DIR_PROPERTY);
89  
90          watcher = new DirectoryWatcher(true, true, mcp);
91          this.fileSystemResourceOrigin = new FileSystemResourceOrigin(mcp, mock(DirectoryWatcherService.class), "foo");
92  
93          fileWatcherCallback = new FileWatcherCallback(fileSystemResourceOrigin, watchedPathFilter);
94          watcher.register(watchedDir.getRoot().toPath(), watchedPathFilter, fileWatcherCallback);
95  
96          final Thread watcherThread = new Thread(watcher);
97          watcherThread.start();
98      }
99  
100     @Test
101     public void communicatesFolderCreationAndDeletion() throws Exception {
102         // GIVEN
103         // register a resource change handler
104         final ResourceChangeHandler handler = mock(ResourceChangeHandler.class);
105         fileSystemResourceOrigin.registerResourceChangeHandler(handler);
106 
107         // WHEN
108         // create a folder and a couple of files within it
109         final File qux = watchedDir.newFolder("qux");
110         final Path quux = Files.createFile(qux.toPath().resolve("quux"));
111         final Path xyzzy = Files.createFile(qux.toPath().resolve("xyzzy"));
112 
113         // THEN
114         // both files exist
115         assertThat(quux.toFile().exists(), is(true));
116         assertThat(xyzzy.toFile().exists(), is(true));
117 
118         // corresponding creation changes are communicated
119         verify(handler, timeout(FILE_OPERATION_TIMEOUT)).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/qux")));
120 
121         // The following two verifies are marked with atLeast(1) so that we adapt to the cases when the sub-files of a deleted folder are also reported (not in OS X, but in Linux)
122         // otherwise the test will think there were way too many invocations and fail...
123         verify(handler, timeout(FILE_OPERATION_TIMEOUT).atLeast(1)).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/qux/quux")));
124         verify(handler, timeout(FILE_OPERATION_TIMEOUT).atLeast(1)).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/qux/xyzzy")));
125 
126         // WHEN
127         // delete the directory
128         FileUtils.deleteDirectory(qux);
129 
130         // THEN
131         // directory removal is communicated, but unfortunately the contents removal - isn't. One event for the directory
132         // itself is all we can expect
133         verify(handler, timeout(FILE_OPERATION_TIMEOUT)).onResourceChanged(argThat(resourceChange().ofType(REMOVED).at("/qux")));
134     }
135 
136     @Test
137     public void communicatesAllPossibleFileChangeEvents() throws Exception {
138         // GIVEN
139         // register a resource change handler
140         final ResourceChangeHandler handler = mock(ResourceChangeHandler.class);
141         fileSystemResourceOrigin.registerResourceChangeHandler(handler);
142 
143         // WHEN
144         File foo = watchedDir.newFile("foo");
145 
146         // THEN
147         verify(handler, timeout(FILE_OPERATION_TIMEOUT)).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/foo")));
148 
149         // WHEN
150         // modify the file
151         final FileOutputStream fos = new FileOutputStream(foo);
152         fos.write(1);
153         IOUtils.closeQuietly(fos);
154 
155         // THEN
156         verify(handler, timeout(FILE_OPERATION_TIMEOUT)).onResourceChanged(argThat(resourceChange().ofType(MODIFIED).at("/foo")));
157 
158         // WHEN
159         // file is deleted
160         foo.delete();
161 
162         // THEN
163         verify(handler, timeout(FILE_OPERATION_TIMEOUT)).onResourceChanged(argThat(resourceChange().ofType(REMOVED).at("/foo")));
164     }
165 
166     @Test
167     public void movedDirectoryIsDetected() throws Exception {
168         // GIVEN
169         final ResourceChangeHandler resourceChangeHandler = mock(ResourceChangeHandler.class);
170         fileSystemResourceOrigin.registerResourceChangeHandler(resourceChangeHandler);
171 
172         // WHEN
173         // first create a folder an a file in it
174         final Path foo = watchedDir.newFolder("foo").toPath();
175         Files.createFile(foo.resolve("bar.yaml"));
176 
177         // THEN make sure at least file creation is communicated (let's save a bit on timeouts)
178 
179         // The following verify is marked with atLeast(1) so that we adapt to the case when the sub-files of a deleted folder are also reported (not in OS X, but in Linux)
180         // otherwise the test will think there were way too many invocations and fail...
181         verify(resourceChangeHandler, timeout(FILE_OPERATION_TIMEOUT).atLeast(1)).onResourceChanged(argThat(resourceChange().at("/foo/bar.yaml").ofType(ADDED)));
182 
183         // THEN create another folder
184         final Path bar = Files.createDirectory(watchedDir.getRoot().toPath().resolve("bar"));
185         // make sure no file with the same name as in foo exists in bar
186         assertThat(() -> fileSystemResourceOrigin.getByPath("/bar/bar.yaml"),
187                 throwsAnException(instanceOf(ResourceOrigin.ResourceNotFoundException.class)));
188 
189         // WHEN whole folder is moved
190         Files.move(foo, bar, StandardCopyOption.REPLACE_EXISTING);
191 
192         // THEN old folder is destroyed
193         verify(resourceChangeHandler, timeout(FILE_OPERATION_TIMEOUT).atLeastOnce()).onResourceChanged(argThat(resourceChange().at("/foo").ofType(REMOVED)));
194         // New file is created
195         verify(resourceChangeHandler, timeout(FILE_OPERATION_TIMEOUT).atLeastOnce()).onResourceChanged(argThat(resourceChange().at("/bar/bar.yaml").ofType(ADDED)));
196 
197         assertThat(fileSystemResourceOrigin.getByPath("/bar/bar.yaml"), file().withPath("/bar/bar.yaml"));
198     }
199 
200     @Test
201     public void subTreeIsSkippedWhenFileCantBeRead() throws Exception {
202         // GIVEN
203         final ResourceChangeHandler handler = mock(ResourceChangeHandler.class);
204         fileSystemResourceOrigin.registerResourceChangeHandler(handler);
205 
206         // WHEN
207         // let's create a folder with a file inside of it
208         File toCreateDir = watchedDir.newFolder("qux");
209         Files.createFile(toCreateDir.toPath().resolve("bar.yaml"));
210         assertThat(fileSystemResourceOrigin.getByPath("/qux/bar.yaml"), file().withPath("/qux/bar.yaml"));
211 
212         // THEN
213         // so far, so good
214         verify(handler, timeout(FILE_OPERATION_TIMEOUT)).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/qux")));
215 
216         // WHEN
217         // remove rights on the folder
218         toCreateDir.setReadable(false);
219         toCreateDir.setWritable(false);
220         toCreateDir.setExecutable(false);
221 
222         // THEN
223         // file is no longer accessible
224         assertThat(() -> fileSystemResourceOrigin.getByPath("/qux/bar.yaml"), throwsAnException(instanceOf(ResourceOrigin.ResourceNotFoundException.class)));
225     }
226 
227     @Test
228     public void watchedPathFilterDiscardsIgnoredElementsRegistration() throws Exception {
229         // GIVEN
230         final ResourceChangeHandler handler = mock(ResourceChangeHandler.class);
231         fileSystemResourceOrigin.registerResourceChangeHandler(handler);
232 
233         // WHEN
234         // let's create an ignored directory with files
235         final File qux = watchedDir.newFolder(IGNORED_DIRECTORY);
236         final Path quux = Files.createFile(qux.toPath().resolve("quux"));
237         final Path xyzzy = Files.createFile(qux.toPath().resolve("xyzzy"));
238         assertThat(quux.toFile().exists(), is(true));
239         assertThat(xyzzy.toFile().exists(), is(true));
240 
241         // THEN
242         // no events is what we want
243         verify(handler, never()).onResourceChanged(argThat(resourceChange().ofType(ADDED).at(getIgnoredDirectoryPath(""))));
244         verify(handler, never()).onResourceChanged(argThat(resourceChange().ofType(ADDED).at(getIgnoredDirectoryPath("/quux"))));
245         verify(handler, never()).onResourceChanged(argThat(resourceChange().ofType(ADDED).at(getIgnoredDirectoryPath("/xyzzy"))));
246     }
247 
248     @Test
249     public void allowsDirectoryWatchingViaSymbolicLinks() throws Exception {
250         // GIVEN
251         final ResourceChangeHandler handler = mock(ResourceChangeHandler.class);
252         fileSystemResourceOrigin.registerResourceChangeHandler(handler);
253 
254         // create a symbolic link and make it point at a non-watched folder containing one file
255         final Path symLinkPath = watchedDir.getRoot().toPath().resolve(Paths.get("symLink"));
256         final Path symLinkTarget = nonWatchedDir.newFolder("symLinkTarget").toPath();
257         Files.createFile(symLinkTarget.resolve(Paths.get("bar.yaml")));
258         final Path symbolicLink = Files.createSymbolicLink(symLinkPath, symLinkTarget);
259 
260         // make sure that the resulting path is visible via a sym link
261         final Path barPathViaSymLink = symbolicLink.resolve(Paths.get("bar.yaml"));
262         assertThat(exists(barPathViaSymLink), is(true));
263 
264         // WHEN
265         // a symbolic link is added to the watched dir
266         watcher.register(symbolicLink, watchedPathFilter, fileWatcherCallback);
267 
268         // THEN
269         // make sure the corresponding event is registered by our watcher upon adding the link
270         verify(handler, timeout(FILE_OPERATION_TIMEOUT).atLeastOnce()).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/symLink/bar.yaml")));
271 
272         // WHEN
273         // another file is added to the sym link target
274         Files.createFile(symLinkTarget.resolve(Paths.get("foo.yaml")));
275 
276         // make sure that the resulting path is visible via a sym link
277         final Path fooPathViaSymLink = symbolicLink.resolve(Paths.get("foo.yaml"));
278         assertThat(exists(fooPathViaSymLink), is(true));
279 
280         // THEN
281         // a corresponding event is registered by our watcher
282         verify(handler, timeout(FILE_OPERATION_TIMEOUT).atLeastOnce()).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/symLink/foo.yaml")));
283     }
284 
285     private String getIgnoredDirectoryPath(String path) {
286         Path ignoredDirectoryPath = Paths.get(IGNORED_DIRECTORY);
287         return ignoredDirectoryPath.resolve(path).toString();
288     }
289 }