1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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
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];
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
108
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
132 directoryWatcherService.register(rootPath, exclusionsFilter, new FileWatcherCallback(this, exclusionsFilter, new VisitorFunction(visitor)));
133 } catch (IOException e) {
134 throw new RuntimeException(e);
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);
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
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");
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
260
261
262
263 protected FileSystemResource newResource(Path path) {
264
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);
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 }