1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.logging.log4j.core.config.plugins;
18
19 import java.io.File;
20 import java.io.FileInputStream;
21 import java.io.FileNotFoundException;
22 import java.io.IOException;
23 import java.io.UnsupportedEncodingException;
24 import java.lang.annotation.Annotation;
25 import java.net.URI;
26 import java.net.URL;
27 import java.net.URLDecoder;
28 import java.util.Arrays;
29 import java.util.Collection;
30 import java.util.Enumeration;
31 import java.util.HashSet;
32 import java.util.List;
33 import java.util.Set;
34 import java.util.jar.JarEntry;
35 import java.util.jar.JarInputStream;
36
37 import org.apache.logging.log4j.Logger;
38 import org.apache.logging.log4j.core.helpers.Charsets;
39 import org.apache.logging.log4j.core.helpers.Loader;
40 import org.apache.logging.log4j.status.StatusLogger;
41 import org.osgi.framework.FrameworkUtil;
42 import org.osgi.framework.wiring.BundleWiring;
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77 public class ResolverUtil {
78
79 private static final Logger LOGGER = StatusLogger.getLogger();
80
81 private static final String VFSZIP = "vfszip";
82
83 private static final String BUNDLE_RESOURCE = "bundleresource";
84
85
86 private final Set<Class<?>> classMatches = new HashSet<Class<?>>();
87
88
89 private final Set<URI> resourceMatches = new HashSet<URI>();
90
91
92
93
94
95 private ClassLoader classloader;
96
97
98
99
100
101
102
103 public Set<Class<?>> getClasses() {
104 return classMatches;
105 }
106
107
108
109
110
111 public Set<URI> getResources() {
112 return resourceMatches;
113 }
114
115
116
117
118
119
120
121
122 public ClassLoader getClassLoader() {
123 return classloader != null ? classloader : (classloader = Loader.getClassLoader(ResolverUtil.class, null));
124 }
125
126
127
128
129
130
131
132 public void setClassLoader(final ClassLoader classloader) { this.classloader = classloader; }
133
134
135
136
137
138
139
140
141
142
143 public void findImplementations(final Class<?> parent, final String... packageNames) {
144 if (packageNames == null) {
145 return;
146 }
147
148 final Test test = new IsA(parent);
149 for (final String pkg : packageNames) {
150 findInPackage(test, pkg);
151 }
152 }
153
154
155
156
157
158
159
160
161 public void findSuffix(final String suffix, final String... packageNames) {
162 if (packageNames == null) {
163 return;
164 }
165
166 final Test test = new NameEndsWith(suffix);
167 for (final String pkg : packageNames) {
168 findInPackage(test, pkg);
169 }
170 }
171
172
173
174
175
176
177
178
179 public void findAnnotated(final Class<? extends Annotation> annotation, final String... packageNames) {
180 if (packageNames == null) {
181 return;
182 }
183
184 final Test test = new AnnotatedWith(annotation);
185 for (final String pkg : packageNames) {
186 findInPackage(test, pkg);
187 }
188 }
189
190 public void findNamedResource(final String name, final String... pathNames) {
191 if (pathNames == null) {
192 return;
193 }
194
195 final Test test = new NameIs(name);
196 for (final String pkg : pathNames) {
197 findInPackage(test, pkg);
198 }
199 }
200
201
202
203
204
205
206
207
208 public void find(final Test test, final String... packageNames) {
209 if (packageNames == null) {
210 return;
211 }
212
213 for (final String pkg : packageNames) {
214 findInPackage(test, pkg);
215 }
216 }
217
218
219
220
221
222
223
224
225
226
227
228 public void findInPackage(final Test test, String packageName) {
229 packageName = packageName.replace('.', '/');
230 final ClassLoader loader = getClassLoader();
231 Enumeration<URL> urls;
232
233 try {
234 urls = loader.getResources(packageName);
235 } catch (final IOException ioe) {
236 LOGGER.warn("Could not read package: " + packageName, ioe);
237 return;
238 }
239
240 while (urls.hasMoreElements()) {
241 try {
242 final URL url = urls.nextElement();
243 String urlPath = extractPath(url);
244
245 LOGGER.info("Scanning for classes in [" + urlPath + "] matching criteria: " + test);
246
247 if (VFSZIP.equals(url.getProtocol())) {
248 final String path = urlPath.substring(0, urlPath.length() - packageName.length() - 2);
249 final URL newURL = new URL(url.getProtocol(), url.getHost(), path);
250 @SuppressWarnings("resource")
251 final JarInputStream stream = new JarInputStream(newURL.openStream());
252 try {
253 loadImplementationsInJar(test, packageName, path, stream);
254 } finally {
255 close(stream, newURL);
256 }
257 } else if (BUNDLE_RESOURCE.equals(url.getProtocol())) {
258 loadImplementationsInBundle(test, packageName);
259 } else {
260 final File file = new File(urlPath);
261 if (file.isDirectory()) {
262 loadImplementationsInDirectory(test, packageName, file);
263 } else {
264 loadImplementationsInJar(test, packageName, file);
265 }
266 }
267 } catch (final IOException ioe) {
268 LOGGER.warn("could not read entries", ioe);
269 }
270 }
271 }
272
273 String extractPath(final URL url) throws UnsupportedEncodingException {
274 String urlPath = url.getPath();
275
276
277
278 if (urlPath.startsWith("jar:")) {
279 urlPath = urlPath.substring(4);
280 }
281
282 if (urlPath.startsWith("file:")) {
283 urlPath = urlPath.substring(5);
284 }
285
286 if (urlPath.indexOf('!') > 0) {
287 urlPath = urlPath.substring(0, urlPath.indexOf('!'));
288 }
289
290
291
292 final String protocol = url.getProtocol();
293 final List<String> neverDecode = Arrays.asList(VFSZIP, BUNDLE_RESOURCE);
294 if (neverDecode.contains(protocol)) {
295 return urlPath;
296 }
297 if (new File(urlPath).exists()) {
298
299 return urlPath;
300 }
301 urlPath = URLDecoder.decode(urlPath, Charsets.UTF_8.name());
302 return urlPath;
303 }
304
305 private void loadImplementationsInBundle(final Test test, final String packageName) {
306
307 final BundleWiring wiring = (BundleWiring) FrameworkUtil.getBundle(
308 ResolverUtil.class).adapt(BundleWiring.class);
309 final Collection<String> list = wiring.listResources(packageName, "*.class",
310 BundleWiring.LISTRESOURCES_RECURSE);
311 for (final String name : list) {
312 addIfMatching(test, name);
313 }
314 }
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329 private void loadImplementationsInDirectory(final Test test, final String parent, final File location) {
330 final File[] files = location.listFiles();
331 if (files == null) {
332 return;
333 }
334
335 StringBuilder builder;
336 for (final File file : files) {
337 builder = new StringBuilder();
338 builder.append(parent).append("/").append(file.getName());
339 final String packageOrClass = parent == null ? file.getName() : builder.toString();
340
341 if (file.isDirectory()) {
342 loadImplementationsInDirectory(test, packageOrClass, file);
343 } else if (isTestApplicable(test, file.getName())) {
344 addIfMatching(test, packageOrClass);
345 }
346 }
347 }
348
349 private boolean isTestApplicable(final Test test, final String path) {
350 return test.doesMatchResource() || path.endsWith(".class") && test.doesMatchClass();
351 }
352
353
354
355
356
357
358
359
360
361
362 private void loadImplementationsInJar(final Test test, final String parent, final File jarFile) {
363 @SuppressWarnings("resource")
364 JarInputStream jarStream = null;
365 try {
366 jarStream = new JarInputStream(new FileInputStream(jarFile));
367 loadImplementationsInJar(test, parent, jarFile.getPath(), jarStream);
368 } catch (final FileNotFoundException ex) {
369 LOGGER.error("Could not search jar file '" + jarFile + "' for classes matching criteria: " + test
370 + " file not found");
371 } catch (final IOException ioe) {
372 LOGGER.error("Could not search jar file '" + jarFile + "' for classes matching criteria: " + test
373 + " due to an IOException", ioe);
374 } finally {
375 close(jarStream, jarFile);
376 }
377 }
378
379
380
381
382
383 private void close(JarInputStream jarStream, final Object source) {
384 if (jarStream != null) {
385 try {
386 jarStream.close();
387 } catch (IOException e) {
388 LOGGER.error("Error closing JAR file stream for {}", source, e);
389 }
390 }
391 }
392
393
394
395
396
397
398
399
400
401
402 private void loadImplementationsInJar(final Test test, final String parent, final String path,
403 final JarInputStream stream) {
404
405 try {
406 JarEntry entry;
407
408 while ((entry = stream.getNextJarEntry()) != null) {
409 final String name = entry.getName();
410 if (!entry.isDirectory() && name.startsWith(parent) && isTestApplicable(test, name)) {
411 addIfMatching(test, name);
412 }
413 }
414 } catch (final IOException ioe) {
415 LOGGER.error("Could not search jar file '" + path + "' for classes matching criteria: " +
416 test + " due to an IOException", ioe);
417 }
418 }
419
420
421
422
423
424
425
426
427 protected void addIfMatching(final Test test, final String fqn) {
428 try {
429 final ClassLoader loader = getClassLoader();
430 if (test.doesMatchClass()) {
431 final String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
432 if (LOGGER.isDebugEnabled()) {
433 LOGGER.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
434 }
435
436 final Class<?> type = loader.loadClass(externalName);
437 if (test.matches(type)) {
438 classMatches.add(type);
439 }
440 }
441 if (test.doesMatchResource()) {
442 URL url = loader.getResource(fqn);
443 if (url == null) {
444 url = loader.getResource(fqn.substring(1));
445 }
446 if (url != null && test.matches(url.toURI())) {
447 resourceMatches.add(url.toURI());
448 }
449 }
450 } catch (final Throwable t) {
451 LOGGER.warn("Could not examine class '" + fqn + "' due to a " +
452 t.getClass().getName() + " with message: " + t.getMessage());
453 }
454 }
455
456
457
458
459
460 public interface Test {
461
462
463
464
465
466
467 boolean matches(Class<?> type);
468
469
470
471
472
473
474 boolean matches(URI resource);
475
476 boolean doesMatchClass();
477
478 boolean doesMatchResource();
479 }
480
481
482
483
484 public abstract static class ClassTest implements Test {
485 @Override
486 public boolean matches(final URI resource) {
487 throw new UnsupportedOperationException();
488 }
489
490 @Override
491 public boolean doesMatchClass() {
492 return true;
493 }
494
495 @Override
496 public boolean doesMatchResource() {
497 return false;
498 }
499 }
500
501
502
503
504 public abstract static class ResourceTest implements Test {
505 @Override
506 public boolean matches(final Class<?> cls) {
507 throw new UnsupportedOperationException();
508 }
509
510 @Override
511 public boolean doesMatchClass() {
512 return false;
513 }
514
515 @Override
516 public boolean doesMatchResource() {
517 return true;
518 }
519 }
520
521
522
523
524
525 public static class IsA extends ClassTest {
526 private final Class<?> parent;
527
528
529
530
531
532 public IsA(final Class<?> parentType) { this.parent = parentType; }
533
534
535
536
537
538
539 @Override
540 public boolean matches(final Class<?> type) {
541 return type != null && parent.isAssignableFrom(type);
542 }
543
544 @Override
545 public String toString() {
546 return "is assignable to " + parent.getSimpleName();
547 }
548 }
549
550
551
552
553 public static class NameEndsWith extends ClassTest {
554 private final String suffix;
555
556
557
558
559
560 public NameEndsWith(final String suffix) { this.suffix = suffix; }
561
562
563
564
565
566
567 @Override
568 public boolean matches(final Class<?> type) {
569 return type != null && type.getName().endsWith(suffix);
570 }
571
572 @Override
573 public String toString() {
574 return "ends with the suffix " + suffix;
575 }
576 }
577
578
579
580
581
582 public static class AnnotatedWith extends ClassTest {
583 private final Class<? extends Annotation> annotation;
584
585
586
587
588
589 public AnnotatedWith(final Class<? extends Annotation> annotation) {
590 this.annotation = annotation;
591 }
592
593
594
595
596
597
598 @Override
599 public boolean matches(final Class<?> type) {
600 return type != null && type.isAnnotationPresent(annotation);
601 }
602
603 @Override
604 public String toString() {
605 return "annotated with @" + annotation.getSimpleName();
606 }
607 }
608
609
610
611
612 public static class NameIs extends ResourceTest {
613 private final String name;
614
615 public NameIs(final String name) { this.name = "/" + name; }
616
617 @Override
618 public boolean matches(final URI resource) {
619 return resource.getPath().endsWith(name);
620 }
621
622 @Override public String toString() {
623 return "named " + name;
624 }
625 }
626 }