View Javadoc
1   /**
2    * This file Copyright (c) 2015-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 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  
42  import java.io.IOException;
43  import java.io.InputStream;
44  import java.nio.charset.Charset;
45  import java.nio.charset.StandardCharsets;
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  
81      private final ExclusionsFilter exclusionsFilter;
82      private final Function<Path, FileSystemResource> transformFunction;
83      private final String excludedDirectories;
84      private final DirectoryWatcherService directoryWatcherService;
85  
86      FileSystemResourceOrigin(@Provided MagnoliaConfigurationProperties mcp, @Provided DirectoryWatcherService directoryWatcherService, String name) {
87          super(name);
88          this.directoryWatcherService = directoryWatcherService;
89  
90          String resourcesDir = mcp.getProperty(RESOURCES_DIR_PROPERTY);
91          if (resourcesDir == null) {
92              log.warn("Could not find magnolia.resources.dir property, please update your magnolia.properties. Falling back to magnolia.home.");
93              resourcesDir = mcp.getProperty("magnolia.home");
94          }
95          this.excludedDirectories = mcp.getProperty("magnolia.resources.filesystem.observation.excludedDirectories");
96          this.rootPath = validateRootPath(resourcesDir);
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 
128     @Override
129     public FileSystemResource getByPath(final String path) {
130         final Path filePath = getPath(path);
131         if (!existsAndAllowed(filePath)) {
132             throw new ResourceNotFoundException(this, path);
133         }
134         return newResource(filePath);
135     }
136 
137     @Override
138     public boolean hasPath(final String path) {
139         final Path filePath = getPath(path);
140         return existsAndAllowed(filePath);
141     }
142 
143     @Override
144     protected void initializeResourceChangeMonitoring() {
145         try {
146             /**
147              * {@code exclusionsFilter} is passed both into {@link DirectoryWatcherService#register(Path, com.google.common.base.Predicate, info.magnolia.dirwatch.WatcherCallback)} and {@link FileWatcherCallback#FileWatcherCallback(FileSystemResourceOrigin, Predicate)}
148              * so that the latter would also be able to apply the filter in case it generates auxiliary events.
149              */
150             directoryWatcherService.register(rootPath, exclusionsFilter, new FileWatcherCallback(this, exclusionsFilter));
151         } catch (IOException e) {
152             log.error("Failed to initialise file system observation: {}! No file system changes are going to be communicated!", e.getMessage(), e);
153         }
154     }
155 
156     @Override
157     protected boolean isFile(FileSystemResource resource) {
158         return Files.isRegularFile(resource.getRealPath());
159     }
160 
161     @Override
162     protected boolean isDirectory(FileSystemResource resource) {
163         return Files.isDirectory(resource.getRealPath());
164     }
165 
166     @Override
167     protected String getPath(FileSystemResource resource) {
168         return parseResourcePath(resource.getRealPath());
169     }
170 
171     /**
172      * Relativize a real path against this {@link FileSystemResourceOrigin origin's} root path and normalize it.
173      *
174      * @param realPath path in file system
175      * @return corresponding string path to the resource in this origin
176      */
177     public String parseResourcePath(Path realPath) {
178         final Path path = rootPath.relativize(realPath);
179         return "/" + FilenameUtils.normalize(path.toString(), true);
180     }
181 
182     @Override
183     protected String getName(FileSystemResource resource) {
184         return resource.getRealPath().getFileName().toString();
185     }
186 
187     @Override
188     protected long getLastModified(FileSystemResource resource) {
189         try {
190             final FileTime lastModifiedTime = Files.getLastModifiedTime(resource.getRealPath(), LINK_OPTIONS);
191             return lastModifiedTime.toMillis();
192         } catch (IOException e) {
193             throw new RuntimeException("Last modified time could not be retrieved for path " + resource + " : " + e, e);
194         }
195     }
196 
197     @Override
198     protected List<FileSystemResource> listChildren(FileSystemResource resource) {
199         if (resource.isFile()) {
200             throw new IllegalStateException(resource.getPath() + " is not a directory.");
201         }
202 
203         Path parent = resource.getRealPath();
204 
205         try (DirectoryStream<Path> ds = Files.newDirectoryStream(parent, exclusionsFilter)) {
206             return Lists.newArrayList(Iterables.transform(ds, transformFunction));
207         } catch (IOException e) {
208             log.error("Failed to list the child resources of a resource at [{}]: {}. Returning empty list...", resource, e.getMessage(), e);
209             return Collections.emptyList();
210         }
211     }
212 
213     @Override
214     protected FileSystemResource getParent(FileSystemResource resource) {
215         if (resource == null) {
216             throw new IllegalStateException("The provided path is null.");
217         }
218         if (!resource.getRealPath().startsWith(rootPath)) {
219             throw new IllegalStateException("The provided path '" + resource + "' is not a descendant of this origin's root path.");
220         }
221         if (resource.getRealPath().equals(rootPath)) {
222             return null;
223         }
224         return newResource(resource.getRealPath().getParent());
225     }
226 
227     @Override
228     protected InputStream doOpenStream(FileSystemResource resource) throws IOException {
229         return Files.newInputStream(resource.getRealPath());
230     }
231 
232     @Override
233     protected Charset getCharsetFor(FileSystemResource resource) {
234         return StandardCharsets.UTF_8; // TODO ?
235     }
236 
237     protected Path validateRootPath(String path) {
238         Path rootPath = Paths.get(path);
239         if (!Files.exists(rootPath) && Files.exists(rootPath.getParent()) && Files.isDirectory(rootPath.getParent())) {
240             try {
241                 Files.createDirectory(rootPath);
242             } catch (IOException e) {
243                 throw new IllegalStateException("Tried to create " + rootPath + " and failed: " + ExceptionUtil.exceptionToWords(e), e);
244             }
245             log.info("Created {}", rootPath);
246         }
247 
248         if (Files.exists(rootPath) && !Files.isDirectory(rootPath)) {
249             throw new IllegalStateException("Trying to load resources from " + rootPath + ", which is not a directory.");
250         }
251 
252         if (!Files.exists(rootPath)) {
253             log.warn("Trying to load resources from {}, but this directory does not exist", rootPath);
254         }
255         return rootPath;
256     }
257 
258     protected Path getPath(String path) {
259         return Paths.get(rootPath.toString(), path);
260     }
261 
262     protected boolean existsAndAllowed(Path filePath) {
263         return Files.exists(filePath) && exclusionsFilter.apply(filePath);
264     }
265 
266     /**
267      * Create a {@link info.magnolia.resourceloader.Resource} out of a relative path.
268      *
269      * @param path expected to be a relative path under the rootPath
270      */
271     protected FileSystemResource newResource(Path path) {
272         // Validate on construction that Path instance is relative
273         final Path normPath;
274         if (!path.isAbsolute()) {
275             normPath = rootPath.resolve(path).normalize();
276         } else {
277             normPath = path;
278         }
279 
280         if (!normPath.startsWith(rootPath)) {
281             throw new IllegalStateException(path + " is not within the bounds of the " + rootPath + " root.");
282         }
283         final boolean isDirectory = Files.isDirectory(normPath); // TODO these pbly throw is not-exists
284         final boolean isFile = Files.isRegularFile(normPath);
285         Preconditions.checkArgument((isDirectory || isFile) && (isDirectory != isFile), "%s is neither or both a directory and/or a file.", normPath);
286         return new FileSystemResource(this, normPath);
287     }
288 }