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