JavacFileManager.java revision 2689:f839b50088bc
1/*
2 * Copyright (c) 2005, 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 */
25
26package com.sun.tools.javac.file;
27
28import java.io.ByteArrayOutputStream;
29import java.io.File;
30import java.io.FileNotFoundException;
31import java.io.IOException;
32import java.io.OutputStreamWriter;
33import java.net.MalformedURLException;
34import java.net.URI;
35import java.net.URISyntaxException;
36import java.net.URL;
37import java.nio.CharBuffer;
38import java.nio.charset.Charset;
39import java.util.ArrayList;
40import java.util.Arrays;
41import java.util.Collection;
42import java.util.Collections;
43import java.util.Comparator;
44import java.util.EnumSet;
45import java.util.HashMap;
46import java.util.Iterator;
47import java.util.Map;
48import java.util.Set;
49import java.util.zip.ZipFile;
50
51import javax.lang.model.SourceVersion;
52import javax.tools.FileObject;
53import javax.tools.JavaFileManager;
54import javax.tools.JavaFileObject;
55import javax.tools.StandardJavaFileManager;
56
57import com.sun.tools.javac.file.RelativePath.RelativeFile;
58import com.sun.tools.javac.file.RelativePath.RelativeDirectory;
59import com.sun.tools.javac.util.BaseFileManager;
60import com.sun.tools.javac.util.Context;
61import com.sun.tools.javac.util.DefinedBy;
62import com.sun.tools.javac.util.DefinedBy.Api;
63import com.sun.tools.javac.util.List;
64import com.sun.tools.javac.util.ListBuffer;
65
66import static javax.tools.StandardLocation.*;
67
68/**
69 * This class provides access to the source, class and other files
70 * used by the compiler and related tools.
71 *
72 * <p><b>This is NOT part of any supported API.
73 * If you write code that depends on this, you do so at your own risk.
74 * This code and its internal interfaces are subject to change or
75 * deletion without notice.</b>
76 */
77public class JavacFileManager extends BaseFileManager implements StandardJavaFileManager {
78
79    @SuppressWarnings("cast")
80    public static char[] toArray(CharBuffer buffer) {
81        if (buffer.hasArray())
82            return ((CharBuffer)buffer.compact().flip()).array();
83        else
84            return buffer.toString().toCharArray();
85    }
86
87    private FSInfo fsInfo;
88
89    private boolean contextUseOptimizedZip;
90    private ZipFileIndexCache zipFileIndexCache;
91
92    private final Set<JavaFileObject.Kind> sourceOrClass =
93        EnumSet.of(JavaFileObject.Kind.SOURCE, JavaFileObject.Kind.CLASS);
94
95    protected boolean mmappedIO;
96    protected boolean symbolFileEnabled;
97
98    protected enum SortFiles implements Comparator<File> {
99        FORWARD {
100            public int compare(File f1, File f2) {
101                return f1.getName().compareTo(f2.getName());
102            }
103        },
104        REVERSE {
105            public int compare(File f1, File f2) {
106                return -f1.getName().compareTo(f2.getName());
107            }
108        }
109    }
110
111    protected SortFiles sortFiles;
112
113    /**
114     * Register a Context.Factory to create a JavacFileManager.
115     */
116    public static void preRegister(Context context) {
117        context.put(JavaFileManager.class, new Context.Factory<JavaFileManager>() {
118            public JavaFileManager make(Context c) {
119                return new JavacFileManager(c, true, null);
120            }
121        });
122    }
123
124    /**
125     * Create a JavacFileManager using a given context, optionally registering
126     * it as the JavaFileManager for that context.
127     */
128    public JavacFileManager(Context context, boolean register, Charset charset) {
129        super(charset);
130        if (register)
131            context.put(JavaFileManager.class, this);
132        setContext(context);
133        if (System.getProperty("show.fm.open.close") != null)
134            System.err.println("JavacFileManager.open " + this.hashCode());
135    }
136
137    /**
138     * Set the context for JavacFileManager.
139     */
140    @Override
141    public void setContext(Context context) {
142        super.setContext(context);
143
144        fsInfo = FSInfo.instance(context);
145
146        contextUseOptimizedZip = options.getBoolean("useOptimizedZip", true);
147        if (contextUseOptimizedZip)
148            zipFileIndexCache = ZipFileIndexCache.getSharedInstance();
149
150        mmappedIO = options.isSet("mmappedIO");
151        symbolFileEnabled = !options.isSet("ignore.symbol.file");
152
153        String sf = options.get("sortFiles");
154        if (sf != null) {
155            sortFiles = (sf.equals("reverse") ? SortFiles.REVERSE : SortFiles.FORWARD);
156        }
157    }
158
159    /**
160     * Set whether or not to use ct.sym as an alternate to rt.jar.
161     */
162    public void setSymbolFileEnabled(boolean b) {
163        symbolFileEnabled = b;
164    }
165
166    public JavaFileObject getFileForInput(String name) {
167        return getRegularFile(new File(name));
168    }
169
170    public JavaFileObject getRegularFile(File file) {
171        return new RegularFileObject(this, file);
172    }
173
174    public JavaFileObject getFileForOutput(String classname,
175                                           JavaFileObject.Kind kind,
176                                           JavaFileObject sibling)
177        throws IOException
178    {
179        return getJavaFileForOutput(CLASS_OUTPUT, classname, kind, sibling);
180    }
181
182    @DefinedBy(Api.COMPILER)
183    public Iterable<? extends JavaFileObject> getJavaFileObjectsFromStrings(Iterable<String> names) {
184        ListBuffer<File> files = new ListBuffer<>();
185        for (String name : names)
186            files.append(new File(nullCheck(name)));
187        return getJavaFileObjectsFromFiles(files.toList());
188    }
189
190    @DefinedBy(Api.COMPILER)
191    public Iterable<? extends JavaFileObject> getJavaFileObjects(String... names) {
192        return getJavaFileObjectsFromStrings(Arrays.asList(nullCheck(names)));
193    }
194
195    private static boolean isValidName(String name) {
196        // Arguably, isValidName should reject keywords (such as in SourceVersion.isName() ),
197        // but the set of keywords depends on the source level, and we don't want
198        // impls of JavaFileManager to have to be dependent on the source level.
199        // Therefore we simply check that the argument is a sequence of identifiers
200        // separated by ".".
201        for (String s : name.split("\\.", -1)) {
202            if (!SourceVersion.isIdentifier(s))
203                return false;
204        }
205        return true;
206    }
207
208    private static void validateClassName(String className) {
209        if (!isValidName(className))
210            throw new IllegalArgumentException("Invalid class name: " + className);
211    }
212
213    private static void validatePackageName(String packageName) {
214        if (packageName.length() > 0 && !isValidName(packageName))
215            throw new IllegalArgumentException("Invalid packageName name: " + packageName);
216    }
217
218    public static void testName(String name,
219                                boolean isValidPackageName,
220                                boolean isValidClassName)
221    {
222        try {
223            validatePackageName(name);
224            if (!isValidPackageName)
225                throw new AssertionError("Invalid package name accepted: " + name);
226            printAscii("Valid package name: \"%s\"", name);
227        } catch (IllegalArgumentException e) {
228            if (isValidPackageName)
229                throw new AssertionError("Valid package name rejected: " + name);
230            printAscii("Invalid package name: \"%s\"", name);
231        }
232        try {
233            validateClassName(name);
234            if (!isValidClassName)
235                throw new AssertionError("Invalid class name accepted: " + name);
236            printAscii("Valid class name: \"%s\"", name);
237        } catch (IllegalArgumentException e) {
238            if (isValidClassName)
239                throw new AssertionError("Valid class name rejected: " + name);
240            printAscii("Invalid class name: \"%s\"", name);
241        }
242    }
243
244    private static void printAscii(String format, Object... args) {
245        String message;
246        try {
247            final String ascii = "US-ASCII";
248            message = new String(String.format(null, format, args).getBytes(ascii), ascii);
249        } catch (java.io.UnsupportedEncodingException ex) {
250            throw new AssertionError(ex);
251        }
252        System.out.println(message);
253    }
254
255
256    /**
257     * Insert all files in subdirectory subdirectory of directory directory
258     * which match fileKinds into resultList
259     */
260    private void listDirectory(File directory,
261                               RelativeDirectory subdirectory,
262                               Set<JavaFileObject.Kind> fileKinds,
263                               boolean recurse,
264                               ListBuffer<JavaFileObject> resultList) {
265        File d = subdirectory.getFile(directory);
266        if (!caseMapCheck(d, subdirectory))
267            return;
268
269        File[] files = d.listFiles();
270        if (files == null)
271            return;
272
273        if (sortFiles != null)
274            Arrays.sort(files, sortFiles);
275
276        for (File f: files) {
277            String fname = f.getName();
278            if (f.isDirectory()) {
279                if (recurse && SourceVersion.isIdentifier(fname)) {
280                    listDirectory(directory,
281                                  new RelativeDirectory(subdirectory, fname),
282                                  fileKinds,
283                                  recurse,
284                                  resultList);
285                }
286            } else {
287                if (isValidFile(fname, fileKinds)) {
288                    JavaFileObject fe =
289                        new RegularFileObject(this, fname, new File(d, fname));
290                    resultList.append(fe);
291                }
292            }
293        }
294    }
295
296    /**
297     * Insert all files in subdirectory subdirectory of archive archive
298     * which match fileKinds into resultList
299     */
300    private void listArchive(Archive archive,
301                               RelativeDirectory subdirectory,
302                               Set<JavaFileObject.Kind> fileKinds,
303                               boolean recurse,
304                               ListBuffer<JavaFileObject> resultList) {
305        // Get the files directly in the subdir
306        List<String> files = archive.getFiles(subdirectory);
307        if (files != null) {
308            for (; !files.isEmpty(); files = files.tail) {
309                String file = files.head;
310                if (isValidFile(file, fileKinds)) {
311                    resultList.append(archive.getFileObject(subdirectory, file));
312                }
313            }
314        }
315        if (recurse) {
316            for (RelativeDirectory s: archive.getSubdirectories()) {
317                if (subdirectory.contains(s)) {
318                    // Because the archive map is a flat list of directories,
319                    // the enclosing loop will pick up all child subdirectories.
320                    // Therefore, there is no need to recurse deeper.
321                    listArchive(archive, s, fileKinds, false, resultList);
322                }
323            }
324        }
325    }
326
327    /**
328     * container is a directory, a zip file, or a non-existant path.
329     * Insert all files in subdirectory subdirectory of container which
330     * match fileKinds into resultList
331     */
332    private void listContainer(File container,
333                               RelativeDirectory subdirectory,
334                               Set<JavaFileObject.Kind> fileKinds,
335                               boolean recurse,
336                               ListBuffer<JavaFileObject> resultList) {
337        Archive archive = archives.get(container);
338        if (archive == null) {
339            // archives are not created for directories.
340            if  (fsInfo.isDirectory(container)) {
341                listDirectory(container,
342                              subdirectory,
343                              fileKinds,
344                              recurse,
345                              resultList);
346                return;
347            }
348
349            // Not a directory; either a file or non-existant, create the archive
350            try {
351                archive = openArchive(container);
352            } catch (IOException ex) {
353                log.error("error.reading.file",
354                          container, getMessage(ex));
355                return;
356            }
357        }
358        listArchive(archive,
359                    subdirectory,
360                    fileKinds,
361                    recurse,
362                    resultList);
363    }
364
365    private boolean isValidFile(String s, Set<JavaFileObject.Kind> fileKinds) {
366        JavaFileObject.Kind kind = getKind(s);
367        return fileKinds.contains(kind);
368    }
369
370    private static final boolean fileSystemIsCaseSensitive =
371        File.separatorChar == '/';
372
373    /** Hack to make Windows case sensitive. Test whether given path
374     *  ends in a string of characters with the same case as given name.
375     *  Ignore file separators in both path and name.
376     */
377    private boolean caseMapCheck(File f, RelativePath name) {
378        if (fileSystemIsCaseSensitive) return true;
379        // Note that getCanonicalPath() returns the case-sensitive
380        // spelled file name.
381        String path;
382        try {
383            path = f.getCanonicalPath();
384        } catch (IOException ex) {
385            return false;
386        }
387        char[] pcs = path.toCharArray();
388        char[] ncs = name.path.toCharArray();
389        int i = pcs.length - 1;
390        int j = ncs.length - 1;
391        while (i >= 0 && j >= 0) {
392            while (i >= 0 && pcs[i] == File.separatorChar) i--;
393            while (j >= 0 && ncs[j] == '/') j--;
394            if (i >= 0 && j >= 0) {
395                if (pcs[i] != ncs[j]) return false;
396                i--;
397                j--;
398            }
399        }
400        return j < 0;
401    }
402
403    /**
404     * An archive provides a flat directory structure of a ZipFile by
405     * mapping directory names to lists of files (basenames).
406     */
407    public interface Archive {
408        void close() throws IOException;
409
410        boolean contains(RelativePath name);
411
412        JavaFileObject getFileObject(RelativeDirectory subdirectory, String file);
413
414        List<String> getFiles(RelativeDirectory subdirectory);
415
416        Set<RelativeDirectory> getSubdirectories();
417    }
418
419    public class MissingArchive implements Archive {
420        final File zipFileName;
421        public MissingArchive(File name) {
422            zipFileName = name;
423        }
424        public boolean contains(RelativePath name) {
425            return false;
426        }
427
428        public void close() {
429        }
430
431        public JavaFileObject getFileObject(RelativeDirectory subdirectory, String file) {
432            return null;
433        }
434
435        public List<String> getFiles(RelativeDirectory subdirectory) {
436            return List.nil();
437        }
438
439        public Set<RelativeDirectory> getSubdirectories() {
440            return Collections.emptySet();
441        }
442
443        @Override
444        public String toString() {
445            return "MissingArchive[" + zipFileName + "]";
446        }
447    }
448
449    /** A directory of zip files already opened.
450     */
451    Map<File, Archive> archives = new HashMap<>();
452
453    private static final String[] symbolFileLocation = { "lib", "ct.sym" };
454    private static final RelativeDirectory symbolFilePrefix
455            = new RelativeDirectory("META-INF/sym/rt.jar/");
456
457    /*
458     * This method looks for a ZipFormatException and takes appropriate
459     * evasive action. If there is a failure in the fast mode then we
460     * fail over to the platform zip, and allow it to deal with a potentially
461     * non compliant zip file.
462     */
463    protected Archive openArchive(File zipFilename) throws IOException {
464        try {
465            return openArchive(zipFilename, contextUseOptimizedZip);
466        } catch (IOException ioe) {
467            if (ioe instanceof ZipFileIndex.ZipFormatException) {
468                return openArchive(zipFilename, false);
469            } else {
470                throw ioe;
471            }
472        }
473    }
474
475    /** Open a new zip file directory, and cache it.
476     */
477    private Archive openArchive(File zipFileName, boolean useOptimizedZip) throws IOException {
478        File origZipFileName = zipFileName;
479        if (symbolFileEnabled && locations.isDefaultBootClassPathRtJar(zipFileName)) {
480            File file = zipFileName.getParentFile().getParentFile(); // ${java.home}
481            if (new File(file.getName()).equals(new File("jre")))
482                file = file.getParentFile();
483            // file == ${jdk.home}
484            for (String name : symbolFileLocation)
485                file = new File(file, name);
486            // file == ${jdk.home}/lib/ct.sym
487            if (file.exists())
488                zipFileName = file;
489        }
490
491        Archive archive;
492        try {
493
494            ZipFile zdir = null;
495
496            boolean usePreindexedCache = false;
497            String preindexCacheLocation = null;
498
499            if (!useOptimizedZip) {
500                zdir = new ZipFile(zipFileName);
501            } else {
502                usePreindexedCache = options.isSet("usezipindex");
503                preindexCacheLocation = options.get("java.io.tmpdir");
504                String optCacheLoc = options.get("cachezipindexdir");
505
506                if (optCacheLoc != null && optCacheLoc.length() != 0) {
507                    if (optCacheLoc.startsWith("\"")) {
508                        if (optCacheLoc.endsWith("\"")) {
509                            optCacheLoc = optCacheLoc.substring(1, optCacheLoc.length() - 1);
510                        }
511                        else {
512                            optCacheLoc = optCacheLoc.substring(1);
513                        }
514                    }
515
516                    File cacheDir = new File(optCacheLoc);
517                    if (cacheDir.exists() && cacheDir.canWrite()) {
518                        preindexCacheLocation = optCacheLoc;
519                        if (!preindexCacheLocation.endsWith("/") &&
520                            !preindexCacheLocation.endsWith(File.separator)) {
521                            preindexCacheLocation += File.separator;
522                        }
523                    }
524                }
525            }
526
527            if (origZipFileName == zipFileName) {
528                if (!useOptimizedZip) {
529                    archive = new ZipArchive(this, zdir);
530                } else {
531                    archive = new ZipFileIndexArchive(this,
532                                    zipFileIndexCache.getZipFileIndex(zipFileName,
533                                    null,
534                                    usePreindexedCache,
535                                    preindexCacheLocation,
536                                    options.isSet("writezipindexfiles")));
537                }
538            } else {
539                if (!useOptimizedZip) {
540                    archive = new SymbolArchive(this, origZipFileName, zdir, symbolFilePrefix);
541                } else {
542                    archive = new ZipFileIndexArchive(this,
543                                    zipFileIndexCache.getZipFileIndex(zipFileName,
544                                    symbolFilePrefix,
545                                    usePreindexedCache,
546                                    preindexCacheLocation,
547                                    options.isSet("writezipindexfiles")));
548                }
549            }
550        } catch (FileNotFoundException ex) {
551            archive = new MissingArchive(zipFileName);
552        } catch (ZipFileIndex.ZipFormatException zfe) {
553            throw zfe;
554        } catch (IOException ex) {
555            if (zipFileName.exists())
556                log.error("error.reading.file", zipFileName, getMessage(ex));
557            archive = new MissingArchive(zipFileName);
558        }
559
560        archives.put(origZipFileName, archive);
561        return archive;
562    }
563
564    /** Flush any output resources.
565     */
566    @DefinedBy(Api.COMPILER)
567    public void flush() {
568        contentCache.clear();
569    }
570
571    /**
572     * Close the JavaFileManager, releasing resources.
573     */
574    @DefinedBy(Api.COMPILER)
575    public void close() {
576        if (System.getProperty("show.fm.open.close") != null)
577            System.err.println("JavacFileManager.close " + this.hashCode());
578        for (Iterator<Archive> i = archives.values().iterator(); i.hasNext(); ) {
579            Archive a = i.next();
580            i.remove();
581            try {
582                a.close();
583            } catch (IOException e) {
584            }
585        }
586    }
587
588    @DefinedBy(Api.COMPILER)
589    public ClassLoader getClassLoader(Location location) {
590        nullCheck(location);
591        Iterable<? extends File> path = getLocation(location);
592        if (path == null)
593            return null;
594        ListBuffer<URL> lb = new ListBuffer<>();
595        for (File f: path) {
596            try {
597                lb.append(f.toURI().toURL());
598            } catch (MalformedURLException e) {
599                throw new AssertionError(e);
600            }
601        }
602
603        return getClassLoader(lb.toArray(new URL[lb.size()]));
604    }
605
606    @DefinedBy(Api.COMPILER)
607    public Iterable<JavaFileObject> list(Location location,
608                                         String packageName,
609                                         Set<JavaFileObject.Kind> kinds,
610                                         boolean recurse)
611        throws IOException
612    {
613        // validatePackageName(packageName);
614        nullCheck(packageName);
615        nullCheck(kinds);
616
617        Iterable<? extends File> path = getLocation(location);
618        if (path == null)
619            return List.nil();
620        RelativeDirectory subdirectory = RelativeDirectory.forPackage(packageName);
621        ListBuffer<JavaFileObject> results = new ListBuffer<>();
622
623        for (File directory : path)
624            listContainer(directory, subdirectory, kinds, recurse, results);
625        return results.toList();
626    }
627
628    @DefinedBy(Api.COMPILER)
629    public String inferBinaryName(Location location, JavaFileObject file) {
630        file.getClass(); // null check
631        location.getClass(); // null check
632        // Need to match the path semantics of list(location, ...)
633        Iterable<? extends File> path = getLocation(location);
634        if (path == null) {
635            return null;
636        }
637
638        if (file instanceof BaseFileObject) {
639            return ((BaseFileObject) file).inferBinaryName(path);
640        } else
641            throw new IllegalArgumentException(file.getClass().getName());
642    }
643
644    @DefinedBy(Api.COMPILER)
645    public boolean isSameFile(FileObject a, FileObject b) {
646        nullCheck(a);
647        nullCheck(b);
648        if (!(a instanceof BaseFileObject))
649            throw new IllegalArgumentException("Not supported: " + a);
650        if (!(b instanceof BaseFileObject))
651            throw new IllegalArgumentException("Not supported: " + b);
652        return a.equals(b);
653    }
654
655    @DefinedBy(Api.COMPILER)
656    public boolean hasLocation(Location location) {
657        return getLocation(location) != null;
658    }
659
660    @DefinedBy(Api.COMPILER)
661    public JavaFileObject getJavaFileForInput(Location location,
662                                              String className,
663                                              JavaFileObject.Kind kind)
664        throws IOException
665    {
666        nullCheck(location);
667        // validateClassName(className);
668        nullCheck(className);
669        nullCheck(kind);
670        if (!sourceOrClass.contains(kind))
671            throw new IllegalArgumentException("Invalid kind: " + kind);
672        return getFileForInput(location, RelativeFile.forClass(className, kind));
673    }
674
675    @DefinedBy(Api.COMPILER)
676    public FileObject getFileForInput(Location location,
677                                      String packageName,
678                                      String relativeName)
679        throws IOException
680    {
681        nullCheck(location);
682        // validatePackageName(packageName);
683        nullCheck(packageName);
684        if (!isRelativeUri(relativeName))
685            throw new IllegalArgumentException("Invalid relative name: " + relativeName);
686        RelativeFile name = packageName.length() == 0
687            ? new RelativeFile(relativeName)
688            : new RelativeFile(RelativeDirectory.forPackage(packageName), relativeName);
689        return getFileForInput(location, name);
690    }
691
692    private JavaFileObject getFileForInput(Location location, RelativeFile name) throws IOException {
693        Iterable<? extends File> path = getLocation(location);
694        if (path == null)
695            return null;
696
697        for (File dir: path) {
698            Archive a = archives.get(dir);
699            if (a == null) {
700                if (fsInfo.isDirectory(dir)) {
701                    File f = name.getFile(dir);
702                    if (f.exists())
703                        return new RegularFileObject(this, f);
704                    continue;
705                }
706                // Not a directory, create the archive
707                a = openArchive(dir);
708            }
709            // Process the archive
710            if (a.contains(name)) {
711                return a.getFileObject(name.dirname(), name.basename());
712            }
713        }
714        return null;
715    }
716
717    @DefinedBy(Api.COMPILER)
718    public JavaFileObject getJavaFileForOutput(Location location,
719                                               String className,
720                                               JavaFileObject.Kind kind,
721                                               FileObject sibling)
722        throws IOException
723    {
724        nullCheck(location);
725        // validateClassName(className);
726        nullCheck(className);
727        nullCheck(kind);
728        if (!sourceOrClass.contains(kind))
729            throw new IllegalArgumentException("Invalid kind: " + kind);
730        return getFileForOutput(location, RelativeFile.forClass(className, kind), sibling);
731    }
732
733    @DefinedBy(Api.COMPILER)
734    public FileObject getFileForOutput(Location location,
735                                       String packageName,
736                                       String relativeName,
737                                       FileObject sibling)
738        throws IOException
739    {
740        nullCheck(location);
741        // validatePackageName(packageName);
742        nullCheck(packageName);
743        if (!isRelativeUri(relativeName))
744            throw new IllegalArgumentException("Invalid relative name: " + relativeName);
745        RelativeFile name = packageName.length() == 0
746            ? new RelativeFile(relativeName)
747            : new RelativeFile(RelativeDirectory.forPackage(packageName), relativeName);
748        return getFileForOutput(location, name, sibling);
749    }
750
751    private JavaFileObject getFileForOutput(Location location,
752                                            RelativeFile fileName,
753                                            FileObject sibling)
754        throws IOException
755    {
756        File dir;
757        if (location == CLASS_OUTPUT) {
758            if (getClassOutDir() != null) {
759                dir = getClassOutDir();
760            } else {
761                File siblingDir = null;
762                if (sibling != null && sibling instanceof RegularFileObject) {
763                    siblingDir = ((RegularFileObject)sibling).file.getParentFile();
764                }
765                return new RegularFileObject(this, new File(siblingDir, fileName.basename()));
766            }
767        } else if (location == SOURCE_OUTPUT) {
768            dir = (getSourceOutDir() != null ? getSourceOutDir() : getClassOutDir());
769        } else {
770            Iterable<? extends File> path = locations.getLocation(location);
771            dir = null;
772            for (File f: path) {
773                dir = f;
774                break;
775            }
776        }
777
778        File file = fileName.getFile(dir); // null-safe
779        return new RegularFileObject(this, file);
780
781    }
782
783    @DefinedBy(Api.COMPILER)
784    public Iterable<? extends JavaFileObject> getJavaFileObjectsFromFiles(
785        Iterable<? extends File> files)
786    {
787        ArrayList<RegularFileObject> result;
788        if (files instanceof Collection<?>)
789            result = new ArrayList<>(((Collection<?>)files).size());
790        else
791            result = new ArrayList<>();
792        for (File f: files)
793            result.add(new RegularFileObject(this, nullCheck(f)));
794        return result;
795    }
796
797    @DefinedBy(Api.COMPILER)
798    public Iterable<? extends JavaFileObject> getJavaFileObjects(File... files) {
799        return getJavaFileObjectsFromFiles(Arrays.asList(nullCheck(files)));
800    }
801
802    @DefinedBy(Api.COMPILER)
803    public void setLocation(Location location,
804                            Iterable<? extends File> path)
805        throws IOException
806    {
807        nullCheck(location);
808        locations.setLocation(location, path);
809    }
810
811    @DefinedBy(Api.COMPILER)
812    public Iterable<? extends File> getLocation(Location location) {
813        nullCheck(location);
814        return locations.getLocation(location);
815    }
816
817    private File getClassOutDir() {
818        return locations.getOutputLocation(CLASS_OUTPUT);
819    }
820
821    private File getSourceOutDir() {
822        return locations.getOutputLocation(SOURCE_OUTPUT);
823    }
824
825    /**
826     * Enforces the specification of a "relative" name as used in
827     * {@linkplain #getFileForInput(Location,String,String)
828     * getFileForInput}.  This method must follow the rules defined in
829     * that method, do not make any changes without consulting the
830     * specification.
831     */
832    protected static boolean isRelativeUri(URI uri) {
833        if (uri.isAbsolute())
834            return false;
835        String path = uri.normalize().getPath();
836        if (path.length() == 0 /* isEmpty() is mustang API */)
837            return false;
838        if (!path.equals(uri.getPath())) // implicitly checks for embedded . and ..
839            return false;
840        if (path.startsWith("/") || path.startsWith("./") || path.startsWith("../"))
841            return false;
842        return true;
843    }
844
845    // Convenience method
846    protected static boolean isRelativeUri(String u) {
847        try {
848            return isRelativeUri(new URI(u));
849        } catch (URISyntaxException e) {
850            return false;
851        }
852    }
853
854    /**
855     * Converts a relative file name to a relative URI.  This is
856     * different from File.toURI as this method does not canonicalize
857     * the file before creating the URI.  Furthermore, no schema is
858     * used.
859     * @param file a relative file name
860     * @return a relative URI
861     * @throws IllegalArgumentException if the file name is not
862     * relative according to the definition given in {@link
863     * javax.tools.JavaFileManager#getFileForInput}
864     */
865    public static String getRelativeName(File file) {
866        if (!file.isAbsolute()) {
867            String result = file.getPath().replace(File.separatorChar, '/');
868            if (isRelativeUri(result))
869                return result;
870        }
871        throw new IllegalArgumentException("Invalid relative path: " + file);
872    }
873
874    /**
875     * Get a detail message from an IOException.
876     * Most, but not all, instances of IOException provide a non-null result
877     * for getLocalizedMessage().  But some instances return null: in these
878     * cases, fallover to getMessage(), and if even that is null, return the
879     * name of the exception itself.
880     * @param e an IOException
881     * @return a string to include in a compiler diagnostic
882     */
883    public static String getMessage(IOException e) {
884        String s = e.getLocalizedMessage();
885        if (s != null)
886            return s;
887        s = e.getMessage();
888        if (s != null)
889            return s;
890        return e.toString();
891    }
892}
893