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
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
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
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
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
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
148
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
173
174
175
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;
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
268
269
270
271 protected FileSystemResource newResource(Path path) {
272
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);
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 }