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  
60  import org.apache.commons.io.FileUtils;
61  import org.apache.commons.io.IOUtils;
62  import org.junit.Before;
63  import org.junit.Rule;
64  import org.junit.Test;
65  import org.junit.rules.TemporaryFolder;
66  
67  import com.google.common.base.Predicate;
68  
69  public class FileWatcherCallbackTest {
70  
71      private static final int FILE_OPERATION_TIMEOUT = 30 * 1000;
72      private static final String IGNORED_DIRECTORY = "rsyncstagingdir";
73  
74      @Rule
75      public TemporaryFolder watchedDir = new TemporaryFolder();
76  
77      @Rule
78      public TemporaryFolder nonWatchedDir = new TemporaryFolder();
79  
80      private Predicate<Path> watchedPathFilter = path -> !path.endsWith(IGNORED_DIRECTORY);
81  
82      private FileSystemResourceOrigin fileSystemResourceOrigin;
83      private DirectoryWatcher watcher;
84      private FileWatcherCallback fileWatcherCallback;
85  
86      @Before
87      public void setUp() throws Exception {
88          final MagnoliaConfigurationProperties mcp = mock(MagnoliaConfigurationProperties.class);
89          doReturn(watchedDir.getRoot().toString()).when(mcp).getProperty(RESOURCES_DIR_PROPERTY);
90  
91          watcher = new DirectoryWatcher(true, true, mcp);
92          this.fileSystemResourceOrigin = new FileSystemResourceOrigin(mcp, mock(DirectoryWatcherService.class), "foo");
93  
94          fileWatcherCallback = new FileWatcherCallback(fileSystemResourceOrigin, watchedPathFilter);
95          watcher.register(watchedDir.getRoot().toPath(), watchedPathFilter, fileWatcherCallback);
96  
97          final Thread watcherThread = new Thread(watcher);
98          watcherThread.start();
99      }
100 
101     @Test
102     public void communicatesFolderCreationAndDeletion() throws Exception {
103         // GIVEN
104         // register a resource change handler
105         final ResourceChangeHandler handler = mock(ResourceChangeHandler.class);
106         fileSystemResourceOrigin.registerResourceChangeHandler(handler);
107 
108         // WHEN
109         // create a folder and a couple of files within it
110         final File qux = watchedDir.newFolder("qux");
111         final Path quux = Files.createFile(qux.toPath().resolve("quux"));
112         final Path xyzzy = Files.createFile(qux.toPath().resolve("xyzzy"));
113 
114         // THEN
115         // both files exist
116         assertThat(quux.toFile().exists(), is(true));
117         assertThat(xyzzy.toFile().exists(), is(true));
118 
119         // corresponding creation changes are communicated
120         verify(handler, timeout(FILE_OPERATION_TIMEOUT)).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/qux")));
121 
122         // 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)
123         // otherwise the test will think there were way too many invocations and fail...
124         verify(handler, timeout(FILE_OPERATION_TIMEOUT).atLeast(1)).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/qux/quux")));
125         verify(handler, timeout(FILE_OPERATION_TIMEOUT).atLeast(1)).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/qux/xyzzy")));
126 
127         // WHEN
128         // delete the directory
129         FileUtils.deleteDirectory(qux);
130 
131         // THEN
132         // directory removal is communicated, but unfortunately the contents removal - isn't. One event for the directory
133         // itself is all we can expect
134         verify(handler, timeout(FILE_OPERATION_TIMEOUT)).onResourceChanged(argThat(resourceChange().ofType(REMOVED).at("/qux")));
135     }
136 
137     @Test
138     public void communicatesAllPossibleFileChangeEvents() throws Exception {
139         // GIVEN
140         // register a resource change handler
141         final ResourceChangeHandler handler = mock(ResourceChangeHandler.class);
142         fileSystemResourceOrigin.registerResourceChangeHandler(handler);
143 
144         // WHEN
145         File foo = watchedDir.newFile("foo");
146 
147         // THEN
148         verify(handler, timeout(FILE_OPERATION_TIMEOUT)).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/foo")));
149 
150         // WHEN
151         // modify the file
152         final FileOutputStream fos = new FileOutputStream(foo);
153         fos.write(1);
154         IOUtils.closeQuietly(fos);
155 
156         // THEN
157         verify(handler, timeout(FILE_OPERATION_TIMEOUT)).onResourceChanged(argThat(resourceChange().ofType(MODIFIED).at("/foo")));
158 
159         // WHEN
160         // file is deleted
161         foo.delete();
162 
163         // THEN
164         verify(handler, timeout(FILE_OPERATION_TIMEOUT)).onResourceChanged(argThat(resourceChange().ofType(REMOVED).at("/foo")));
165     }
166 
167     @Test
168     public void movedDirectoryIsDetected() throws Exception {
169         // GIVEN
170         final ResourceChangeHandler resourceChangeHandler = mock(ResourceChangeHandler.class);
171         fileSystemResourceOrigin.registerResourceChangeHandler(resourceChangeHandler);
172 
173         // WHEN
174         // first create a folder an a file in it
175         final Path foo = watchedDir.newFolder("foo").toPath();
176         Files.createFile(foo.resolve("bar.yaml"));
177 
178         // THEN make sure at least file creation is communicated (let's save a bit on timeouts)
179 
180         // 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)
181         // otherwise the test will think there were way too many invocations and fail...
182         verify(resourceChangeHandler, timeout(FILE_OPERATION_TIMEOUT).atLeast(1)).onResourceChanged(argThat(resourceChange().at("/foo/bar.yaml").ofType(ADDED)));
183 
184         // THEN create another folder
185         final Path bar = Files.createDirectory(watchedDir.getRoot().toPath().resolve("bar"));
186         // make sure no file with the same name as in foo exists in bar
187         assertThat(() -> fileSystemResourceOrigin.getByPath("/bar/bar.yaml"),
188                 throwsAnException(instanceOf(ResourceOrigin.ResourceNotFoundException.class)));
189 
190         // WHEN whole folder is moved
191         Files.move(foo, bar, StandardCopyOption.REPLACE_EXISTING);
192 
193         // THEN old folder is destroyed
194         verify(resourceChangeHandler, timeout(FILE_OPERATION_TIMEOUT).atLeastOnce()).onResourceChanged(argThat(resourceChange().at("/foo").ofType(REMOVED)));
195         // New file is created
196         verify(resourceChangeHandler, timeout(FILE_OPERATION_TIMEOUT).atLeastOnce()).onResourceChanged(argThat(resourceChange().at("/bar/bar.yaml").ofType(ADDED)));
197 
198         assertThat(fileSystemResourceOrigin.getByPath("/bar/bar.yaml"), file().withPath("/bar/bar.yaml"));
199     }
200 
201     @Test
202     public void subTreeIsSkippedWhenFileCantBeRead() throws Exception {
203         // GIVEN
204         final ResourceChangeHandler handler = mock(ResourceChangeHandler.class);
205         fileSystemResourceOrigin.registerResourceChangeHandler(handler);
206 
207         // WHEN
208         // let's create a folder with a file inside of it
209         File toCreateDir = watchedDir.newFolder("qux");
210         Files.createFile(toCreateDir.toPath().resolve("bar.yaml"));
211         assertThat(fileSystemResourceOrigin.getByPath("/qux/bar.yaml"), file().withPath("/qux/bar.yaml"));
212 
213         // THEN
214         // so far, so good
215         verify(handler, timeout(FILE_OPERATION_TIMEOUT)).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/qux")));
216 
217         // WHEN
218         // remove rights on the folder
219         toCreateDir.setReadable(false);
220         toCreateDir.setWritable(false);
221         toCreateDir.setExecutable(false);
222 
223         // THEN
224         // file is no longer accessible
225         assertThat(() -> fileSystemResourceOrigin.getByPath("/qux/bar.yaml"), throwsAnException(instanceOf(ResourceOrigin.ResourceNotFoundException.class)));
226     }
227 
228     @Test
229     public void watchedPathFilterDiscardsIgnoredElementsRegistration() throws Exception {
230         // GIVEN
231         final ResourceChangeHandler handler = mock(ResourceChangeHandler.class);
232         fileSystemResourceOrigin.registerResourceChangeHandler(handler);
233 
234         // WHEN
235         // let's create an ignored directory with files
236         final File qux = watchedDir.newFolder(IGNORED_DIRECTORY);
237         final Path quux = Files.createFile(qux.toPath().resolve("quux"));
238         final Path xyzzy = Files.createFile(qux.toPath().resolve("xyzzy"));
239         assertThat(quux.toFile().exists(), is(true));
240         assertThat(xyzzy.toFile().exists(), is(true));
241 
242         // THEN
243         // no events is what we want
244         verify(handler, never()).onResourceChanged(argThat(resourceChange().ofType(ADDED).at(getIgnoredDirectoryPath(""))));
245         verify(handler, never()).onResourceChanged(argThat(resourceChange().ofType(ADDED).at(getIgnoredDirectoryPath("/quux"))));
246         verify(handler, never()).onResourceChanged(argThat(resourceChange().ofType(ADDED).at(getIgnoredDirectoryPath("/xyzzy"))));
247     }
248 
249     @Test
250     public void allowsDirectoryWatchingViaSymbolicLinks() throws Exception {
251         // GIVEN
252         final ResourceChangeHandler handler = mock(ResourceChangeHandler.class);
253         fileSystemResourceOrigin.registerResourceChangeHandler(handler);
254 
255         // create a symbolic link and make it point at a non-watched folder containing one file
256         final Path symLinkPath = watchedDir.getRoot().toPath().resolve(Paths.get("symLink"));
257         final Path symLinkTarget = nonWatchedDir.newFolder("symLinkTarget").toPath();
258         Files.createFile(symLinkTarget.resolve(Paths.get("bar.yaml")));
259         final Path symbolicLink = Files.createSymbolicLink(symLinkPath, symLinkTarget);
260 
261         // make sure that the resulting path is visible via a sym link
262         final Path barPathViaSymLink = symbolicLink.resolve(Paths.get("bar.yaml"));
263         assertThat(exists(barPathViaSymLink), is(true));
264 
265         // WHEN
266         // a symbolic link is added to the watched dir
267         watcher.register(symbolicLink, watchedPathFilter, fileWatcherCallback);
268 
269         // THEN
270         // make sure the corresponding event is registered by our watcher upon adding the link
271         verify(handler, timeout(FILE_OPERATION_TIMEOUT).atLeastOnce()).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/symLink/bar.yaml")));
272 
273         // WHEN
274         // another file is added to the sym link target
275         Files.createFile(symLinkTarget.resolve(Paths.get("foo.yaml")));
276 
277         // make sure that the resulting path is visible via a sym link
278         final Path fooPathViaSymLink = symbolicLink.resolve(Paths.get("foo.yaml"));
279         assertThat(exists(fooPathViaSymLink), is(true));
280 
281         // THEN
282         // a corresponding event is registered by our watcher
283         verify(handler, timeout(FILE_OPERATION_TIMEOUT).atLeastOnce()).onResourceChanged(argThat(resourceChange().ofType(ADDED).at("/symLink/foo.yaml")));
284     }
285 
286     private String getIgnoredDirectoryPath(String path) {
287         Path ignoredDirectoryPath = Paths.get(IGNORED_DIRECTORY);
288         return ignoredDirectoryPath.resolve(path).toString();
289     }
290 }