View Javadoc
1   /**
2    * This file Copyright (c) 2015-2016 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 info.magnolia.cms.util.ExceptionUtil;
37  import info.magnolia.dirwatch.DirectoryWatcherService;
38  import info.magnolia.init.MagnoliaConfigurationProperties;
39  import info.magnolia.resourceloader.AbstractResourceOrigin;
40  import info.magnolia.resourceloader.ResourceOriginFactory;
41  import info.magnolia.resourceloader.ResourceVisitor;
42  
43  import java.io.IOException;
44  import java.io.InputStream;
45  import java.nio.charset.Charset;
46  import java.nio.file.DirectoryStream;
47  import java.nio.file.Files;
48  import java.nio.file.LinkOption;
49  import java.nio.file.Path;
50  import java.nio.file.Paths;
51  import java.nio.file.attribute.FileTime;
52  import java.util.Arrays;
53  import java.util.Collections;
54  import java.util.List;
55  
56  import org.apache.commons.io.FilenameUtils;
57  import org.apache.commons.lang3.StringUtils;
58  import org.slf4j.Logger;
59  import org.slf4j.LoggerFactory;
60  
61  import com.google.auto.factory.AutoFactory;
62  import com.google.auto.factory.Provided;
63  import com.google.common.base.Function;
64  import com.google.common.base.Preconditions;
65  import com.google.common.collect.Iterables;
66  import com.google.common.collect.Lists;
67  
68  /**
69   * A {@link info.magnolia.resourceloader.ResourceOrigin} which loads resources on the file system.
70   */
71  @AutoFactory(implementing = ResourceOriginFactory.class)
72  public class FileSystemResourceOrigin extends AbstractResourceOrigin<FileSystemResource> {
73      private static final Logger log = LoggerFactory.getLogger(FileSystemResourceOrigin.class);
74  
75      public static final String RESOURCES_DIR_PROPERTY = "magnolia.resources.dir";
76      private static final LinkOption[] LINK_OPTIONS = new LinkOption[0]; // Use LinkOption.NOFOLLOW_LINKS if we don't follow links
77      private static final List<String> EXCLUDED_DIRECTORIES = Arrays.asList("META-INF", "WEB-INF", "cache", "docroot", "logs", "repositories", "tmp");
78  
79      private final Path rootPath;
80      private final DirectoryWatcherService directoryWatcherService;
81  
82      private final ExclusionsFilter exclusionsFilter;
83      private final Function<Path, FileSystemResource> transformFunction;
84      private final String excludedDirectories;
85  
86      FileSystemResourceOrigin(@Provided MagnoliaConfigurationProperties mcp, @Provided DirectoryWatcherService directoryWatcherService, String name) {
87          super(name);
88  
89          String resourcesDir = mcp.getProperty(RESOURCES_DIR_PROPERTY);
90          if (resourcesDir == null) {
91              log.warn("Could not find magnolia.resources.dir property, please update your magnolia.properties. Falling back to magnolia.home.");
92              resourcesDir = mcp.getProperty("magnolia.home");
93          }
94          this.excludedDirectories = mcp.getProperty("magnolia.resources.filesystem.observation.excludedDirectories");
95          this.rootPath = validateRootPath(resourcesDir);
96          this.directoryWatcherService = directoryWatcherService;
97          this.exclusionsFilter = buildExclusionsFilter();
98          this.transformFunction = new Function<Path, FileSystemResource>() {
99              @Override
100             public FileSystemResource apply(Path path) {
101                 return newResource(path);
102             }
103         };
104     }
105 
106     /**
107      * Builds exclusion filter, if present uses {@literal magnolia.resources.filesystem.observation.excludedDirectories property}
108      * for defining excluded <strong>root directories</strong>. Otherwise falls back to {@link #EXCLUDED_DIRECTORIES default} list of excluded directories.
109      */
110     protected ExclusionsFilter buildExclusionsFilter() {
111         List<String> configuredExcludedDirectories = Lists.newArrayList();
112         if (StringUtils.isNotBlank(excludedDirectories)) {
113             for (String directory : StringUtils.split(excludedDirectories, ",")) {
114                 configuredExcludedDirectories.add(directory.trim());
115             }
116         } else {
117             configuredExcludedDirectories = EXCLUDED_DIRECTORIES;
118         }
119         return new ExclusionsFilter(rootPath, configuredExcludedDirectories, Collections.<String>emptyList(), Collections.<String>emptyList());
120     }
121 
122     @Override
123     public FileSystemResource getRoot() {
124         return newResource(rootPath);
125     }
126 
127     @Override
128     public void watchForChanges(final ResourceVisitor visitor) {
129         log.info("Setting up {} to watch on {}", directoryWatcherService, rootPath);
130         try {
131             // TODO passing exclusionsFilter to both register() and the FileWatcherCallback seems redundant
132             directoryWatcherService.register(rootPath, exclusionsFilter, new FileWatcherCallback(this, exclusionsFilter, new VisitorFunction(visitor)));
133         } catch (IOException e) {
134             throw new RuntimeException(e); // TODO
135         }
136     }
137 
138     @Override
139     public FileSystemResource getByPath(final String path) {
140         final Path filePath = getPath(path);
141         if (!existsAndAllowed(filePath)) {
142             throw new ResourceNotFoundException(this, path);
143         }
144         return newResource(filePath);
145     }
146 
147     @Override
148     public boolean hasPath(final String path) {
149         final Path filePath = getPath(path);
150         return existsAndAllowed(filePath);
151     }
152 
153     @Override
154     protected boolean isFile(FileSystemResource resource) {
155         return Files.isRegularFile(resource.getRealPath());
156     }
157 
158     @Override
159     protected boolean isDirectory(FileSystemResource resource) {
160         return Files.isDirectory(resource.getRealPath());
161     }
162 
163     @Override
164     protected String getPath(FileSystemResource resource) {
165         return getStringPathForFilePath(resource.getRealPath());
166     }
167 
168     private String getStringPathForFilePath(Path resource) {
169         final Path path = rootPath.relativize(resource);
170         return "/" + FilenameUtils.normalize(path.toString(), true);
171     }
172 
173     @Override
174     protected String getName(FileSystemResource resource) {
175         return resource.getRealPath().getFileName().toString();
176     }
177 
178     @Override
179     protected long getLastModified(FileSystemResource resource) {
180         try {
181             final FileTime lastModifiedTime = Files.getLastModifiedTime(resource.getRealPath(), LINK_OPTIONS);
182             return lastModifiedTime.toMillis();
183         } catch (IOException e) {
184             throw new RuntimeException("Last modified time could not be retrieved for path " + resource + " : " + e, e);
185         }
186     }
187 
188     @Override
189     protected List<FileSystemResource> listChildren(FileSystemResource resource) {
190         if (resource.isFile()) {
191             throw new IllegalStateException(resource.getPath() + " is not a directory.");
192         }
193 
194         Path parent = resource.getRealPath();
195 
196         try (DirectoryStream<Path> ds = Files.newDirectoryStream(parent, exclusionsFilter)) {
197             return Lists.newArrayList(Iterables.transform(ds, transformFunction));
198         } catch (IOException e) {
199             throw new RuntimeException(e); // TODO
200         }
201     }
202 
203     @Override
204     protected FileSystemResource getParent(FileSystemResource resource) {
205         if (resource == null) {
206             throw new IllegalStateException("The provided path is null.");
207         }
208         if (!resource.getRealPath().startsWith(rootPath)) {
209             throw new IllegalStateException("The provided path '" + resource + "' is not a descendant of this origin's root path.");
210         }
211         if (resource.getRealPath().equals(rootPath)) {
212             return null;
213         }
214         return newResource(resource.getRealPath().getParent());
215     }
216 
217     @Override
218     protected InputStream doOpenStream(FileSystemResource resource) throws IOException {
219         // TODO Why do we do rootPath.resolve here (and only here ?)
220         final Path path = rootPath.resolve(resource.getRealPath());
221         return Files.newInputStream(path);
222     }
223 
224     @Override
225     protected Charset getCharsetFor(FileSystemResource resource) {
226         return Charset.forName("UTF-8"); // TODO ?
227     }
228 
229     protected Path validateRootPath(String path) {
230         Path rootPath = Paths.get(path);
231         if (!Files.exists(rootPath) && Files.exists(rootPath.getParent()) && Files.isDirectory(rootPath.getParent())) {
232             try {
233                 Files.createDirectory(rootPath);
234             } catch (IOException e) {
235                 throw new IllegalStateException("Tried to create " + rootPath + " and failed: " + ExceptionUtil.exceptionToWords(e), e);
236             }
237             log.info("Created {}", rootPath);
238         }
239 
240         if (Files.exists(rootPath) && !Files.isDirectory(rootPath)) {
241             throw new IllegalStateException("Trying to load resources from " + rootPath + ", which is not a directory.");
242         }
243 
244         if (!Files.exists(rootPath)) {
245             log.warn("Trying to load resources from {}, but this directory does not exist", rootPath);
246         }
247         return rootPath;
248     }
249 
250     protected Path getPath(String path) {
251         return Paths.get(rootPath.toString(), path);
252     }
253 
254     protected boolean existsAndAllowed(Path filePath) {
255         return Files.exists(filePath) && exclusionsFilter.apply(filePath);
256     }
257 
258     /**
259      * Create a {@link info.magnolia.resourceloader.Resource} out of a relative path.
260      *
261      * @param path expected to be a relative path under the rootPath
262      */
263     protected FileSystemResource newResource(Path path) {
264         // Validate on construction that Path instance is relative
265         final Path normPath;
266         if (!path.isAbsolute()) {
267             normPath = rootPath.resolve(path).normalize();
268         } else {
269             normPath = path;
270         }
271 
272         if (!normPath.startsWith(rootPath)) {
273             throw new IllegalStateException(path + " is not within the bounds of the " + rootPath + " root.");
274         }
275         final boolean isDirectory = Files.isDirectory(normPath); // TODO these pbly throw is not-exists
276         final boolean isFile = Files.isRegularFile(normPath);
277         Preconditions.checkArgument((isDirectory || isFile) && (isDirectory != isFile), "%s is neither or both a directory and/or a file.", normPath);
278         return new FileSystemResource(this, normPath);
279     }
280 
281 }