-
Notifications
You must be signed in to change notification settings - Fork 53
Expand file tree
/
Copy pathFileUtils.java
More file actions
798 lines (738 loc) · 25.8 KB
/
FileUtils.java
File metadata and controls
798 lines (738 loc) · 25.8 KB
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
35
36
37
38
39
40
41
42
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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
/*
* #%L
* SciJava Common shared library for SciJava software.
* %%
* Copyright (C) 2009 - 2026 SciJava developers.
* %%
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
* #L%
*/
// File path shortening code adapted from:
// from: https://www.rgagnon.com/javadetails/java-0661.html
package org.scijava.util;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.scijava.Context;
/**
* Useful methods for working with file paths.
*
* @author Johannes Schindelin
* @author Curtis Rueden
* @author Grant Harris
*/
public final class FileUtils {
public static final int DEFAULT_SHORTENER_THRESHOLD = 4;
public static final String SHORTENER_BACKSLASH_REGEX = "\\\\";
public static final String SHORTENER_SLASH_REGEX = "/";
public static final String SHORTENER_BACKSLASH = "\\";
public static final String SHORTENER_SLASH = "/";
public static final String SHORTENER_ELLIPSE = "...";
/** A regular expression to match filenames containing version information. */
private static final Pattern VERSION_PATTERN = buildVersionPattern();
private FileUtils() {
// prevent instantiation of utility class
}
/**
* Gets the absolute path to the given file, with the directory separator
* standardized to forward slash, like most platforms use.
*
* @param file The file whose path will be obtained and standardized.
* @return The file's standardized absolute path.
*/
public static String getPath(final File file) {
final String path = file.getAbsolutePath();
final String slash = System.getProperty("file.separator");
return getPath(path, slash);
}
/**
* Gets a standardized path based on the given one, with the directory
* separator standardized from the specific separator to forward slash, like
* most platforms use.
*
* @param path The path to standardize.
* @param separator The directory separator to be standardized.
* @return The standardized path.
*/
public static String getPath(final String path, final String separator) {
// NB: Standardize directory separator (i.e., avoid Windows nonsense!).
return path.replaceAll(Pattern.quote(separator), "/");
}
/**
* Extracts the file extension from a file.
*
* @param file the file object
* @return the file extension (excluding the dot), or the empty string when
* the file name does not contain dots
*/
public static String getExtension(final File file) {
final String name = file.getName();
final int dot = name.lastIndexOf('.');
if (dot < 0) return "";
return name.substring(dot + 1);
}
/**
* Extracts the file extension from a file path.
*
* @param path the path to the file (relative or absolute)
* @return the file extension (excluding the dot), or the empty string when
* the file name does not contain dots
*/
public static String getExtension(final String path) {
return getExtension(new File(path));
}
/** Gets the {@link Date} of the file's last modification. */
public static Date getModifiedTime(final File file) {
final long modifiedTime = file.lastModified();
final Calendar c = Calendar.getInstance();
c.setTimeInMillis(modifiedTime);
return c.getTime();
}
/**
* Reads the contents of the given file into a new byte array.
*
* @see DigestUtils#string(byte[]) To convert a byte array to a string.
* @throws IOException If the file cannot be read.
*/
public static byte[] readFile(final File file) throws IOException {
final long length = file.length();
if (length > Integer.MAX_VALUE) {
throw new IllegalArgumentException("File too large");
}
final byte[] bytes = new byte[(int) length];
try (final DataInputStream dis = new DataInputStream(new FileInputStream(
file)))
{
dis.readFully(bytes);
}
return bytes;
}
/**
* Writes the given byte array to the specified file.
*
* @see DigestUtils#bytes(String) To convert a string to a byte array.
* @throws IOException If the file cannot be written.
*/
public static void writeFile(final File file, final byte[] bytes)
throws IOException
{
try (final FileOutputStream out = new FileOutputStream(file)) {
out.write(bytes);
}
}
public static String stripFilenameVersion(final String filename) {
final Matcher matcher = VERSION_PATTERN.matcher(filename);
if (!matcher.matches()) return filename;
return matcher.group(1) + matcher.group(5);
}
/**
* Lists all versions of a given (possibly versioned) file name.
*
* @param directory the directory to scan
* @param filename the file name to use
* @return the list of matches
*/
public static File[] getAllVersions(final File directory,
final String filename)
{
final Matcher matcher = VERSION_PATTERN.matcher(filename);
if (!matcher.matches()) {
final File file = new File(directory, filename);
return file.exists() ? new File[] { file } : null;
}
final String baseName = matcher.group(1);
final String classifier = matcher.group(6);
return directory.listFiles(new FilenameFilter() {
@Override
public boolean accept(final File dir, final String name) {
if (!name.startsWith(baseName)) return false;
final Matcher matcher2 = VERSION_PATTERN.matcher(name);
return matcher2.matches() && baseName.equals(matcher2.group(1)) &&
equals(classifier, matcher2.group(6));
}
private boolean equals(final String a, final String b) {
if (a == null) {
return b == null;
}
return a.equals(b);
}
});
}
/**
* Converts the given {@link URL} to its corresponding {@link File}.
* <p>
* This method is similar to calling {@code new File(url.toURI())} except that
* it also handles "jar:file:" URLs, returning the path to the JAR file.
* </p>
*
* @param url The URL to convert.
* @return A file path suitable for use with e.g. {@link FileInputStream}
* @throws IllegalArgumentException if the URL does not correspond to a file.
*/
public static File urlToFile(final URL url) {
return url == null ? null : urlToFile(url.toString());
}
/**
* Converts the given URL string to its corresponding {@link File}.
*
* @param url The URL to convert.
* @return A file path suitable for use with e.g. {@link FileInputStream}
* @throws IllegalArgumentException if the URL does not correspond to a file.
*/
public static File urlToFile(final String url) {
String path = url;
if (path.startsWith("jar:")) {
// remove "jar:" prefix and "!/" suffix
final int index = path.indexOf("!/");
path = path.substring(4, index);
}
try {
if (PlatformUtils.isWindows() && path.matches("file:[A-Za-z]:.*")) {
path = "file:/" + path.substring(5);
}
return new File(new URL(path).toURI());
}
catch (final MalformedURLException e) {
// NB: URL is not completely well-formed.
}
catch (final URISyntaxException e) {
// NB: URL is not completely well-formed.
}
if (path.startsWith("file:")) {
// pass through the URL as-is, minus "file:" prefix
path = path.substring(5);
return new File(path);
}
throw new IllegalArgumentException("Invalid URL: " + url);
}
/**
* Shortens the path to a maximum of 4 path elements.
*
* @param path the path to the file (relative or absolute)
* @return shortened path
*/
public static String shortenPath(final String path) {
return shortenPath(path, DEFAULT_SHORTENER_THRESHOLD);
}
/**
* Shortens the path based on the given maximum number of path elements. E.g.,
* "C:/1/2/test.txt" returns "C:/1/.../test.txt" if threshold is 1.
*
* @param path the path to the file (relative or absolute)
* @param threshold the number of directories to keep unshortened
* @return shortened path
*/
public static String shortenPath(final String path, final int threshold) {
String regex = SHORTENER_BACKSLASH_REGEX;
String sep = SHORTENER_BACKSLASH;
if (path.indexOf("/") > 0) {
regex = SHORTENER_SLASH_REGEX;
sep = SHORTENER_SLASH;
}
String pathtemp[] = path.split(regex);
// remove empty elements
int elem = 0;
{
final String newtemp[] = new String[pathtemp.length];
int j = 0;
for (int i = 0; i < pathtemp.length; i++) {
if (!pathtemp[i].equals("")) {
newtemp[j++] = pathtemp[i];
elem++;
}
}
pathtemp = newtemp;
}
if (elem > threshold) {
final StringBuilder sb = new StringBuilder();
int index = 0;
// drive or protocol
final int pos2dots = path.indexOf(":");
if (pos2dots > 0) {
// case c:\ c:/ etc.
sb.append(path.substring(0, pos2dots + 2));
index++;
// case http:// ftp:// etc.
if (path.indexOf(":/") > 0 && pathtemp[0].length() > 2) {
sb.append(SHORTENER_SLASH);
}
}
else {
final boolean isUNC =
path.substring(0, 2).equals(SHORTENER_BACKSLASH_REGEX);
if (isUNC) {
sb.append(SHORTENER_BACKSLASH).append(SHORTENER_BACKSLASH);
}
}
for (; index <= threshold; index++) {
sb.append(pathtemp[index]).append(sep);
}
if (index == (elem - 1)) {
sb.append(pathtemp[elem - 1]);
}
else {
sb.append(SHORTENER_ELLIPSE).append(sep).append(pathtemp[elem - 1]);
}
return sb.toString();
}
return path;
}
/**
* Compacts a path into a given number of characters. The result is similar to
* the Win32 API PathCompactPathExA.
*
* @param path the path to the file (relative or absolute)
* @param limit the number of characters to which the path should be limited
* @return shortened path
*/
public static String limitPath(final String path, final int limit) {
if (path.length() <= limit) return path;
final char shortPathArray[] = new char[limit];
final char pathArray[] = path.toCharArray();
final char ellipseArray[] = SHORTENER_ELLIPSE.toCharArray();
final int pathindex = pathArray.length - 1;
final int shortpathindex = limit - 1;
// fill the array from the end
int i = 0;
for (; i < limit; i++) {
if (pathArray[pathindex - i] != '/' && pathArray[pathindex - i] != '\\') {
shortPathArray[shortpathindex - i] = pathArray[pathindex - i];
}
else {
break;
}
}
// check how much space is left
final int free = limit - i;
if (free < SHORTENER_ELLIPSE.length()) {
// fill the beginning with ellipse
for (int j = 0; j < ellipseArray.length; j++) {
shortPathArray[j] = ellipseArray[j];
}
}
else {
// fill the beginning with path and leave room for the ellipse
int j = 0;
for (; j + ellipseArray.length < free; j++) {
shortPathArray[j] = pathArray[j];
}
// ... add the ellipse
for (int k = 0; j + k < free; k++) {
shortPathArray[j + k] = ellipseArray[k];
}
}
return new String(shortPathArray);
}
/**
* Creates a temporary directory.
* <p>
* Since there is no atomic operation to do that, we create a temporary file,
* delete it and create a directory in its place. To avoid race conditions, we
* use the optimistic approach: if the directory cannot be created, we try to
* obtain a new temporary file rather than erroring out.
* </p>
* <p>
* It is the caller's responsibility to make sure that the directory is
* deleted; see {@link #deleteRecursively(File)}.
* </p>
*
* @param prefix The prefix string to be used in generating the file's name;
* see {@link File#createTempFile(String, String, File)}
* @return An abstract pathname denoting a newly-created empty directory
* @throws IOException
*/
public static File createTemporaryDirectory(final String prefix)
throws IOException
{
return createTemporaryDirectory(prefix, null, null);
}
/**
* Creates a temporary directory.
* <p>
* Since there is no atomic operation to do that, we create a temporary file,
* delete it and create a directory in its place. To avoid race conditions, we
* use the optimistic approach: if the directory cannot be created, we try to
* obtain a new temporary file rather than erroring out.
* </p>
* <p>
* It is the caller's responsibility to make sure that the directory is
* deleted; see {@link #deleteRecursively(File)}.
* </p>
*
* @param prefix The prefix string to be used in generating the file's name;
* see {@link File#createTempFile(String, String, File)}
* @param suffix The suffix string to be used in generating the file's name;
* see {@link File#createTempFile(String, String, File)}
* @return An abstract pathname denoting a newly-created empty directory
* @throws IOException
*/
public static File createTemporaryDirectory(final String prefix,
final String suffix) throws IOException
{
return createTemporaryDirectory(prefix, suffix, null);
}
/**
* Creates a temporary directory.
* <p>
* Since there is no atomic operation to do that, we create a temporary file,
* delete it and create a directory in its place. To avoid race conditions, we
* use the optimistic approach: if the directory cannot be created, we try to
* obtain a new temporary file rather than erroring out.
* </p>
* <p>
* It is the caller's responsibility to make sure that the directory is
* deleted; see {@link #deleteRecursively(File)}.
* </p>
*
* @param prefix The prefix string to be used in generating the file's name;
* see {@link File#createTempFile(String, String, File)}
* @param suffix The suffix string to be used in generating the file's name;
* see {@link File#createTempFile(String, String, File)}
* @param directory The directory in which the file is to be created, or null
* if the default temporary-file directory is to be used
* @return: An abstract pathname denoting a newly-created empty directory
* @throws IOException
*/
public static File createTemporaryDirectory(final String prefix,
final String suffix, final File directory) throws IOException
{
for (int counter = 0; counter < 10; counter++) {
final File file = File.createTempFile(prefix, suffix, directory);
if (!file.delete()) {
throw new IOException("Could not delete file " + file);
}
// in case of a race condition, just try again
if (file.mkdir()) return file;
}
throw new IOException(
"Could not create temporary directory (too many race conditions?)");
}
/**
* Deletes a directory recursively.
*
* @param directory The directory to delete.
* @return whether it succeeded (see also {@link File#delete()})
*/
public static boolean deleteRecursively(final File directory) {
if (directory == null) return true;
final File[] list = directory.listFiles();
if (list == null) return true;
for (final File file : list) {
if (file.isFile()) {
if (!file.delete()) return false;
}
else if (file.isDirectory()) {
if (!deleteRecursively(file)) return false;
}
}
return directory.delete();
}
/**
* Recursively lists the contents of the referenced directory. Directories are
* excluded from the result. Supported protocols include {@code file} and
* {@code jar}.
*
* @param directory The directory whose contents should be listed.
* @return A collection of {@link URL}s representing the directory's contents.
* @see #listContents(URL, boolean, boolean)
*/
public static Collection<URL> listContents(final URL directory) {
return listContents(directory, true, true);
}
/**
* Lists all contents of the referenced directory. Supported protocols include
* {@code file} and {@code jar}.
*
* @param directory The directory whose contents should be listed.
* @param recurse Whether to list contents recursively, as opposed to only the
* directory's direct contents.
* @param filesOnly Whether to exclude directories in the resulting collection
* of contents.
* @return A collection of {@link URL}s representing the directory's contents.
*/
public static Collection<URL> listContents(final URL directory,
final boolean recurse, final boolean filesOnly)
{
return appendContents(new ArrayList<URL>(), directory, recurse, filesOnly);
}
/**
* Recursively adds contents from the referenced directory to an existing
* collection. Directories are excluded from the result. Supported protocols
* include {@code file} and {@code jar}.
*
* @param result The collection to which contents should be added.
* @param directory The directory whose contents should be listed.
* @return A collection of {@link URL}s representing the directory's contents.
* @see #appendContents(Collection, URL, boolean, boolean)
*/
public static Collection<URL> appendContents(final Collection<URL> result,
final URL directory)
{
return appendContents(result, directory, true, true);
}
/**
* Add contents from the referenced directory to an existing collection.
* Supported protocols include {@code file} and {@code jar}.
*
* @param result The collection to which contents should be added.
* @param directory The directory whose contents should be listed.
* @param recurse Whether to append contents recursively, as opposed to only
* the directory's direct contents.
* @param filesOnly Whether to exclude directories in the resulting collection
* of contents.
* @return A collection of {@link URL}s representing the directory's contents.
*/
public static Collection<URL> appendContents(final Collection<URL> result,
final URL directory, final boolean recurse, final boolean filesOnly)
{
if (directory == null) return result; // nothing to append
final String protocol = directory.getProtocol();
if (protocol.equals("file")) {
final File dir = urlToFile(directory);
final File[] list = dir.listFiles();
if (list != null) {
for (final File file : list) {
try {
if (!filesOnly || file.isFile()) {
result.add(file.toURI().toURL());
}
if (recurse && file.isDirectory()) {
appendContents(result, file.toURI().toURL(), recurse, filesOnly);
}
}
catch (final MalformedURLException e) {
e.printStackTrace();
}
}
}
}
else if (protocol.equals("jar")) {
try {
final String url = directory.toString();
final int bang = url.indexOf("!/");
if (bang < 0) return result;
final String prefix = url.substring(bang + 2);
final String baseURL = url.substring(0, bang + 2);
final JarURLConnection connection =
(JarURLConnection) new URL(baseURL).openConnection();
try (final JarFile jar = connection.getJarFile()) {
for (final JarEntry entry : new IteratorPlus<>(jar.entries())) {
final String urlEncoded =
new URI(null, null, entry.getName(), null).toString();
if (urlEncoded.length() > prefix.length() && // omit directory itself
urlEncoded.startsWith(prefix))
{
if (filesOnly && urlEncoded.endsWith("/")) {
// URL is directory; exclude it
continue;
}
if (!recurse) {
// check whether this URL is a *direct* child of the directory
final int slash = urlEncoded.indexOf("/", prefix.length());
if (slash >= 0 && slash != urlEncoded.length() - 1) {
// not a direct child
continue;
}
}
result.add(new URL(baseURL + urlEncoded));
}
}
}
}
catch (final IOException e) {
e.printStackTrace();
}
catch (final URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
return result;
}
/**
* Finds {@link URL}s of available resources. Both JAR files and files on disk
* are searched, according to the following mechanism:
* <ol>
* <li>Resources at the given {@code pathPrefix} are discovered using
* {@link ClassLoader#getResources(String)} with the current thread's context
* class loader. In particular, this invocation discovers resources in JAR
* files beneath the given {@code pathPrefix}.</li>
* <li>The directory named {@code pathPrefix} beneath the given
* {@code baseDirectory} is scanned last, so that users can more easily
* override resources provided inside JAR files by placing a resource of the
* same name within that directory.</li>
* </ol>
* <p>
* In both cases, resources are then recursively scanned using
* {@link #listContents(URL)}, and anything matching the given {@code regex}
* pattern is added to the output map.
* </p>
*
* @param regex The regex to use when matching resources, or null to match
* everything.
* @param pathPrefix The path to search for resources.
* @param baseDirectory The {@code baseDirectory/pathPrefix} directory to scan
* <em>after</em> the URL resources.
* @return A map of URLs referencing the matched resources.
* @see AppUtils#getBaseDirectory
*/
public static Map<String, URL> findResources(final String regex,
final String pathPrefix, final File baseDirectory)
{
// scan URL resource paths first
final ClassLoader loader = Context.getClassLoader();
final ArrayList<URL> urls = new ArrayList<>();
try {
urls.addAll(Collections.list(loader.getResources(pathPrefix + "/")));
}
catch (final IOException exc) {
// error loading resources; proceed with an empty list
}
// scan directory second; user can thus override resources from JARs
if (baseDirectory != null) {
try {
urls.add(new File(baseDirectory, pathPrefix).toURI().toURL());
}
catch (final MalformedURLException exc) {
// error adding directory; proceed without it
}
}
return findResources(regex, urls);
}
/**
* Finds {@link URL}s of resources known to the system.
* <p>
* Each of the given {@link URL}s is recursively scanned using
* {@link #listContents(URL)}, and anything matching the given {@code regex}
* pattern is added to the output map.
* </p>
*
* @param regex The regex to use when matching resources, or null to match
* everything.
* @param urls Paths to search for resources.
* @return A map of URLs referencing the matched resources.
*/
public static Map<String, URL> findResources(final String regex,
final Iterable<URL> urls)
{
final HashMap<String, URL> result = new HashMap<>();
final Pattern pattern = regex == null ? null : Pattern.compile(regex);
for (final URL url : urls) {
getResources(pattern, result, url);
}
return result;
}
// -- Helper methods --
/** Builds the {@link #VERSION_PATTERN} constant. */
private static Pattern buildVersionPattern() {
final String version =
"\\d+(\\.\\d+|\\d{7})+[a-z]?\\d?(-[A-Za-z0-9.]+?|\\.GA)*?";
final String suffix = "\\.jar(-[a-z]*)?";
return Pattern.compile("(.+?)(-" + version + ")?((-(" + classifiers() +
"))?(" + suffix + "))");
}
/** Helper method of {@link #buildVersionPattern()}. */
private static String classifiers() {
final String[] classifiers = {
"swing",
"swt",
"shaded",
"sources",
"javadoc",
"natives?-?\\w*",
"(natives-)?(android|linux|macosx|macos|solaris|windows)-" +
"(aarch64|amd64|arm64|armv6hf|armv6|arm|" +
"i386|i486|i586|i686|universal|x86[_-]32|x86[_-]64|x86)",
};
final StringBuilder sb = new StringBuilder("(");
for (final String classifier : classifiers) {
if (sb.length() > 1) sb.append("|");
sb.append(classifier);
}
sb.append(")");
return sb.toString();
}
/** Helper method of {@link #findResources(String, Iterable)}. */
private static void getResources(final Pattern pattern,
final Map<String, URL> result, final URL base)
{
final String prefix = urlPath(base);
if (prefix == null) return; // unsupported base URL
for (final URL url : FileUtils.listContents(base)) {
final String s = urlPath(url);
if (s == null || !s.startsWith(prefix)) continue;
if (pattern == null || pattern.matcher(s).matches()) {
// this resource matches the pattern
final String key = urlPath(s.substring(prefix.length()));
if (key != null) result.put(key, url);
}
}
}
/** Helper method of {@link #getResources(Pattern, Map, URL)}. */
private static String urlPath(final URL url) {
try {
return url.toURI().toString();
}
catch (final URISyntaxException exc) {
return null;
}
}
/** Helper method of {@link #getResources(Pattern, Map, URL)}. */
private static String urlPath(final String path) {
try {
return new URI(path).getPath();
}
catch (final URISyntaxException exc) {
return null;
}
}
// -- Deprecated methods --
/**
* Returns the {@link Matcher} object dissecting a versioned file name.
*
* @param filename the file name
* @return the {@link Matcher} object
* @deprecated see {@link #stripFilenameVersion(String)}
*/
@Deprecated
public static Matcher matchVersionedFilename(final String filename) {
return VERSION_PATTERN.matcher(filename);
}
}