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