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