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.resourceloader.AbstractResourceOrigin;
39 import info.magnolia.resourceloader.ResourceOriginFactory;
40 import info.magnolia.resourceloader.ResourceVisitor;
41
42 import java.io.IOException;
43 import java.io.InputStream;
44 import java.net.URL;
45 import java.nio.charset.Charset;
46 import java.util.Collection;
47 import java.util.LinkedHashMap;
48 import java.util.List;
49 import java.util.Map;
50 import java.util.Set;
51
52 import org.apache.commons.lang3.StringUtils;
53 import org.reflections.Reflections;
54 import org.reflections.scanners.ResourcesScanner;
55 import org.reflections.util.ClasspathHelper;
56 import org.reflections.util.ConfigurationBuilder;
57 import org.reflections.util.FilterBuilder;
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.Joiner;
64 import com.google.common.base.Predicate;
65 import com.google.common.base.Predicates;
66 import com.google.common.collect.Collections2;
67
68
69
70
71
72
73
74
75
76
77
78
79
80 @AutoFactory(implementing = ResourceOriginFactory.class)
81 public class ClasspathResourceOrigin extends AbstractResourceOrigin<ClasspathResource> {
82
83 private static final String CONFIG_FILES_PATTERN = ".*\\.yaml$";
84
85 private static final Logger log = LoggerFactory.getLogger(ClasspathResourceOrigin.class);
86
87 private final ClasspathScannerService classpathScannerService;
88
89 private Map<String, ClasspathResource> resources;
90
91 ClasspathResourceOrigin(@Provided ClasspathScannerService classpathScannerService, String name) {
92 super(name);
93 this.classpathScannerService = classpathScannerService;
94 collectResources();
95 }
96
97 @Override
98 public ClasspathResource getRoot() {
99 return getByPath("/");
100 }
101
102 @Override
103 public void watchForChanges(ResourceVisitor visitor) {
104 if (!classpathScannerService.isEnabled()) {
105 return;
106 }
107
108 String observedResourcesPattern = observedResourcesPattern();
109 log.info("Setting up {} to watch for resource changes with pattern {}.", classpathScannerService, observedResourcesPattern);
110
111
112
113
114
115 final FilterBuilder resourcesFilter = new FilterBuilder();
116
117 resourcesFilter.include(observedResourcesPattern);
118 applyResourceExclusions(resourcesFilter);
119
120 final Predicate<? super URL> urlsFilter = urlsFilter();
121
122 classpathScannerService.watch(resourcesFilter, urlsFilter, new VisitorFunction(this, visitor));
123 }
124
125 protected String observedResourcesPattern() {
126 return CONFIG_FILES_PATTERN;
127 }
128
129 @Override
130 public ClasspathResource getByPath(String path) {
131
132 final ClasspathResource resource = resources.get(path);
133 if (resource == null) {
134 throw new ResourceNotFoundException(this, path);
135 }
136 return resource;
137 }
138
139 @Override
140 public boolean hasPath(String path) {
141
142 return resources.containsKey(path);
143 }
144
145 @Override
146 protected boolean isFile(ClasspathResource resource) {
147 throw shouldNotBeCalled();
148 }
149
150 @Override
151 protected boolean isDirectory(ClasspathResource resource) {
152 throw shouldNotBeCalled();
153 }
154
155 @Override
156 protected String getPath(ClasspathResource resource) {
157 return resource.getRealPath();
158 }
159
160 @Override
161 protected String getName(ClasspathResource resource) {
162 return StringUtils.substringAfterLast(resource.getRealPath(), "/");
163 }
164
165 @Override
166 protected long getLastModified(ClasspathResource resource) {
167 try {
168 final URL resourceUrl = getUrl(resource);
169 return resourceUrl.openConnection().getLastModified();
170 } catch (IOException e) {
171 throw new RuntimeException("Last modified time could not be retrieved for path " + resource + " : " + e, e);
172 }
173 }
174
175 @Override
176 protected List<ClasspathResource> listChildren(ClasspathResource resource) {
177 throw shouldNotBeCalled();
178 }
179
180 @Override
181 protected ClasspathResource getParent(ClasspathResource resource) {
182 throw shouldNotBeCalled();
183 }
184
185 @Override
186 protected InputStream doOpenStream(ClasspathResource resource) throws IOException {
187 final URL url = getUrl(resource);
188
189 return url.openStream();
190 }
191
192 @Override
193 protected Charset getCharsetFor(ClasspathResource resource) {
194 return Charset.forName("UTF-8");
195 }
196
197 protected URL getUrl(ClasspathResource resource) {
198 final URL url = ClasspathResourcesUtil.getResource(resource.getRealPath());
199 if (url == null) {
200 throw new IllegalStateException("Can't open stream for " + resource);
201 }
202 return url;
203 }
204
205 protected void collectResources() {
206
207 final long start = System.currentTimeMillis();
208 final Predicate<String> resourcesFilter = resourcesFilter();
209 final Collection<URL> classpathUrls = classpathUrls();
210 final Reflections reflections = new Reflections(new ConfigurationBuilder()
211 .setScanners(new ResourcesScanner())
212 .setUrls(classpathUrls)
213 .filterInputsBy(resourcesFilter)
214 );
215
216 final Set<String> resources = reflections.getResources(Predicates.<String>alwaysTrue());
217 long scanDone = System.currentTimeMillis();
218 log.debug("Took {}ms to find {} resources", scanDone - start, resources.size());
219
220
221 this.resources = new LinkedHashMap<>();
222 for (String resource : resources) {
223 createResourcesFor(resource);
224 }
225 log.debug("Took {}ms to build virtual directory structure", System.currentTimeMillis() - scanDone);
226 }
227
228 protected Collection<URL> classpathUrls() {
229 final Collection<URL> allURLs = ClasspathHelper.forClassLoader(ClasspathHelper.contextClassLoader());
230 Predicate<? super URL> excludedExtensions = urlsFilter();
231 return Collections2.filter(allURLs, excludedExtensions);
232 }
233
234 protected Predicate<? super URL> urlsFilter() {
235 final String excludedUrlExtensionsPattern = extensionsPattern(excludedUrlExtensions());
236 return URLPredicate.excludedExtensions(excludedUrlExtensionsPattern);
237 }
238
239 protected Predicate<String> resourcesFilter() {
240 final FilterBuilder filterBuilder = new FilterBuilder();
241 return applyResourceExclusions(filterBuilder);
242 }
243
244
245
246
247 private FilterBuilder applyResourceExclusions(FilterBuilder filterBuilder) {
248
249 filterBuilder.exclude("mgnl-.*");
250
251 filterBuilder.exclude(extensionsPattern(excludedResourcesExtensions()));
252
253 final String[] excludedPackages = excludedPackages();
254 for (String excludedPackage : excludedPackages) {
255 filterBuilder.excludePackage(excludedPackage);
256 }
257 return filterBuilder;
258 }
259
260 protected String extensionsPattern(String[] extensions) {
261 return ".*\\.(" + Joiner.on('|').join(extensions) + ")$";
262 }
263
264 protected String[] excludedUrlExtensions() {
265 return new String[] {
266
267 "dylib", "dll", "jnilib",
268
269 "pom"
270 };
271 }
272
273 protected String[] excludedResourcesExtensions() {
274 return new String[] {
275 "package\\.html", "java", "jar",
276
277 "dylib", "dll", "jnilib"
278 };
279 }
280
281 protected String[] excludedPackages() {
282 return new String[] {
283 "META-INF",
284 "com.oracle.java",
285 "com.oracle.tools",
286 "com.sun",
287 "sun",
288 "oracle",
289 "java",
290 "javax",
291 "jdk",
292 "org.apache",
293 "lombok",
294 "VAADIN",
295 "gwt-unitCache"
296 };
297 }
298
299 protected ClasspathResource createResourcesFor(final String resourcePath) {
300 final String validatedPath = validatePath(resourcePath);
301 final ClasspathResource newResource = newClasspathResource(validatedPath, true);
302
303
304 ClasspathResource resource = newResource;
305 String parentPath = getParentPath(resource.getPath());
306 while (parentPath != null) {
307 ClasspathResource parent;
308 if (!resources.containsKey(parentPath)) {
309
310 parent = newClasspathResource(parentPath, false);
311 resource.setParent(parent);
312 } else {
313
314 parent = resources.get(parentPath);
315 resource.setParent(parent);
316 break;
317 }
318 parentPath = getParentPath(parentPath);
319 resource = parent;
320 }
321 return newResource;
322 }
323
324 protected ClasspathResource newClasspathResource(String path, boolean isFile) {
325 final ClasspathResource resource = new ClasspathResource(this, path, isFile);
326 resources.put(resource.getPath(), resource);
327 return resource;
328 }
329
330
331
332
333 protected String validatePath(String resource) {
334 if (resource.startsWith("/")) {
335 throw new IllegalStateException("Resources are not expected to start with / when scanning the classpath; got " + resource);
336 }
337 return "/" + resource;
338 }
339
340 private String getParentPath(String resourcePath) {
341 if ("/".equals(resourcePath)) {
342
343 return null;
344 }
345 String parentPath = StringUtils.substringBeforeLast(resourcePath, "/");
346 if (parentPath.isEmpty()) {
347 parentPath = "/";
348 }
349 return parentPath;
350 }
351
352 private IllegalStateException shouldNotBeCalled() {
353 return new IllegalStateException("This method should not be called, it is implemented directly by ClasspathResource");
354 }
355
356 }