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.classpath;
35
36 import info.magnolia.classpathwatch.ClasspathScannerService;
37 import info.magnolia.cms.util.ClasspathResourcesUtil;
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.File;
44 import java.io.IOException;
45 import java.io.InputStream;
46 import java.net.JarURLConnection;
47 import java.net.URL;
48 import java.net.URLClassLoader;
49 import java.net.URLConnection;
50 import java.nio.charset.Charset;
51 import java.util.Collection;
52 import java.util.LinkedHashMap;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Set;
56
57 import javax.servlet.ServletContext;
58
59 import org.apache.commons.lang3.StringUtils;
60 import org.reflections.Reflections;
61 import org.reflections.scanners.ResourcesScanner;
62 import org.reflections.util.ClasspathHelper;
63 import org.reflections.util.ConfigurationBuilder;
64 import org.reflections.util.FilterBuilder;
65 import org.slf4j.Logger;
66 import org.slf4j.LoggerFactory;
67
68 import com.google.auto.factory.AutoFactory;
69 import com.google.auto.factory.Provided;
70 import com.google.common.base.Joiner;
71 import com.google.common.base.Predicate;
72 import com.google.common.base.Predicates;
73 import com.google.common.collect.Collections2;
74 import com.google.common.collect.FluentIterable;
75 import com.google.common.collect.Sets;
76 import com.google.common.io.Resources;
77
78
79
80
81
82
83
84
85
86
87
88
89
90 @AutoFactory(implementing = ResourceOriginFactory.class)
91 public class ClasspathResourceOrigin extends AbstractResourceOrigin<ClasspathResource> {
92
93 private static final Logger log = LoggerFactory.getLogger(ClasspathResourceOrigin.class);
94 private static final String OBSERVED_FILES_PATTERN = ".*\\.(ftl|yaml)$";
95
96 private final ClasspathScannerService classpathScannerService;
97 private final ServletContext servletContext;
98 private final MagnoliaConfigurationProperties magnoliaProperties;
99
100 private Map<String, ClasspathResource> resourceCache;
101
102 ClasspathResourceOrigin(@Provided ClasspathScannerService classpathScannerService, String name, @Provided ServletContext ctx, @Provided MagnoliaConfigurationProperties magnoliaProperties) {
103 super(name);
104 this.classpathScannerService = classpathScannerService;
105 this.servletContext = ctx;
106 this.magnoliaProperties = magnoliaProperties;
107 collectResources();
108 }
109
110 @Override
111 public ClasspathResource getRoot() {
112 return getByPath("/");
113 }
114
115 @Override
116 public void watchForChanges(ResourceVisitor visitor) {
117 if (!classpathScannerService.isEnabled()) {
118 return;
119 }
120
121 String observedResourcesPattern = observedResourcesPattern();
122 log.info("Setting up {} to watch for resource changes with pattern {}.", classpathScannerService, observedResourcesPattern);
123
124
125
126
127
128 final FilterBuilder resourcesFilter = new FilterBuilder();
129
130 resourcesFilter.include(observedResourcesPattern);
131 applyResourceExclusions(resourcesFilter);
132
133 final Predicate<? super URL> urlsFilter = urlsFilter();
134
135 classpathScannerService.watch(resourcesFilter, urlsFilter, new VisitorFunction(this, visitor));
136 }
137
138 protected String observedResourcesPattern() {
139 String pattern = magnoliaProperties.getProperty("magnolia.resources.classpath.observation.pattern");
140 if (StringUtils.isNotBlank(pattern)) {
141 return pattern;
142 } else {
143 log.info("Classpath observation pattern is not being set explicitly, current observation is made for only ftl + yaml files");
144 return OBSERVED_FILES_PATTERN;
145 }
146 }
147
148 @Override
149 public ClasspathResource getByPath(String path) {
150 String decoratedPath = decoratePath(path);
151 String validatedPath = validatePath(decoratedPath);
152 if (resourceCache.containsKey(validatedPath)) {
153 return resourceCache.get(validatedPath);
154 }
155
156
157 ClasspathResource resource = getResourceFor(validatedPath);
158 if (resource == null) {
159 throw new ResourceNotFoundException(this, validatedPath);
160 }
161
162 resourceCache.put(validatedPath, resource);
163 return resource;
164 }
165
166 ClasspathResource getResourceFor(String path) {
167 String strippedPath = stripLeadingSlash(path);
168 if (!hasPath(path) || !resourcesFilter().apply(strippedPath)) {
169 return null;
170 }
171
172 boolean isFile = isResourceFile(strippedPath);
173 return newClasspathResource(path, isFile);
174 }
175
176 @Override
177 public boolean hasPath(String path) {
178 final String decoratedPath = decoratePath(path);
179 return resourceCache.containsKey(validatePath(decoratedPath)) || getResourceUrl(stripLeadingSlash(decoratedPath)) != null;
180 }
181
182 private URL getResourceUrl(String path) {
183 URLConnection connection = null;
184 try {
185 URL resource = Resources.getResource(path);
186
187 connection = resource.openConnection();
188 connection.getInputStream();
189 return resource;
190 } catch (IllegalArgumentException | IOException e) {
191 return null;
192 } finally {
193 if (connection != null) {
194 tryToCloseConnection(connection);
195 }
196 }
197 }
198
199 private void tryToCloseConnection(URLConnection connection) {
200 if (connection instanceof JarURLConnection) {
201 JarURLConnection jar = (JarURLConnection) connection;
202 try {
203 if (jar.getUseCaches()) {
204 jar.getJarFile().close();
205 }
206 } catch (IOException ignored) {
207
208
209 }
210 } else {
211 try {
212 InputStream inputStream = connection.getInputStream();
213 if (inputStream != null) {
214 inputStream.close();
215 }
216 } catch (IOException e) {
217 log.debug("Stream could not be closed", e);
218 }
219 }
220 }
221
222
223
224
225
226
227 boolean isResourceFile(String path) {
228 String strippedPath = stripLastSlash(path);
229
230 URL filePath = Resources.getResource(path);
231 File file = new File(filePath.getFile());
232 if (file.exists()) {
233 return file.isFile();
234 }
235
236 try {
237 Resources.getResource(strippedPath + "/");
238 return false;
239 } catch (IllegalArgumentException e) {
240 return true;
241 }
242 }
243
244
245
246
247 private String stripLastSlash(String path) {
248 String strippedPath;
249 if (path.endsWith("/")) {
250 strippedPath = StringUtils.chop(path);
251 } else {
252 strippedPath = path;
253 }
254 return strippedPath;
255 }
256
257 @Override
258 protected boolean isFile(ClasspathResource resource) {
259 throw shouldNotBeCalled();
260 }
261
262 @Override
263 protected boolean isDirectory(ClasspathResource resource) {
264 throw shouldNotBeCalled();
265 }
266
267 @Override
268 protected String getPath(ClasspathResource resource) {
269 return resource.getRealPath();
270 }
271
272 @Override
273 protected String getName(ClasspathResource resource) {
274 return StringUtils.substringAfterLast(resource.getRealPath(), "/");
275 }
276
277 @Override
278 protected long getLastModified(ClasspathResource resource) {
279 try {
280 final URL resourceUrl = getUrl(resource);
281 return resourceUrl.openConnection().getLastModified();
282 } catch (IOException e) {
283 throw new RuntimeException("Last modified time could not be retrieved for path " + resource + " : " + e, e);
284 }
285 }
286
287
288
289
290
291
292
293
294
295
296
297
298 @Override
299 protected List<ClasspathResource> listChildren(ClasspathResource resource) {
300 if (resource != null && resource.isFile()) {
301 throw new IllegalArgumentException(resource.getRealPath() + " is not a directory.");
302 }
303
304 Set<ClasspathResource> children = Sets.newHashSet();
305 ChildFinder childFinder = new ChildFinder(resource.getRealPath());
306
307 for (String childPath : childFinder.getDirectChildren()) {
308 URL resourceUrl = getResourceUrl(stripLeadingSlash(childPath));
309 if (resourceUrl == null) {
310 resourceCache.remove(childPath);
311 } else {
312 children.add(resourceCache.get(validatePath(childPath)));
313 }
314 }
315
316 for (String path : childFinder.getDirectories()) {
317 children.add(newClasspathResource(path, false));
318 }
319
320 return FluentIterable.from(children).toList();
321 }
322
323 protected ClasspathResource updateResourceFor(String resourcePath) {
324 URL resource = getResourceUrl(resourcePath);
325 String validatedPath = validatePath(resourcePath);
326 if (resource != null) {
327
328 return newClasspathResource(validatedPath, true);
329 } else {
330 removeFromCache(validatedPath);
331 return new ClasspathResource(this, validatedPath, true);
332 }
333 }
334
335 private void removeFromCache(String resourcePath) {
336 resourceCache.remove(validatePath(resourcePath));
337 }
338
339 @Override
340 protected ClasspathResource getParent(ClasspathResource resource) {
341 if ("/".equals(resource.getRealPath())) {
342 return null;
343 }
344
345 String parentPath = StringUtils.substringBeforeLast(resource.getRealPath(), "/");
346 if (StringUtils.isNotBlank(parentPath)) {
347 return getByPath(parentPath);
348 } else {
349 return getRoot();
350 }
351 }
352
353 @Override
354 protected InputStream doOpenStream(ClasspathResource resource) throws IOException {
355 final URL url = getUrl(resource);
356
357 return url.openStream();
358 }
359
360 @Override
361 protected Charset getCharsetFor(ClasspathResource resource) {
362 return Charset.forName("UTF-8");
363 }
364
365 protected URL getUrl(ClasspathResource resource) {
366 final URL url = ClasspathResourcesUtil.getResource(resource.getRealPath());
367 if (url == null) {
368 throw new IllegalStateException("Can't open stream for " + resource);
369 }
370 return url;
371 }
372
373 protected void collectResources() {
374
375 final long start = System.currentTimeMillis();
376 final Predicate<String> resourcesFilter = resourcesFilter();
377 final Collection<URL> classpathUrls = classpathUrls();
378 final Reflections reflections = new Reflections(new ConfigurationBuilder()
379 .setScanners(new ResourcesScanner())
380 .setUrls(classpathUrls)
381 .filterInputsBy(resourcesFilter)
382 );
383
384 final Set<String> resources = reflections.getResources(Predicates.<String>alwaysTrue());
385 long scanDone = System.currentTimeMillis();
386 log.debug("Took {}ms to find {} resources", scanDone - start, resources.size());
387
388
389 this.resourceCache = new LinkedHashMap<>();
390 for (String resource : resources) {
391 newClasspathResource(resource, true);
392 }
393 log.debug("Took {}ms to build virtual directory structure", System.currentTimeMillis() - scanDone);
394 }
395
396 protected Collection<URL> classpathUrls() {
397 final ClassLoader contextClassLoader = ClasspathHelper.contextClassLoader();
398 final Collection<URL> allURLs;
399 if (contextClassLoader instanceof URLClassLoader) {
400 allURLs = ClasspathHelper.forClassLoader(contextClassLoader);
401 } else {
402
403
404
405 allURLs = ClasspathHelper.forWebInfLib(servletContext);
406 }
407 Predicate<? super URL> excludedExtensions = urlsFilter();
408 return Collections2.filter(allURLs, excludedExtensions);
409 }
410
411 protected Predicate<? super URL> urlsFilter() {
412 final String excludedUrlExtensionsPattern = extensionsPattern(excludedUrlExtensions());
413 return URLPredicate.excludedExtensions(excludedUrlExtensionsPattern);
414 }
415
416 protected Predicate<String> resourcesFilter() {
417 final FilterBuilder filterBuilder = new FilterBuilder();
418 return applyResourceExclusions(filterBuilder);
419 }
420
421
422
423
424 private FilterBuilder applyResourceExclusions(FilterBuilder filterBuilder) {
425
426 filterBuilder.exclude("mgnl-.*");
427
428 filterBuilder.exclude(extensionsPattern(excludedResourcesExtensions()));
429
430 final String[] excludedPackages = excludedPackages();
431 for (String excludedPackage : excludedPackages) {
432 filterBuilder.excludePackage(excludedPackage);
433 }
434 return filterBuilder;
435 }
436
437 protected String extensionsPattern(String[] extensions) {
438 return ".*\\.(" + Joiner.on('|').join(extensions) + ")$";
439 }
440
441 protected String[] excludedUrlExtensions() {
442 return new String[]{
443
444 "dylib", "dll", "jnilib",
445
446 "pom"
447 };
448 }
449
450 protected String[] excludedResourcesExtensions() {
451 return new String[]{
452 "package\\.html", "java", "jar",
453
454 "dylib", "dll", "jnilib"
455 };
456 }
457
458 protected String[] excludedPackages() {
459 return new String[]{
460 "META-INF",
461 "com.oracle.java",
462 "com.oracle.tools",
463 "com.sun",
464 "sun",
465 "oracle",
466 "java",
467 "javax",
468 "jdk",
469 "org.apache",
470 "lombok",
471 "VAADIN",
472 "gwt-unitCache"
473 };
474 }
475
476
477
478
479
480
481 @Deprecated
482 protected ClasspathResource createResourcesFor(final String resourcePath) {
483 return newClasspathResource(resourcePath, true);
484 }
485
486 protected ClasspathResource newClasspathResource(String path, boolean isFile) {
487 final String validatedPath = validatePath(path);
488 final ClasspathResource resource = new ClasspathResource(this, validatedPath, isFile);
489 resourceCache.put(validatedPath, resource);
490 return resource;
491 }
492
493
494
495
496
497
498
499 protected String validatePath(String resource) {
500 if (resource.startsWith("/")) {
501 return resource;
502 } else {
503 return "/" + resource;
504 }
505 }
506
507 private class ChildFinder {
508
509 private final String parentPath;
510 private final Set<String> directories = Sets.newHashSet();
511 private final Set<String> directChildren = Sets.newHashSet();
512
513 private ChildFinder(String initialParentPath) {
514 if (StringUtils.isNotEmpty(initialParentPath)) {
515 this.parentPath = initialParentPath.endsWith("/") ? initialParentPath : initialParentPath + "/";
516 } else {
517 this.parentPath = initialParentPath;
518 }
519
520 doIterate();
521 }
522
523 private void doIterate() {
524 for (String resourcePath : resourceCache.keySet()) {
525 if (isDirectChild(resourcePath)) {
526 directChildren.add(decoratePath(resourcePath));
527 }
528 }
529 }
530
531 private boolean isDirectChild(String path) {
532 String relativePath = path;
533 if (!parentPath.isEmpty()) {
534 if (!relativePath.startsWith(parentPath)) {
535 return false;
536 }
537
538 relativePath = StringUtils.substringAfter(path, parentPath);
539 }
540
541 if (relativePath.contains("/")) {
542 directories.add(decoratePath(parentPath + StringUtils.substringBefore(relativePath, "/")));
543 return false;
544 }
545
546 return !path.equals(parentPath);
547 }
548
549 Set<String> getDirectories() {
550 return directories;
551 }
552
553 Set<String> getDirectChildren() {
554 return directChildren;
555 }
556 }
557
558
559
560
561
562
563
564
565 String decoratePath(String resourcePath) {
566 return resourcePath;
567 }
568
569 private IllegalStateException shouldNotBeCalled() {
570 return new IllegalStateException("This method should not be called, it is implemented directly by ClasspathResource");
571 }
572
573
574
575
576
577
578
579 private String stripLeadingSlash(String path) {
580 if (path.startsWith("/")) {
581 return path.substring(1);
582 }
583 return path;
584 }
585 }