1 /*
2  * Copyright (c) 1998, 2020, 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 
26 package jdk.javadoc.internal.doclets.toolkit.util;
27 
28 import java.io.BufferedInputStream;
29 import java.io.BufferedOutputStream;
30 import java.io.BufferedWriter;
31 import java.io.IOException;
32 import java.io.InputStream;
33 import java.io.OutputStream;
34 import java.io.OutputStreamWriter;
35 import java.io.UnsupportedEncodingException;
36 import java.io.Writer;
37 import java.nio.file.DirectoryStream;
38 import java.nio.file.Files;
39 import java.nio.file.Path;
40 import java.nio.file.Paths;
41 import java.util.ArrayList;
42 import java.util.Arrays;
43 import java.util.LinkedHashSet;
44 import java.util.List;
45 import java.util.Objects;
46 import java.util.Set;
47 
48 import javax.tools.DocumentationTool;
49 import javax.tools.FileObject;
50 import javax.tools.JavaFileManager.Location;
51 import javax.tools.JavaFileObject;
52 import javax.tools.StandardJavaFileManager;
53 import javax.tools.StandardLocation;
54 
55 import com.sun.tools.javac.util.Assert;
56 import jdk.javadoc.internal.doclets.toolkit.BaseConfiguration;
57 
58 /**
59  * Implementation of DocFileFactory using a {@link StandardJavaFileManager}.
60  *
61  *  <p><b>This is NOT part of any supported API.
62  *  If you write code that depends on this, you do so at your own risk.
63  *  This code and its internal interfaces are subject to change or
64  *  deletion without notice.</b>
65  *
66  */
67 class StandardDocFileFactory extends DocFileFactory {
68     private final StandardJavaFileManager fileManager;
69     private Path destDir;
70 
StandardDocFileFactory(BaseConfiguration configuration)71     public StandardDocFileFactory(BaseConfiguration configuration) {
72         super(configuration);
73         fileManager = (StandardJavaFileManager) configuration.getFileManager();
74     }
75 
76     @Override
setDestDir(String destDirName)77     public void setDestDir(String destDirName) throws SimpleDocletException {
78         if (destDir != null)
79             throw new AssertionError("destDir already initialized: " + destDir);
80 
81         if (!destDirName.isEmpty()
82                 || !fileManager.hasLocation(DocumentationTool.Location.DOCUMENTATION_OUTPUT)) {
83             try {
84                 String dirName = destDirName.isEmpty() ? "." : destDirName;
85                 Path dir = Paths.get(dirName);
86                 fileManager.setLocationFromPaths(DocumentationTool.Location.DOCUMENTATION_OUTPUT, Arrays.asList(dir));
87             } catch (IOException e) {
88                 // generic IOException from file manager, setting location, e.g. file not a directory
89                 String message = configuration.getDocResources().getText("doclet.error.initializing.dest.dir", e);
90                 throw new SimpleDocletException(message, e);
91             }
92         }
93 
94         destDir = fileManager.getLocationAsPaths(DocumentationTool.Location.DOCUMENTATION_OUTPUT).iterator().next();
95     }
96 
getDestDir()97     private Path getDestDir() {
98         Objects.requireNonNull(destDir, "destDir not initialized");
99         return destDir;
100     }
101 
102     @Override
createFileForDirectory(String file)103     public DocFile createFileForDirectory(String file) {
104         return new StandardDocFile(Paths.get(file));
105     }
106 
107     @Override
createFileForInput(String file)108     public DocFile createFileForInput(String file) {
109         return new StandardDocFile(Paths.get(file));
110     }
111 
112     @Override
createFileForOutput(DocPath path)113     public DocFile createFileForOutput(DocPath path) {
114         return new StandardDocFile(DocumentationTool.Location.DOCUMENTATION_OUTPUT, path);
115     }
116 
117     @Override
list(Location location, DocPath path)118     Iterable<DocFile> list(Location location, DocPath path) {
119         Location l = ((location == StandardLocation.SOURCE_PATH)
120                 && !fileManager.hasLocation(StandardLocation.SOURCE_PATH))
121                 ? StandardLocation.CLASS_PATH
122                 : location;
123 
124         Set<DocFile> files = new LinkedHashSet<>();
125         for (Path f: fileManager.getLocationAsPaths(l)) {
126             if (Files.isDirectory(f)) {
127                 f = f.resolve(path.getPath());
128                 if (Files.exists(f))
129                     files.add(new StandardDocFile(f));
130             }
131         }
132         return files;
133     }
134 
newFile(Path dir, String path)135     private static Path newFile(Path dir, String path) {
136         return (dir == null) ? Paths.get(path) : dir.resolve(path);
137     }
138 
139     class StandardDocFile extends DocFile {
140         private final Path file;
141 
142         /** Create a StandardDocFile for a given file. */
StandardDocFile(Path file)143         private StandardDocFile(Path file) {
144             this.file = file;
145         }
146 
147         /** Create a StandardDocFile for a given location and relative path. */
StandardDocFile(Location location, DocPath path)148         private StandardDocFile(Location location, DocPath path) {
149             super(location, path);
150             Assert.check(location == DocumentationTool.Location.DOCUMENTATION_OUTPUT);
151             this.file = newFile(getDestDir(), path.getPath());
152         }
153 
154         @Override
getFileObject()155         public FileObject getFileObject()  {
156             return getJavaFileObjectForInput(file);
157         }
158 
159         /**
160          * Open an input stream for the file.
161          *
162          * @throws DocFileIOException if there is a problem while opening stream
163          */
164         @Override
openInputStream()165         public InputStream openInputStream() throws DocFileIOException {
166             try {
167                 JavaFileObject fo = getJavaFileObjectForInput(file);
168                 return new BufferedInputStream(fo.openInputStream());
169             } catch (IOException e) {
170                 throw new DocFileIOException(this, DocFileIOException.Mode.READ, e);
171             }
172         }
173 
174         /**
175          * Open an output stream for the file.
176          * The file must have been created with a location of
177          * {@link DocumentationTool.Location#DOCUMENTATION_OUTPUT} and a corresponding relative path.
178          *
179          * @throws DocFileIOException if there is a problem while opening stream
180          */
181         @Override
openOutputStream()182         public OutputStream openOutputStream() throws DocFileIOException {
183             if (location != DocumentationTool.Location.DOCUMENTATION_OUTPUT)
184                 throw new IllegalStateException();
185 
186             try {
187                 OutputStream out = getFileObjectForOutput(path).openOutputStream();
188                 return new BufferedOutputStream(out);
189             } catch (IOException e) {
190                 throw new DocFileIOException(this, DocFileIOException.Mode.WRITE, e);
191             }
192         }
193 
194         /**
195          * Open an writer for the file, using the encoding (if any) given in the
196          * doclet configuration.
197          * The file must have been created with a location of
198          * {@link DocumentationTool.Location#DOCUMENTATION_OUTPUT} and a corresponding relative path.
199          *
200          * @throws DocFileIOException if there is a problem while opening stream
201          * @throws UnsupportedEncodingException if the configured encoding is not supported
202          */
203         @Override
openWriter()204         public Writer openWriter() throws DocFileIOException, UnsupportedEncodingException {
205             if (location != DocumentationTool.Location.DOCUMENTATION_OUTPUT)
206                 throw new IllegalStateException();
207 
208             try {
209                 OutputStream out = getFileObjectForOutput(path).openOutputStream();
210                 String docencoding = configuration.getOptions().docEncoding();
211                 return new BufferedWriter(new OutputStreamWriter(out, docencoding));
212             } catch (IOException e) {
213                 throw new DocFileIOException(this, DocFileIOException.Mode.WRITE, e);
214             }
215         }
216 
217         /** Return true if the file can be read. */
218         @Override
canRead()219         public boolean canRead() {
220             return Files.isReadable(file);
221         }
222 
223         /** Return true if the file can be written. */
224         @Override
canWrite()225         public boolean canWrite() {
226             return Files.isWritable(file);
227         }
228 
229         /** Return true if the file exists. */
230         @Override
exists()231         public boolean exists() {
232             return Files.exists(file);
233         }
234 
235         /** Return the base name (last component) of the file name. */
236         @Override
getName()237         public String getName() {
238             return file.getFileName().toString();
239         }
240 
241         /** Return the file system path for this file. */
242         @Override
getPath()243         public String getPath() {
244             return file.toString();
245         }
246 
247         /** Return true is file has an absolute path name. */
248         @Override
isAbsolute()249         public boolean isAbsolute() {
250             return file.isAbsolute();
251         }
252 
253         /** Return true is file identifies a directory. */
254         @Override
isDirectory()255         public boolean isDirectory() {
256             return Files.isDirectory(file);
257         }
258 
259         /** Return true is file identifies a file. */
260         @Override
isFile()261         public boolean isFile() {
262             return Files.isRegularFile(file);
263         }
264 
265         /** Return true if this file is the same as another. */
266         @Override
isSameFile(DocFile other)267         public boolean isSameFile(DocFile other) {
268             if (!(other instanceof StandardDocFile))
269                 return false;
270 
271             try {
272                 return Files.isSameFile(file, ((StandardDocFile) other).file);
273             } catch (IOException e) {
274                 return false;
275             }
276         }
277 
278         /** If the file is a directory, list its contents. */
279         @Override
list()280         public Iterable<DocFile> list() throws DocFileIOException {
281             List<DocFile> files = new ArrayList<>();
282             try (DirectoryStream<Path> ds = Files.newDirectoryStream(file)) {
283                 for (Path f: ds) {
284                     files.add(new StandardDocFile(f));
285                 }
286             } catch (IOException e) {
287                 throw new DocFileIOException(this, DocFileIOException.Mode.READ, e);
288             }
289             return files;
290         }
291 
292         /** Create the file as a directory, including any parent directories. */
293         @Override
mkdirs()294         public boolean mkdirs() {
295             try {
296                 Files.createDirectories(file);
297                 return true;
298             } catch (IOException e) {
299                 return false;
300             }
301         }
302 
303         /**
304          * Derive a new file by resolving a relative path against this file.
305          * The new file will inherit the configuration and location of this file
306          * If this file has a path set, the new file will have a corresponding
307          * new path.
308          */
309         @Override
resolve(DocPath p)310         public DocFile resolve(DocPath p) {
311             return resolve(p.getPath());
312         }
313 
314         /**
315          * Derive a new file by resolving a relative path against this file.
316          * The new file will inherit the configuration and location of this file
317          * If this file has a path set, the new file will have a corresponding
318          * new path.
319          */
320         @Override
resolve(String p)321         public DocFile resolve(String p) {
322             if (location == null && path == null) {
323                 return new StandardDocFile(file.resolve(p));
324             } else {
325                 return new StandardDocFile(location, path.resolve(p));
326             }
327         }
328 
329         /**
330          * Resolve a relative file against the given output location.
331          * @param locn Currently, only
332          * {@link DocumentationTool.Location#DOCUMENTATION_OUTPUT} is supported.
333          */
334         @Override
resolveAgainst(Location locn)335         public DocFile resolveAgainst(Location locn) {
336             if (locn != DocumentationTool.Location.DOCUMENTATION_OUTPUT)
337                 throw new IllegalArgumentException();
338             return new StandardDocFile(getDestDir().resolve(file));
339         }
340 
341         /** Return a string to identify the contents of this object,
342          * for debugging purposes.
343          */
344         @Override
toString()345         public String toString() {
346             StringBuilder sb = new StringBuilder();
347             sb.append("StandardDocFile[");
348             if (location != null)
349                 sb.append("locn:").append(location).append(",");
350             if (path != null)
351                 sb.append("path:").append(path.getPath()).append(",");
352             sb.append("file:").append(file);
353             sb.append("]");
354             return sb.toString();
355         }
356 
getJavaFileObjectForInput(Path file)357         private JavaFileObject getJavaFileObjectForInput(Path file) {
358             return fileManager.getJavaFileObjects(file).iterator().next();
359         }
360 
getFileObjectForOutput(DocPath path)361         private FileObject getFileObjectForOutput(DocPath path) throws IOException {
362             // break the path into a package-part and the rest, by finding
363             // the position of the last '/' before an invalid character for a
364             // package name, such as the "." before an extension or the "-"
365             // in filenames like package-summary.html, doc-files or src-html.
366             String p = path.getPath();
367             int lastSep = -1;
368             for (int i = 0; i < p.length(); i++) {
369                 char ch = p.charAt(i);
370                 if (ch == '/') {
371                     lastSep = i;
372                 } else if (i == lastSep + 1 && !Character.isJavaIdentifierStart(ch)
373                         || !Character.isJavaIdentifierPart(ch)) {
374                     break;
375                 }
376             }
377             String pkg = (lastSep == -1) ? "" : p.substring(0, lastSep);
378             String rest = p.substring(lastSep + 1);
379             return fileManager.getFileForOutput(location, pkg, rest, null);
380         }
381     }
382 }
383