001/*
002 * Copyright (c) 1998, 2022, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package org.jdrupes.mdoclet.internal.doclets.toolkit.util;
027
028import java.io.BufferedReader;
029import java.io.IOException;
030import java.io.InputStream;
031import java.io.InputStreamReader;
032import java.io.OutputStream;
033import java.io.UnsupportedEncodingException;
034import java.io.Writer;
035import java.nio.file.Path;
036import java.util.MissingResourceException;
037import java.util.regex.Matcher;
038import java.util.regex.Pattern;
039
040import javax.tools.DocumentationTool;
041import javax.tools.FileObject;
042import javax.tools.JavaFileManager.Location;
043
044import org.jdrupes.mdoclet.internal.doclets.toolkit.BaseConfiguration;
045import org.jdrupes.mdoclet.internal.doclets.toolkit.Resources;
046
047import javax.tools.StandardLocation;
048
049/**
050 * Abstraction for handling files, which may be specified directly
051 * (e.g. via a path on the command line) or relative to a Location.
052 */
053public abstract class DocFile {
054
055    /**
056     * The line separator for the current platform.
057     * Use this when writing to external files.
058     */
059    public static final String PLATFORM_LINE_SEPARATOR
060        = System.getProperty("line.separator");
061
062    /** Create a DocFile for a directory. */
063    public static DocFile createFileForDirectory(
064            BaseConfiguration configuration, String file) {
065        return DocFileFactory.getFactory(configuration)
066            .createFileForDirectory(file);
067    }
068
069    /** Create a DocFile for a file that will be opened for reading. */
070    public static DocFile createFileForInput(BaseConfiguration configuration,
071            String file) {
072        return DocFileFactory.getFactory(configuration)
073            .createFileForInput(file);
074    }
075
076    /** Create a DocFile for a file that will be opened for reading. */
077    public static DocFile createFileForInput(BaseConfiguration configuration,
078            Path file) {
079        return DocFileFactory.getFactory(configuration)
080            .createFileForInput(file);
081    }
082
083    /** Create a DocFile for a file that will be opened for writing. */
084    public static DocFile createFileForOutput(BaseConfiguration configuration,
085            DocPath path) {
086        return DocFileFactory.getFactory(configuration)
087            .createFileForOutput(path);
088    }
089
090    /**
091     * The location for this file. Maybe null if the file was created without
092     * a location or path.
093     */
094    protected final Location location;
095
096    /**
097     * The path relative to the (output) location. Maybe null if the file was
098     * created without a location or path.
099     */
100    protected final DocPath path;
101
102    /**
103     * List the directories and files found in subdirectories along the
104     * elements of the given location.
105     * @param configuration the doclet configuration
106     * @param location currently, only {@link StandardLocation#SOURCE_PATH} is supported.
107     * @param path the subdirectory of the directories of the location for which to
108     *  list files
109     */
110    public static Iterable<DocFile> list(BaseConfiguration configuration,
111            Location location, DocPath path) {
112        return DocFileFactory.getFactory(configuration).list(location, path);
113    }
114
115    /** Create a DocFile without a location or path */
116    protected DocFile() {
117        this.location = null;
118        this.path = null;
119    }
120
121    /** Create a DocFile for a given location and relative path. */
122    protected DocFile(Location location, DocPath path) {
123        this.location = location;
124        this.path = path;
125    }
126
127    /**
128     * Returns a file object for the file.
129     * @return a file object
130     */
131    public abstract FileObject getFileObject();
132
133    /**
134     * Open an input stream for the file.
135     *
136     * @return an open input stream for the file
137     * @throws DocFileIOException if there is a problem opening the stream
138     */
139    public abstract InputStream openInputStream() throws DocFileIOException;
140
141    /**
142     * Open an output stream for the file.
143     * The file must have been created with a location of
144     * {@link DocumentationTool.Location#DOCUMENTATION_OUTPUT}
145     * and a corresponding relative path.
146     *
147     * @return an open output stream for the file
148     * @throws DocFileIOException if there is a problem opening the stream
149     * @throws UnsupportedEncodingException if the configured encoding is not supported
150     */
151    public abstract OutputStream openOutputStream()
152            throws DocFileIOException, UnsupportedEncodingException;
153
154    /**
155     * Open an writer for the file, using the encoding (if any) given in the
156     * doclet configuration.
157     * The file must have been created with a location of
158     * {@link DocumentationTool.Location#DOCUMENTATION_OUTPUT} and a corresponding relative path.
159     *
160     * @return an open output stream for the file
161     * @throws DocFileIOException if there is a problem opening the stream
162     * @throws UnsupportedEncodingException if the configured encoding is not supported
163     */
164    public abstract Writer openWriter()
165            throws DocFileIOException, UnsupportedEncodingException;
166
167    /**
168     * Copy the contents of another file directly to this file.
169     *
170     * @param fromFile the file to be copied
171     * @throws DocFileIOException if there is a problem file copying the file
172     */
173    public void copyFile(DocFile fromFile) throws DocFileIOException {
174        try (OutputStream output = openOutputStream()) {
175            try (InputStream input = fromFile.openInputStream()) {
176                byte[] bytearr = new byte[1024];
177                int len;
178                while ((len = read(fromFile, input, bytearr)) != -1) {
179                    write(this, output, bytearr, len);
180                }
181            } catch (IOException e) {
182                throw new DocFileIOException(fromFile,
183                    DocFileIOException.Mode.READ, e);
184            }
185        } catch (IOException e) {
186            throw new DocFileIOException(this, DocFileIOException.Mode.WRITE,
187                e);
188        }
189    }
190
191    /**
192     * Copy the contents of a resource file to this file.
193     *
194     * @param resource the path of the resource, relative to the package of this class
195     * @param overwrite whether or not to overwrite the file if it already exists
196     * @param replaceNewLine if false, the file is copied as a binary file;
197     *     if true, the file is written line by line, using the platform line
198     *     separator
199     *
200     * @throws DocFileIOException if there is a problem while writing the copy
201     * @throws ResourceIOException if there is a problem while reading the resource
202     */
203    public void copyResource(DocPath resource, boolean overwrite,
204            boolean replaceNewLine)
205            throws DocFileIOException, ResourceIOException {
206        if (exists() && !overwrite)
207            return;
208
209        copyResource(resource, replaceNewLine, null);
210    }
211
212    /**
213     * Copy the contents of a resource file to this file.
214     *
215     * @param resource the path of the resource, relative to the package of this class
216     * @param resources if not {@code null}, substitute occurrences of {@code ##REPLACE:key##}
217     *
218     * @throws DocFileIOException if there is a problem while writing the copy
219     * @throws ResourceIOException if there is a problem while reading the resource
220     */
221    public void copyResource(DocPath resource, Resources resources)
222            throws DocFileIOException, ResourceIOException {
223        copyResource(resource, true, resources);
224    }
225
226    private void copyResource(DocPath resource, boolean replaceNewLine,
227            Resources resources)
228            throws DocFileIOException, ResourceIOException {
229        try {
230            InputStream in = BaseConfiguration.class
231                .getResourceAsStream(resource.getPath());
232            if (in == null)
233                return;
234
235            try {
236                if (replaceNewLine) {
237                    try (BufferedReader reader
238                        = new BufferedReader(new InputStreamReader(in))) {
239                        try (Writer writer = openWriter()) {
240                            String line;
241                            while ((line
242                                = readResourceLine(resource, reader)) != null) {
243                                write(this, writer, resources == null ? line
244                                    : localize(line, resources));
245                                write(this, writer, PLATFORM_LINE_SEPARATOR);
246                            }
247                        } catch (IOException e) {
248                            throw new DocFileIOException(this,
249                                DocFileIOException.Mode.WRITE, e);
250                        }
251                    }
252                } else {
253                    try (OutputStream out = openOutputStream()) {
254                        byte[] buf = new byte[2048];
255                        int n;
256                        while ((n = readResource(resource, in, buf)) > 0) {
257                            write(this, out, buf, n);
258                        }
259                    } catch (IOException e) {
260                        throw new DocFileIOException(this,
261                            DocFileIOException.Mode.WRITE, e);
262                    }
263                }
264            } finally {
265                in.close();
266            }
267        } catch (IOException e) {
268            throw new ResourceIOException(resource, e);
269        }
270    }
271
272    private static final Pattern replacePtn
273        = Pattern.compile("##REPLACE:(?<key>[A-Za-z0-9._]+)##");
274
275    private String localize(String line, Resources resources) {
276        Matcher m = replacePtn.matcher(line);
277        StringBuilder sb = null;
278        int start = 0;
279        while (m.find()) {
280            if (sb == null) {
281                sb = new StringBuilder();
282            }
283            sb.append(line, start, m.start());
284            try {
285                sb.append(resources.getText(m.group("key")));
286            } catch (MissingResourceException e) {
287                sb.append(m.group());
288            }
289            start = m.end();
290        }
291        if (sb == null) {
292            return line;
293        } else {
294            sb.append(line.substring(start));
295            return sb.toString();
296        }
297    }
298
299    /** Return true if the file can be read. */
300    public abstract boolean canRead();
301
302    /** Return true if the file can be written. */
303    public abstract boolean canWrite();
304
305    /** Return true if the file exists. */
306    public abstract boolean exists();
307
308    /** Return the base name (last component) of the file name. */
309    public abstract String getName();
310
311    /** Return the file system path for this file. */
312    public abstract String getPath();
313
314    /** Return true if file has an absolute path name. */
315    public abstract boolean isAbsolute();
316
317    /** Return true if file identifies a directory. */
318    public abstract boolean isDirectory();
319
320    /** Return true if file identifies a file. */
321    public abstract boolean isFile();
322
323    /** Return true if this file is the same as another. */
324    public abstract boolean isSameFile(DocFile other);
325
326    /** If the file is a directory, list its contents.
327     *
328     * @return the contents of the directory
329     * @throws DocFileIOException if there is a problem while listing the directory
330     */
331    public abstract Iterable<DocFile> list() throws DocFileIOException;
332
333    /** Create the file as a directory, including any parent directories. */
334    public abstract boolean mkdirs();
335
336    /**
337     * Derive a new file by resolving a relative path against this file.
338     * The new file will inherit the configuration and location of this file
339     * If this file has a path set, the new file will have a corresponding
340     * new path.
341     */
342    public abstract DocFile resolve(DocPath p);
343
344    /**
345     * Derive a new file by resolving a relative path against this file.
346     * The new file will inherit the configuration and location of this file
347     * If this file has a path set, the new file will have a corresponding
348     * new path.
349     */
350    public abstract DocFile resolve(String p);
351
352    /**
353     * Resolve a relative file against the given output location.
354     * @param locn Currently, only
355     * {@link DocumentationTool.Location#DOCUMENTATION_OUTPUT} is supported.
356     */
357    public abstract DocFile resolveAgainst(Location locn);
358
359    /**
360     * Reads from an input stream opened from a given file into a given buffer.
361     * If an {@code IOException} occurs, it is wrapped in a {@code DocFileIOException}.
362     *
363     * @param inFile the file for the stream
364     * @param input  the stream
365     * @param buf    the buffer
366     *
367     * @return the number of bytes read, or -1 if at end of file
368     * @throws DocFileIOException if an exception occurred while reading the stream
369     */
370    private static int read(DocFile inFile, InputStream input, byte[] buf)
371            throws DocFileIOException {
372        try {
373            return input.read(buf);
374        } catch (IOException e) {
375            throw new DocFileIOException(inFile, DocFileIOException.Mode.READ,
376                e);
377        }
378    }
379
380    /**
381     * Writes to an output stream for a given file from a given buffer.
382     * If an {@code IOException} occurs, it is wrapped in a {@code DocFileIOException}.
383     *
384     * @param outFile the file for the stream
385     * @param out     the stream
386     * @param buf     the buffer
387     *
388     * @throws DocFileIOException if an exception occurred while writing the stream
389     */
390    private static void write(DocFile outFile, OutputStream out, byte[] buf,
391            int len) throws DocFileIOException {
392        try {
393            out.write(buf, 0, len);
394        } catch (IOException e) {
395            throw new DocFileIOException(outFile, DocFileIOException.Mode.WRITE,
396                e);
397        }
398    }
399
400    /**
401     * Writes text to an output stream for a given file from a given buffer.
402     * If an {@code IOException} occurs, it is wrapped in a {@code DocFileIOException}.
403     *
404     * @param outFile the file for the stream
405     * @param out     the stream
406     * @param text    the text to be written
407     *
408     * @throws DocFileIOException if an exception occurred while writing the stream
409     */
410    private static void write(DocFile outFile, Writer out, String text)
411            throws DocFileIOException {
412        try {
413            out.write(text);
414        } catch (IOException e) {
415            throw new DocFileIOException(outFile, DocFileIOException.Mode.WRITE,
416                e);
417        }
418    }
419
420    /**
421     * Reads from an input stream opened from a given resource into a given buffer.
422     * If an {@code IOException} occurs, it is wrapped in a {@code ResourceIOException}.
423     *
424     * @param docPath the resource for the stream
425     * @param in      the stream
426     * @param buf     the buffer
427     *
428     * @return the number of bytes read, or -1 if at end of file
429     * @throws ResourceIOException if an exception occurred while reading the stream
430     */
431    private static int readResource(DocPath docPath, InputStream in, byte[] buf)
432            throws ResourceIOException {
433        try {
434            return in.read(buf);
435        } catch (IOException e) {
436            throw new ResourceIOException(docPath, e);
437        }
438    }
439
440    /**
441     * Reads a line of characters from an input stream opened from a given resource.
442     * If an {@code IOException} occurs, it is wrapped in a {@code ResourceIOException}.
443     *
444     * @param docPath the resource for the stream
445     * @param in      the stream
446     *
447     * @return the line of text, or {@code null} if at end of stream
448     * @throws ResourceIOException if an exception occurred while reading the stream
449     */
450    private static String readResourceLine(DocPath docPath, BufferedReader in)
451            throws ResourceIOException {
452        try {
453            return in.readLine();
454        } catch (IOException e) {
455            throw new ResourceIOException(docPath, e);
456        }
457    }
458}