Locations.java revision 2734:b96d74fa60aa
1/*
2 * Copyright (c) 2003, 2014, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25package com.sun.tools.javac.file;
26
27import java.io.File;
28import java.io.FileNotFoundException;
29import java.io.IOException;
30import java.net.MalformedURLException;
31import java.net.URL;
32import java.nio.file.Files;
33import java.nio.file.Path;
34import java.nio.file.Paths;
35import java.util.ArrayList;
36import java.util.Arrays;
37import java.util.Collection;
38import java.util.Collections;
39import java.util.EnumMap;
40import java.util.EnumSet;
41import java.util.HashMap;
42import java.util.HashSet;
43import java.util.Iterator;
44import java.util.LinkedHashSet;
45import java.util.Map;
46import java.util.Set;
47import java.util.regex.Pattern;
48import java.util.stream.Stream;
49import java.util.zip.ZipFile;
50
51import javax.tools.JavaFileManager;
52import javax.tools.JavaFileManager.Location;
53import javax.tools.StandardJavaFileManager;
54import javax.tools.StandardLocation;
55
56import com.sun.tools.javac.code.Lint;
57import com.sun.tools.javac.main.Option;
58import com.sun.tools.javac.util.ListBuffer;
59import com.sun.tools.javac.util.Log;
60import com.sun.tools.javac.util.StringUtils;
61
62import static javax.tools.StandardLocation.CLASS_PATH;
63import static javax.tools.StandardLocation.PLATFORM_CLASS_PATH;
64import static javax.tools.StandardLocation.SOURCE_PATH;
65
66import static com.sun.tools.javac.main.Option.BOOTCLASSPATH;
67import static com.sun.tools.javac.main.Option.DJAVA_ENDORSED_DIRS;
68import static com.sun.tools.javac.main.Option.DJAVA_EXT_DIRS;
69import static com.sun.tools.javac.main.Option.ENDORSEDDIRS;
70import static com.sun.tools.javac.main.Option.EXTDIRS;
71import static com.sun.tools.javac.main.Option.XBOOTCLASSPATH;
72import static com.sun.tools.javac.main.Option.XBOOTCLASSPATH_APPEND;
73import static com.sun.tools.javac.main.Option.XBOOTCLASSPATH_PREPEND;
74
75/**
76 * This class converts command line arguments, environment variables and system properties (in
77 * File.pathSeparator-separated String form) into a boot class path, user class path, and source
78 * path (in {@code Collection<String>} form).
79 *
80 * <p>
81 * <b>This is NOT part of any supported API. If you write code that depends on this, you do so at
82 * your own risk. This code and its internal interfaces are subject to change or deletion without
83 * notice.</b>
84 */
85public class Locations {
86
87    /**
88     * The log to use for warning output
89     */
90    private Log log;
91
92    /**
93     * Access to (possibly cached) file info
94     */
95    private FSInfo fsInfo;
96
97    /**
98     * Whether to warn about non-existent path elements
99     */
100    private boolean warn;
101
102    public Locations() {
103        initHandlers();
104    }
105
106    // could replace Lint by "boolean warn"
107    public void update(Log log, Lint lint, FSInfo fsInfo) {
108        this.log = log;
109        warn = lint.isEnabled(Lint.LintCategory.PATH);
110        this.fsInfo = fsInfo;
111    }
112
113    public Collection<Path> bootClassPath() {
114        return getLocation(PLATFORM_CLASS_PATH);
115    }
116
117    public boolean isDefaultBootClassPath() {
118        BootClassPathLocationHandler h
119                = (BootClassPathLocationHandler) getHandler(PLATFORM_CLASS_PATH);
120        return h.isDefault();
121    }
122
123    boolean isDefaultBootClassPathRtJar(Path file) {
124        BootClassPathLocationHandler h
125                = (BootClassPathLocationHandler) getHandler(PLATFORM_CLASS_PATH);
126        return h.isDefaultRtJar(file);
127    }
128
129    public Collection<Path> userClassPath() {
130        return getLocation(CLASS_PATH);
131    }
132
133    public Collection<Path> sourcePath() {
134        Collection<Path> p = getLocation(SOURCE_PATH);
135        // TODO: this should be handled by the LocationHandler
136        return p == null || p.isEmpty() ? null : p;
137    }
138
139    /**
140     * Split a search path into its elements. Empty path elements will be ignored.
141     *
142     * @param searchPath The search path to be split
143     * @return The elements of the path
144     */
145    private static Iterable<Path> getPathEntries(String searchPath) {
146        return getPathEntries(searchPath, null);
147    }
148
149    /**
150     * Split a search path into its elements. If emptyPathDefault is not null, all empty elements in the
151     * path, including empty elements at either end of the path, will be replaced with the value of
152     * emptyPathDefault.
153     *
154     * @param searchPath The search path to be split
155     * @param emptyPathDefault The value to substitute for empty path elements, or null, to ignore
156     * empty path elements
157     * @return The elements of the path
158     */
159    private static Iterable<Path> getPathEntries(String searchPath, Path emptyPathDefault) {
160        ListBuffer<Path> entries = new ListBuffer<>();
161        for (String s: searchPath.split(Pattern.quote(File.pathSeparator), -1)) {
162            if (s.isEmpty()) {
163                if (emptyPathDefault != null) {
164                    entries.add(emptyPathDefault);
165                }
166            } else {
167                entries.add(Paths.get(s));
168            }
169        }
170        return entries;
171    }
172
173    /**
174     * Utility class to help evaluate a path option. Duplicate entries are ignored, jar class paths
175     * can be expanded.
176     */
177    private class SearchPath extends LinkedHashSet<Path> {
178
179        private static final long serialVersionUID = 0;
180
181        private boolean expandJarClassPaths = false;
182        private final Set<Path> canonicalValues = new HashSet<>();
183
184        public SearchPath expandJarClassPaths(boolean x) {
185            expandJarClassPaths = x;
186            return this;
187        }
188
189        /**
190         * What to use when path element is the empty string
191         */
192        private Path emptyPathDefault = null;
193
194        public SearchPath emptyPathDefault(Path x) {
195            emptyPathDefault = x;
196            return this;
197        }
198
199        public SearchPath addDirectories(String dirs, boolean warn) {
200            boolean prev = expandJarClassPaths;
201            expandJarClassPaths = true;
202            try {
203                if (dirs != null) {
204                    for (Path dir : getPathEntries(dirs)) {
205                        addDirectory(dir, warn);
206                    }
207                }
208                return this;
209            } finally {
210                expandJarClassPaths = prev;
211            }
212        }
213
214        public SearchPath addDirectories(String dirs) {
215            return addDirectories(dirs, warn);
216        }
217
218        private void addDirectory(Path dir, boolean warn) {
219            if (!Files.isDirectory(dir)) {
220                if (warn) {
221                    log.warning(Lint.LintCategory.PATH,
222                            "dir.path.element.not.found", dir);
223                }
224                return;
225            }
226
227            try (Stream<Path> s = Files.list(dir)) {
228                s.filter(dirEntry -> isArchive(dirEntry))
229                        .forEach(dirEntry -> addFile(dirEntry, warn));
230            } catch (IOException ignore) {
231            }
232        }
233
234        public SearchPath addFiles(String files, boolean warn) {
235            if (files != null) {
236                addFiles(getPathEntries(files, emptyPathDefault), warn);
237            }
238            return this;
239        }
240
241        public SearchPath addFiles(String files) {
242            return addFiles(files, warn);
243        }
244
245        public SearchPath addFiles(Iterable<? extends Path> files, boolean warn) {
246            if (files != null) {
247                for (Path file : files) {
248                    addFile(file, warn);
249                }
250            }
251            return this;
252        }
253
254        public SearchPath addFiles(Iterable<? extends Path> files) {
255            return addFiles(files, warn);
256        }
257
258        public void addFile(Path file, boolean warn) {
259            if (contains(file)) {
260                // discard duplicates
261                return;
262            }
263
264            if (!fsInfo.exists(file)) {
265                /* No such file or directory exists */
266                if (warn) {
267                    log.warning(Lint.LintCategory.PATH,
268                            "path.element.not.found", file);
269                }
270                super.add(file);
271                return;
272            }
273
274            Path canonFile = fsInfo.getCanonicalFile(file);
275            if (canonicalValues.contains(canonFile)) {
276                /* Discard duplicates and avoid infinite recursion */
277                return;
278            }
279
280            if (fsInfo.isFile(file)) {
281                /* File is an ordinary file. */
282                if (!isArchive(file)) {
283                    /* Not a recognized extension; open it to see if
284                     it looks like a valid zip file. */
285                    try {
286                        ZipFile z = new ZipFile(file.toFile());
287                        z.close();
288                        if (warn) {
289                            log.warning(Lint.LintCategory.PATH,
290                                    "unexpected.archive.file", file);
291                        }
292                    } catch (IOException e) {
293                        // FIXME: include e.getLocalizedMessage in warning
294                        if (warn) {
295                            log.warning(Lint.LintCategory.PATH,
296                                    "invalid.archive.file", file);
297                        }
298                        return;
299                    }
300                }
301            }
302
303            /* Now what we have left is either a directory or a file name
304             conforming to archive naming convention */
305            super.add(file);
306            canonicalValues.add(canonFile);
307
308            if (expandJarClassPaths && fsInfo.isFile(file)) {
309                addJarClassPath(file, warn);
310            }
311        }
312
313        // Adds referenced classpath elements from a jar's Class-Path
314        // Manifest entry.  In some future release, we may want to
315        // update this code to recognize URLs rather than simple
316        // filenames, but if we do, we should redo all path-related code.
317        private void addJarClassPath(Path jarFile, boolean warn) {
318            try {
319                for (Path f : fsInfo.getJarClassPath(jarFile)) {
320                    addFile(f, warn);
321                }
322            } catch (IOException e) {
323                log.error("error.reading.file", jarFile, JavacFileManager.getMessage(e));
324            }
325        }
326    }
327
328    /**
329     * Base class for handling support for the representation of Locations. Implementations are
330     * responsible for handling the interactions between the command line options for a location,
331     * and API access via setLocation.
332     *
333     * @see #initHandlers
334     * @see #getHandler
335     */
336    protected abstract class LocationHandler {
337
338        final Location location;
339        final Set<Option> options;
340
341        /**
342         * Create a handler. The location and options provide a way to map from a location or an
343         * option to the corresponding handler.
344         *
345         * @param location the location for which this is the handler
346         * @param options the options affecting this location
347         * @see #initHandlers
348         */
349        protected LocationHandler(Location location, Option... options) {
350            this.location = location;
351            this.options = options.length == 0
352                    ? EnumSet.noneOf(Option.class)
353                    : EnumSet.copyOf(Arrays.asList(options));
354        }
355
356        /**
357         * @see JavaFileManager#handleOption
358         */
359        abstract boolean handleOption(Option option, String value);
360
361        /**
362         * @see StandardJavaFileManager#getLocation
363         */
364        abstract Collection<Path> getLocation();
365
366        /**
367         * @see StandardJavaFileManager#setLocation
368         */
369        abstract void setLocation(Iterable<? extends Path> files) throws IOException;
370    }
371
372    /**
373     * General purpose implementation for output locations, such as -d/CLASS_OUTPUT and
374     * -s/SOURCE_OUTPUT. All options are treated as equivalent (i.e. aliases.) The value is a single
375     * file, possibly null.
376     */
377    private class OutputLocationHandler extends LocationHandler {
378
379        private Path outputDir;
380
381        OutputLocationHandler(Location location, Option... options) {
382            super(location, options);
383        }
384
385        @Override
386        boolean handleOption(Option option, String value) {
387            if (!options.contains(option)) {
388                return false;
389            }
390
391            // TODO: could/should validate outputDir exists and is a directory
392            // need to decide how best to report issue for benefit of
393            // direct API call on JavaFileManager.handleOption(specifies IAE)
394            // vs. command line decoding.
395            outputDir = (value == null) ? null : Paths.get(value);
396            return true;
397        }
398
399        @Override
400        Collection<Path> getLocation() {
401            return (outputDir == null) ? null : Collections.singleton(outputDir);
402        }
403
404        @Override
405        void setLocation(Iterable<? extends Path> files) throws IOException {
406            if (files == null) {
407                outputDir = null;
408            } else {
409                Iterator<? extends Path> pathIter = files.iterator();
410                if (!pathIter.hasNext()) {
411                    throw new IllegalArgumentException("empty path for directory");
412                }
413                Path dir = pathIter.next();
414                if (pathIter.hasNext()) {
415                    throw new IllegalArgumentException("path too long for directory");
416                }
417                if (!Files.exists(dir)) {
418                    throw new FileNotFoundException(dir + ": does not exist");
419                } else if (!Files.isDirectory(dir)) {
420                    throw new IOException(dir + ": not a directory");
421                }
422                outputDir = dir;
423            }
424        }
425    }
426
427    /**
428     * General purpose implementation for search path locations, such as -sourcepath/SOURCE_PATH and
429     * -processorPath/ANNOTATION_PROCESS_PATH. All options are treated as equivalent (i.e. aliases.)
430     * The value is an ordered set of files and/or directories.
431     */
432    private class SimpleLocationHandler extends LocationHandler {
433
434        protected Collection<Path> searchPath;
435
436        SimpleLocationHandler(Location location, Option... options) {
437            super(location, options);
438        }
439
440        @Override
441        boolean handleOption(Option option, String value) {
442            if (!options.contains(option)) {
443                return false;
444            }
445            searchPath = value == null ? null
446                    : Collections.unmodifiableCollection(createPath().addFiles(value));
447            return true;
448        }
449
450        @Override
451        Collection<Path> getLocation() {
452            return searchPath;
453        }
454
455        @Override
456        void setLocation(Iterable<? extends Path> files) {
457            SearchPath p;
458            if (files == null) {
459                p = computePath(null);
460            } else {
461                p = createPath().addFiles(files);
462            }
463            searchPath = Collections.unmodifiableCollection(p);
464        }
465
466        protected SearchPath computePath(String value) {
467            return createPath().addFiles(value);
468        }
469
470        protected SearchPath createPath() {
471            return new SearchPath();
472        }
473    }
474
475    /**
476     * Subtype of SimpleLocationHandler for -classpath/CLASS_PATH. If no value is given, a default
477     * is provided, based on system properties and other values.
478     */
479    private class ClassPathLocationHandler extends SimpleLocationHandler {
480
481        ClassPathLocationHandler() {
482            super(StandardLocation.CLASS_PATH,
483                    Option.CLASSPATH, Option.CP);
484        }
485
486        @Override
487        Collection<Path> getLocation() {
488            lazy();
489            return searchPath;
490        }
491
492        @Override
493        protected SearchPath computePath(String value) {
494            String cp = value;
495
496            // CLASSPATH environment variable when run from `javac'.
497            if (cp == null) {
498                cp = System.getProperty("env.class.path");
499            }
500
501            // If invoked via a java VM (not the javac launcher), use the
502            // platform class path
503            if (cp == null && System.getProperty("application.home") == null) {
504                cp = System.getProperty("java.class.path");
505            }
506
507            // Default to current working directory.
508            if (cp == null) {
509                cp = ".";
510            }
511
512            return createPath().addFiles(cp);
513        }
514
515        @Override
516        protected SearchPath createPath() {
517            return new SearchPath()
518                    .expandJarClassPaths(true) // Only search user jars for Class-Paths
519                    .emptyPathDefault(Paths.get("."));  // Empty path elt ==> current directory
520        }
521
522        private void lazy() {
523            if (searchPath == null) {
524                setLocation(null);
525            }
526        }
527    }
528
529    /**
530     * Custom subtype of LocationHandler for PLATFORM_CLASS_PATH. Various options are supported for
531     * different components of the platform class path. Setting a value with setLocation overrides
532     * all existing option values. Setting any option overrides any value set with setLocation, and
533     * reverts to using default values for options that have not been set. Setting -bootclasspath or
534     * -Xbootclasspath overrides any existing value for -Xbootclasspath/p: and -Xbootclasspath/a:.
535     */
536    private class BootClassPathLocationHandler extends LocationHandler {
537
538        private Collection<Path> searchPath;
539        final Map<Option, String> optionValues = new EnumMap<>(Option.class);
540
541        /**
542         * rt.jar as found on the default bootclasspath. If the user specified a bootclasspath, null
543         * is used.
544         */
545        private Path defaultBootClassPathRtJar = null;
546
547        /**
548         * Is bootclasspath the default?
549         */
550        private boolean isDefaultBootClassPath;
551
552        BootClassPathLocationHandler() {
553            super(StandardLocation.PLATFORM_CLASS_PATH,
554                    Option.BOOTCLASSPATH, Option.XBOOTCLASSPATH,
555                    Option.XBOOTCLASSPATH_PREPEND,
556                    Option.XBOOTCLASSPATH_APPEND,
557                    Option.ENDORSEDDIRS, Option.DJAVA_ENDORSED_DIRS,
558                    Option.EXTDIRS, Option.DJAVA_EXT_DIRS);
559        }
560
561        boolean isDefault() {
562            lazy();
563            return isDefaultBootClassPath;
564        }
565
566        boolean isDefaultRtJar(Path file) {
567            lazy();
568            return file.equals(defaultBootClassPathRtJar);
569        }
570
571        @Override
572        boolean handleOption(Option option, String value) {
573            if (!options.contains(option)) {
574                return false;
575            }
576
577            option = canonicalize(option);
578            optionValues.put(option, value);
579            if (option == BOOTCLASSPATH) {
580                optionValues.remove(XBOOTCLASSPATH_PREPEND);
581                optionValues.remove(XBOOTCLASSPATH_APPEND);
582            }
583            searchPath = null;  // reset to "uninitialized"
584            return true;
585        }
586        // where
587        // TODO: would be better if option aliasing was handled at a higher
588        // level
589        private Option canonicalize(Option option) {
590            switch (option) {
591                case XBOOTCLASSPATH:
592                    return Option.BOOTCLASSPATH;
593                case DJAVA_ENDORSED_DIRS:
594                    return Option.ENDORSEDDIRS;
595                case DJAVA_EXT_DIRS:
596                    return Option.EXTDIRS;
597                default:
598                    return option;
599            }
600        }
601
602        @Override
603        Collection<Path> getLocation() {
604            lazy();
605            return searchPath;
606        }
607
608        @Override
609        void setLocation(Iterable<? extends Path> files) {
610            if (files == null) {
611                searchPath = null;  // reset to "uninitialized"
612            } else {
613                defaultBootClassPathRtJar = null;
614                isDefaultBootClassPath = false;
615                SearchPath p = new SearchPath().addFiles(files, false);
616                searchPath = Collections.unmodifiableCollection(p);
617                optionValues.clear();
618            }
619        }
620
621        SearchPath computePath() {
622            defaultBootClassPathRtJar = null;
623            SearchPath path = new SearchPath();
624
625            String bootclasspathOpt = optionValues.get(BOOTCLASSPATH);
626            String endorseddirsOpt = optionValues.get(ENDORSEDDIRS);
627            String extdirsOpt = optionValues.get(EXTDIRS);
628            String xbootclasspathPrependOpt = optionValues.get(XBOOTCLASSPATH_PREPEND);
629            String xbootclasspathAppendOpt = optionValues.get(XBOOTCLASSPATH_APPEND);
630            path.addFiles(xbootclasspathPrependOpt);
631
632            if (endorseddirsOpt != null) {
633                path.addDirectories(endorseddirsOpt);
634            } else {
635                path.addDirectories(System.getProperty("java.endorsed.dirs"), false);
636            }
637
638            if (bootclasspathOpt != null) {
639                path.addFiles(bootclasspathOpt);
640            } else {
641                // Standard system classes for this compiler's release.
642                String files = System.getProperty("sun.boot.class.path");
643                path.addFiles(files, false);
644                Path rt_jar = Paths.get("rt.jar");
645                for (Path file : getPathEntries(files)) {
646                    if (file.getFileName().equals(rt_jar)) {
647                        defaultBootClassPathRtJar = file;
648                    }
649                }
650            }
651
652            path.addFiles(xbootclasspathAppendOpt);
653
654            // Strictly speaking, standard extensions are not bootstrap
655            // classes, but we treat them identically, so we'll pretend
656            // that they are.
657            if (extdirsOpt != null) {
658                path.addDirectories(extdirsOpt);
659            } else {
660                path.addDirectories(System.getProperty("java.ext.dirs"), false);
661            }
662
663            isDefaultBootClassPath
664                    = (xbootclasspathPrependOpt == null)
665                    && (bootclasspathOpt == null)
666                    && (xbootclasspathAppendOpt == null);
667
668            return path;
669        }
670
671        private void lazy() {
672            if (searchPath == null) {
673                searchPath = Collections.unmodifiableCollection(computePath());
674            }
675        }
676    }
677
678    Map<Location, LocationHandler> handlersForLocation;
679    Map<Option, LocationHandler> handlersForOption;
680
681    void initHandlers() {
682        handlersForLocation = new HashMap<>();
683        handlersForOption = new EnumMap<>(Option.class);
684
685        LocationHandler[] handlers = {
686            new BootClassPathLocationHandler(),
687            new ClassPathLocationHandler(),
688            new SimpleLocationHandler(StandardLocation.SOURCE_PATH, Option.SOURCEPATH),
689            new SimpleLocationHandler(StandardLocation.ANNOTATION_PROCESSOR_PATH, Option.PROCESSORPATH),
690            new OutputLocationHandler((StandardLocation.CLASS_OUTPUT), Option.D),
691            new OutputLocationHandler((StandardLocation.SOURCE_OUTPUT), Option.S),
692            new OutputLocationHandler((StandardLocation.NATIVE_HEADER_OUTPUT), Option.H)
693        };
694
695        for (LocationHandler h : handlers) {
696            handlersForLocation.put(h.location, h);
697            for (Option o : h.options) {
698                handlersForOption.put(o, h);
699            }
700        }
701    }
702
703    public boolean handleOption(Option option, String value) {
704        LocationHandler h = handlersForOption.get(option);
705        return (h == null ? false : h.handleOption(option, value));
706    }
707
708    Collection<Path> getLocation(Location location) {
709        LocationHandler h = getHandler(location);
710        return (h == null ? null : h.getLocation());
711    }
712
713    Path getOutputLocation(Location location) {
714        if (!location.isOutputLocation()) {
715            throw new IllegalArgumentException();
716        }
717        LocationHandler h = getHandler(location);
718        return ((OutputLocationHandler) h).outputDir;
719    }
720
721    void setLocation(Location location, Iterable<? extends Path> files) throws IOException {
722        LocationHandler h = getHandler(location);
723        if (h == null) {
724            if (location.isOutputLocation()) {
725                h = new OutputLocationHandler(location);
726            } else {
727                h = new SimpleLocationHandler(location);
728            }
729            handlersForLocation.put(location, h);
730        }
731        h.setLocation(files);
732    }
733
734    protected LocationHandler getHandler(Location location) {
735        location.getClass(); // null check
736        return handlersForLocation.get(location);
737    }
738
739    /**
740     * Is this the name of an archive file?
741     */
742    private boolean isArchive(Path file) {
743        String n = StringUtils.toLowerCase(file.getFileName().toString());
744        return fsInfo.isFile(file)
745                && (n.endsWith(".jar") || n.endsWith(".zip"));
746    }
747
748    /**
749     * Utility method for converting a search path string to an array of directory and JAR file
750     * URLs.
751     *
752     * Note that this method is called by the DocletInvoker.
753     *
754     * @param path the search path string
755     * @return the resulting array of directory and JAR file URLs
756     */
757    public static URL[] pathToURLs(String path) {
758        java.util.List<URL> urls = new ArrayList<>();
759        for (String s: path.split(Pattern.quote(File.pathSeparator))) {
760            if (!s.isEmpty()) {
761                URL url = fileToURL(Paths.get(s));
762                if (url != null) {
763                    urls.add(url);
764                }
765            }
766        }
767        return urls.toArray(new URL[urls.size()]);
768    }
769
770    /**
771     * Returns the directory or JAR file URL corresponding to the specified local file name.
772     *
773     * @param file the Path object
774     * @return the resulting directory or JAR file URL, or null if unknown
775     */
776    private static URL fileToURL(Path file) {
777        Path p;
778        try {
779            p = file.toRealPath();
780        } catch (IOException e) {
781            p = file.toAbsolutePath();
782        }
783        try {
784            return p.normalize().toUri().toURL();
785        } catch (MalformedURLException e) {
786            return null;
787        }
788    }
789}
790