1 /*
2 * This file is part of ClassGraph.
3 *
4 * Author: Luke Hutchison (luke.hutch@gmail.com)
5 *
6 * Hosted at: https://github.com/classgraph/classgraph
7 *
8 * --
9 *
10 * The MIT License (MIT)
11 *
12 * Copyright (c) 2019 Luke Hutchison
13 *
14 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
15 * documentation files (the "Software"), to deal in the Software without restriction, including without
16 * limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
17 * the Software, and to permit persons to whom the Software is furnished to do so, subject to the following
18 * conditions:
19 *
20 * The above copyright notice and this permission notice shall be included in all copies or substantial
21 * portions of the Software.
22 *
23 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
24 * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
25 * EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
26 * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
27 * OR OTHER DEALINGS IN THE SOFTWARE.
28 */
29 package io.github.classgraph;
30
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.net.URI;
34 import java.net.URL;
35 import java.nio.ByteBuffer;
36 import java.util.AbstractMap.SimpleEntry;
37 import java.util.ArrayList;
38 import java.util.Collection;
39 import java.util.Comparator;
40 import java.util.HashMap;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.Map.Entry;
44
45 import nonapi.io.github.classgraph.utils.CollectionUtils;
46
47 /** An AutoCloseable list of AutoCloseable {@link Resource} objects. */
48 public class ResourceList extends PotentiallyUnmodifiableList<Resource> implements AutoCloseable {
49 /** serialVersionUID. */
50 static final long serialVersionUID = 1L;
51
52 /** An unmodifiable empty {@link ResourceList}. */
53 static final ResourceList EMPTY_LIST = new ResourceList();
54 static {
55 EMPTY_LIST.makeUnmodifiable();
56 }
57
58 /**
59 * Return an unmodifiable empty {@link ResourceList}.
60 *
61 * @return the unmodifiable empty {@link ResourceList}.
62 */
63 public static ResourceList emptyList() {
64 return EMPTY_LIST;
65 }
66
67 /**
68 * Create a new modifiable empty list of {@link Resource} objects.
69 */
70 public ResourceList() {
71 super();
72 }
73
74 /**
75 * Create a new modifiable empty list of {@link Resource} objects, given a size hint.
76 *
77 * @param sizeHint
78 * the size hint
79 */
80 public ResourceList(final int sizeHint) {
81 super(sizeHint);
82 }
83
84 /**
85 * Create a new modifiable empty {@link ResourceList}, given an initial collection of {@link Resource} objects.
86 *
87 * @param resourceCollection
88 * the collection of {@link Resource} objects.
89 */
90 public ResourceList(final Collection<Resource> resourceCollection) {
91 super(resourceCollection);
92 }
93
94 /**
95 * Returns a list of all resources with the requested path. (There may be more than one resource with a given
96 * path, from different classpath elements or modules, so this returns a {@link ResourceList} rather than a
97 * single {@link Resource}.)
98 *
99 * @param resourcePath
100 * The path of a resource
101 * @return A {@link ResourceList} of {@link Resource} objects in this list that have the given path (there may
102 * be more than one resource with a given path, from different classpath elements or modules, so this
103 * returns a {@link ResourceList} rather than a single {@link Resource}.) Returns the empty list if no
104 * resource with is found with a matching path.
105 */
106 public ResourceList get(final String resourcePath) {
107 boolean hasResourceWithPath = false;
108 for (final Resource res : this) {
109 if (res.getPath().equals(resourcePath)) {
110 hasResourceWithPath = true;
111 break;
112 }
113 }
114 if (!hasResourceWithPath) {
115 return EMPTY_LIST;
116 } else {
117 final ResourceList matchingResources = new ResourceList(2);
118 for (final Resource res : this) {
119 if (res.getPath().equals(resourcePath)) {
120 matchingResources.add(res);
121 }
122 }
123 return matchingResources;
124 }
125 }
126
127 // -------------------------------------------------------------------------------------------------------------
128
129 /**
130 * Get the paths of all resources in this list relative to the package root.
131 *
132 * @return The paths of all resources in this list relative to the package root, by calling
133 * {@link Resource#getPath()} for each item in the list.
134 */
135 public List<String> getPaths() {
136 final List<String> resourcePaths = new ArrayList<>(this.size());
137 for (final Resource resource : this) {
138 resourcePaths.add(resource.getPath());
139 }
140 return resourcePaths;
141 }
142
143 /**
144 * Get the paths of all resources in this list relative to the root of the classpath element.
145 *
146 * @return The paths of all resources in this list relative to the root of the classpath element, by calling
147 * {@link Resource#getPathRelativeToClasspathElement()} for each item in the list.
148 */
149 public List<String> getPathsRelativeToClasspathElement() {
150 final List<String> resourcePaths = new ArrayList<>(this.size());
151 for (final Resource resource : this) {
152 resourcePaths.add(resource.getPath());
153 }
154 return resourcePaths;
155 }
156
157 /**
158 * Get the URLs of all resources in this list, by calling {@link Resource#getURL()} for each item in the list.
159 * Note that any resource with a {@code jrt:} URI (e.g. a system resource, or a resource from a jlink'd image)
160 * will cause {@link IllegalArgumentException} to be thrown, since {@link URL} does not support this scheme, so
161 * {@link #getURIs()} is strongly preferred over {@link #getURLs()}.
162 *
163 * @return The URLs of all resources in this list.
164 */
165 public List<URL> getURLs() {
166 final List<URL> resourceURLs = new ArrayList<>(this.size());
167 for (final Resource resource : this) {
168 resourceURLs.add(resource.getURL());
169 }
170 return resourceURLs;
171 }
172
173 /**
174 * Get the URIs of all resources in this list, by calling {@link Resource#getURI()} for each item in the list.
175 *
176 * @return The URIs of all resources in this list.
177 */
178 public List<URI> getURIs() {
179 final List<URI> resourceURLs = new ArrayList<>(this.size());
180 for (final Resource resource : this) {
181 resourceURLs.add(resource.getURI());
182 }
183 return resourceURLs;
184 }
185
186 // -------------------------------------------------------------------------------------------------------------
187
188 /** Returns true if a Resource has a path ending in ".class". */
189 private static final ResourceFilter CLASSFILE_FILTER = new ResourceFilter() {
190 @Override
191 public boolean accept(final Resource resource) {
192 final String path = resource.getPath();
193 if (!path.endsWith(".class") || path.length() < 7) {
194 return false;
195 }
196 // Check filename is not simply ".class"
197 final char c = path.charAt(path.length() - 7);
198 return c != '/' && c != '.';
199 }
200 };
201
202 /**
203 * Return a new {@link ResourceList} consisting of only the resources with the filename extension ".class".
204 *
205 * @return A new {@link ResourceList} consisting of only the resources with the filename extension ".class".
206 */
207 public ResourceList classFilesOnly() {
208 return filter(CLASSFILE_FILTER);
209 }
210
211 /**
212 * Return a new {@link ResourceList} consisting of non-classfile resources only.
213 *
214 * @return A new {@link ResourceList} consisting of only the resources that do not have the filename extension
215 * ".class".
216 */
217 public ResourceList nonClassFilesOnly() {
218 return filter(new ResourceFilter() {
219 @Override
220 public boolean accept(final Resource resource) {
221 return !CLASSFILE_FILTER.accept(resource);
222 }
223 });
224 }
225
226 // -------------------------------------------------------------------------------------------------------------
227
228 /**
229 * Return this {@link ResourceList} as a map from resource path (obtained from {@link Resource#getPath()}) to a
230 * {@link ResourceList} of {@link Resource} objects that have that path.
231 *
232 * @return This {@link ResourceList} as a map from resource path (obtained from {@link Resource#getPath()}) to a
233 * {@link ResourceList} of {@link Resource} objects that have that path.
234 */
235 public Map<String, ResourceList> asMap() {
236 final Map<String, ResourceList> pathToResourceList = new HashMap<>();
237 for (final Resource resource : this) {
238 final String path = resource.getPath();
239 ResourceList resourceList = pathToResourceList.get(path);
240 if (resourceList == null) {
241 resourceList = new ResourceList(1);
242 pathToResourceList.put(path, resourceList);
243 }
244 resourceList.add(resource);
245 }
246 return pathToResourceList;
247 }
248
249 /**
250 * Find duplicate resource paths within this {@link ResourceList}.
251 *
252 * @return A {@link List} of {@link Entry} objects for all resources in the classpath and/or module path that
253 * have a non-unique path (i.e. where there are at least two resources with the same path). The key of
254 * each returned {@link Entry} is the path (obtained from {@link Resource#getPath()}), and the value is
255 * a {@link ResourceList} of at least two unique {@link Resource} objects that have that path.
256 */
257 public List<Entry<String, ResourceList>> findDuplicatePaths() {
258 final List<Entry<String, ResourceList>> duplicatePaths = new ArrayList<>();
259 for (final Entry<String, ResourceList> pathAndResourceList : asMap().entrySet()) {
260 // Find ResourceLists with two or more entries
261 if (pathAndResourceList.getValue().size() > 1) {
262 duplicatePaths.add(new SimpleEntry<>(pathAndResourceList.getKey(), pathAndResourceList.getValue()));
263 }
264 }
265 CollectionUtils.sortIfNotEmpty(duplicatePaths, new Comparator<Entry<String, ResourceList>>() {
266 @Override
267 public int compare(final Entry<String, ResourceList> o1, final Entry<String, ResourceList> o2) {
268 // Sort in lexicographic order of path
269 return o1.getKey().compareTo(o2.getKey());
270 }
271 });
272 return duplicatePaths;
273 }
274
275 // -------------------------------------------------------------------------------------------------------------
276
277 /**
278 * Filter a {@link ResourceList} using a predicate mapping a {@link Resource} object to a boolean, producing
279 * another {@link ResourceList} for all items in the list for which the predicate is true.
280 */
281 @FunctionalInterface
282 public interface ResourceFilter {
283 /**
284 * Whether or not to allow a {@link Resource} list item through the filter.
285 *
286 * @param resource
287 * The {@link Resource} item to filter.
288 * @return Whether or not to allow the item through the filter. If true, the item is copied to the output
289 * list; if false, it is excluded.
290 */
291 boolean accept(Resource resource);
292 }
293
294 /**
295 * Find the subset of the {@link Resource} objects in this list for which the given filter predicate is true.
296 *
297 * @param filter
298 * The {@link ResourceFilter} to apply.
299 * @return The subset of the {@link Resource} objects in this list for which the given filter predicate is true.
300 */
301 public ResourceList filter(final ResourceFilter filter) {
302 final ResourceList resourcesFiltered = new ResourceList();
303 for (final Resource resource : this) {
304 if (filter.accept(resource)) {
305 resourcesFiltered.add(resource);
306 }
307 }
308 return resourcesFiltered;
309 }
310
311 // -------------------------------------------------------------------------------------------------------------
312
313 /** A {@link FunctionalInterface} for consuming the contents of a {@link Resource} as a byte array. */
314 @FunctionalInterface
315 public interface ByteArrayConsumer {
316 /**
317 * Consume the complete content of a {@link Resource} as a byte array.
318 *
319 * @param resource
320 * The {@link Resource} used to load the byte array.
321 * @param byteArray
322 * The complete content of the resource.
323 */
324 void accept(final Resource resource, final byte[] byteArray);
325 }
326
327 /**
328 * Fetch the content of each {@link Resource} in this {@link ResourceList} as a byte array, pass the byte array
329 * to the given {@link ByteArrayConsumer}, then close the underlying InputStream or release the underlying
330 * ByteBuffer by calling {@link Resource#close()}.
331 *
332 * @param byteArrayConsumer
333 * The {@link ByteArrayConsumer}.
334 * @param ignoreIOExceptions
335 * if true, any {@link IOException} thrown while trying to load any of the resources will be silently
336 * ignored.
337 * @throws IllegalArgumentException
338 * if ignoreExceptions is false, and an {@link IOException} is thrown while trying to load any of
339 * the resources.
340 */
341 public void forEachByteArray(final ByteArrayConsumer byteArrayConsumer, final boolean ignoreIOExceptions) {
342 for (final Resource resource : this) {
343 try {
344 final byte[] resourceContent = resource.load();
345 byteArrayConsumer.accept(resource, resourceContent);
346 } catch (final IOException e) {
347 if (!ignoreIOExceptions) {
348 throw new IllegalArgumentException("Could not load resource " + resource, e);
349 }
350 } finally {
351 resource.close();
352 }
353 }
354 }
355
356 /**
357 * Fetch the content of each {@link Resource} in this {@link ResourceList} as a byte array, pass the byte array
358 * to the given {@link ByteArrayConsumer}, then close the underlying InputStream or release the underlying
359 * ByteBuffer by calling {@link Resource#close()}.
360 *
361 * @param byteArrayConsumer
362 * The {@link ByteArrayConsumer}.
363 * @throws IllegalArgumentException
364 * if trying to load any of the resources results in an {@link IOException} being thrown.
365 */
366 public void forEachByteArray(final ByteArrayConsumer byteArrayConsumer) {
367 forEachByteArray(byteArrayConsumer, /* ignoreIOExceptions = */ false);
368 }
369
370 // -------------------------------------------------------------------------------------------------------------
371
372 /** A {@link FunctionalInterface} for consuming the contents of a {@link Resource} as an {@link InputStream}. */
373 @FunctionalInterface
374 public interface InputStreamConsumer {
375 /**
376 * Consume a {@link Resource} as an {@link InputStream}.
377 *
378 * @param resource
379 * The {@link Resource} used to open the {@link InputStream}.
380 * @param inputStream
381 * The {@link InputStream} opened on the resource.
382 */
383 void accept(final Resource resource, final InputStream inputStream);
384 }
385
386 /**
387 * Fetch an {@link InputStream} for each {@link Resource} in this {@link ResourceList}, pass the
388 * {@link InputStream} to the given {@link InputStreamConsumer}, then close the {@link InputStream} after the
389 * {@link InputStreamConsumer} returns, by calling {@link Resource#close()}.
390 *
391 * @param inputStreamConsumer
392 * The {@link InputStreamConsumer}.
393 * @param ignoreIOExceptions
394 * if true, any {@link IOException} thrown while trying to load any of the resources will be silently
395 * ignored.
396 * @throws IllegalArgumentException
397 * if ignoreExceptions is false, and an {@link IOException} is thrown while trying to open any of
398 * the resources.
399 */
400 public void forEachInputStream(final InputStreamConsumer inputStreamConsumer,
401 final boolean ignoreIOExceptions) {
402 for (final Resource resource : this) {
403 try {
404 inputStreamConsumer.accept(resource, resource.open());
405 } catch (final IOException e) {
406 if (!ignoreIOExceptions) {
407 throw new IllegalArgumentException("Could not load resource " + resource, e);
408 }
409 } finally {
410 resource.close();
411 }
412 }
413 }
414
415 /**
416 * Fetch an {@link InputStream} for each {@link Resource} in this {@link ResourceList}, pass the
417 * {@link InputStream} to the given {@link InputStreamConsumer}, then close the {@link InputStream} after the
418 * {@link InputStreamConsumer} returns, by calling {@link Resource#close()}.
419 *
420 * @param inputStreamConsumer
421 * The {@link InputStreamConsumer}.
422 * @throws IllegalArgumentException
423 * if trying to open any of the resources results in an {@link IOException} being thrown.
424 */
425 public void forEachInputStream(final InputStreamConsumer inputStreamConsumer) {
426 forEachInputStream(inputStreamConsumer, /* ignoreIOExceptions = */ false);
427 }
428
429 // -------------------------------------------------------------------------------------------------------------
430
431 /** A {@link FunctionalInterface} for consuming the contents of a {@link Resource} as a {@link ByteBuffer}. */
432 @FunctionalInterface
433 public interface ByteBufferConsumer {
434 /**
435 * Consume a {@link Resource} as a {@link ByteBuffer}.
436 *
437 * @param resource
438 * The {@link Resource} whose content is reflected in the {@link ByteBuffer}.
439 * @param byteBuffer
440 * The {@link ByteBuffer} mapped to the resource.
441 */
442 void accept(final Resource resource, final ByteBuffer byteBuffer);
443 }
444
445 /**
446 * Read each {@link Resource} in this {@link ResourceList} as a {@link ByteBuffer}, pass the {@link ByteBuffer}
447 * to the given {@link InputStreamConsumer}, then release the {@link ByteBuffer} after the
448 * {@link ByteBufferConsumer} returns, by calling {@link Resource#close()}.
449 *
450 * @param byteBufferConsumer
451 * The {@link ByteBufferConsumer}.
452 * @param ignoreIOExceptions
453 * if true, any {@link IOException} thrown while trying to load any of the resources will be silently
454 * ignored.
455 * @throws IllegalArgumentException
456 * if ignoreExceptions is false, and an {@link IOException} is thrown while trying to load any of
457 * the resources.
458 */
459 public void forEachByteBuffer(final ByteBufferConsumer byteBufferConsumer, final boolean ignoreIOExceptions) {
460 for (final Resource resource : this) {
461 try {
462 final ByteBuffer byteBuffer = resource.read();
463 byteBufferConsumer.accept(resource, byteBuffer);
464 } catch (final IOException e) {
465 if (!ignoreIOExceptions) {
466 throw new IllegalArgumentException("Could not load resource " + resource, e);
467 }
468 } finally {
469 resource.close();
470 }
471 }
472 }
473
474 /**
475 * Read each {@link Resource} in this {@link ResourceList} as a {@link ByteBuffer}, pass the {@link ByteBuffer}
476 * to the given {@link InputStreamConsumer}, then release the {@link ByteBuffer} after the
477 * {@link ByteBufferConsumer} returns, by calling {@link Resource#close()}.
478 *
479 * @param byteBufferConsumer
480 * The {@link ByteBufferConsumer}.
481 * @throws IllegalArgumentException
482 * if trying to load any of the resources results in an {@link IOException} being thrown.
483 */
484 public void forEachByteBuffer(final ByteBufferConsumer byteBufferConsumer) {
485 forEachByteBuffer(byteBufferConsumer, /* ignoreIOExceptions = */ false);
486 }
487
488 // -------------------------------------------------------------------------------------------------------------
489
490 /** Close all the {@link Resource} objects in this {@link ResourceList}. */
491 @Override
492 public void close() {
493 for (final Resource resource : this) {
494 resource.close();
495 }
496 }
497 }
498