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