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