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