001/*
002 * Copyright (c) 2017, 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.formats.html;
027
028import com.sun.source.doctree.DocTree;
029import com.sun.source.doctree.EndElementTree;
030import com.sun.source.doctree.StartElementTree;
031import com.sun.source.util.DocTreeFactory;
032
033import javax.lang.model.element.Element;
034import javax.lang.model.element.ModuleElement;
035import javax.lang.model.element.PackageElement;
036import javax.tools.FileObject;
037import javax.tools.JavaFileManager.Location;
038
039import org.jdrupes.mdoclet.internal.doclets.formats.html.Navigation.PageMode;
040import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.BodyContents;
041import org.jdrupes.mdoclet.internal.doclets.formats.html.markup.HtmlTree;
042import org.jdrupes.mdoclet.internal.doclets.toolkit.Content;
043import org.jdrupes.mdoclet.internal.doclets.toolkit.DocFileElement;
044import org.jdrupes.mdoclet.internal.doclets.toolkit.DocFilesHandler;
045import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocFile;
046import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocFileIOException;
047import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocPath;
048import org.jdrupes.mdoclet.internal.doclets.toolkit.util.DocPaths;
049import org.jdrupes.mdoclet.internal.doclets.toolkit.util.Utils;
050import org.jdrupes.mdoclet.internal.doclint.HtmlTag;
051
052import java.net.URI;
053import java.net.URISyntaxException;
054import java.util.ArrayList;
055import java.util.List;
056
057public class DocFilesHandlerImpl implements DocFilesHandler {
058
059    public final Element element;
060    public final Location location;
061    public final DocPath source;
062    public final HtmlConfiguration configuration;
063    private final HtmlOptions options;
064
065    /**
066     * Constructor to construct the DocFilesWriter object.
067     *
068     * @param configuration the configuration of this doclet.
069     * @param element the containing element of the doc-files.
070     *
071     */
072    public DocFilesHandlerImpl(HtmlConfiguration configuration,
073            Element element) {
074        this.configuration = configuration;
075        this.options = configuration.getOptions();
076        this.element = element;
077
078        switch (element.getKind()) {
079        case MODULE -> {
080            ModuleElement mdle = (ModuleElement) element;
081            location = configuration.utils.getLocationForModule(mdle);
082            source = DocPaths.DOC_FILES;
083        }
084
085        case PACKAGE -> {
086            PackageElement pkg = (PackageElement) element;
087            location = configuration.utils.getLocationForPackage(pkg);
088            // Note, given that we have a module-specific location,
089            // we want a module-relative path for the source, and not the
090            // standard path that may include the module directory
091            source = DocPath
092                .create(pkg.getQualifiedName().toString().replace('.', '/'))
093                .resolve(DocPaths.DOC_FILES);
094        }
095
096        default -> throw new AssertionError("unsupported element " + element);
097        }
098    }
099
100    /**
101     * Copy doc-files directory and its contents from the source
102     * elements directory to the generated documentation directory.
103     *
104     * @throws DocFileIOException if there is a problem while copying
105     *         the documentation files
106     */
107    @Override
108    public void copyDocFiles() throws DocFileIOException {
109        boolean first = true;
110        for (DocFile srcdir : DocFile.list(configuration, location, source)) {
111            if (!srcdir.isDirectory()) {
112                continue;
113            }
114            DocPath path = switch (this.element.getKind()) {
115            case MODULE -> DocPaths.forModule((ModuleElement) this.element);
116            case PACKAGE -> configuration.docPaths
117                .forPackage((PackageElement) this.element);
118            default -> throw new AssertionError(
119                "unknown kind:" + this.element.getKind());
120            };
121            copyDirectory(srcdir, path.resolve(DocPaths.DOC_FILES), first);
122            first = false;
123        }
124    }
125
126    @Override
127    public List<DocPath> getStylesheets() throws DocFileIOException {
128        var stylesheets = new ArrayList<DocPath>();
129        for (DocFile srcdir : DocFile.list(configuration, location, source)) {
130            for (DocFile srcFile : srcdir.list()) {
131                if (srcFile.getName().endsWith(".css"))
132                    stylesheets
133                        .add(DocPaths.DOC_FILES.resolve(srcFile.getName()));
134            }
135        }
136        return stylesheets;
137    }
138
139    private void copyDirectory(DocFile srcdir, final DocPath dstDocPath,
140            boolean first) throws DocFileIOException {
141        DocFile dstdir = DocFile.createFileForOutput(configuration, dstDocPath);
142        if (srcdir.isSameFile(dstdir)) {
143            return;
144        }
145        for (DocFile srcfile : srcdir.list()) {
146            // ensure that the name is a valid component in an eventual full
147            // path
148            // and so avoid an equivalent check lower down in the file manager
149            // that throws IllegalArgumentException
150            if (!isValidFilename(srcfile)) {
151                configuration.messages.warning("doclet.Copy_Ignored_warning",
152                    srcfile.getPath());
153                continue;
154            }
155
156            DocFile destfile = dstdir.resolve(srcfile.getName());
157            if (srcfile.isFile()) {
158                if (destfile.exists() && !first) {
159                    configuration.messages.warning(
160                        "doclet.Copy_Overwrite_warning",
161                        srcfile.getPath(), dstdir.getPath());
162                } else {
163                    if (Utils.toLowerCase(srcfile.getPath())
164                        .endsWith(".html")) {
165                        handleHtmlFile(srcfile, dstDocPath);
166                    } else {
167                        configuration.messages.notice(
168                            "doclet.Copying_File_0_To_Dir_1",
169                            srcfile.getPath(), dstdir.getPath());
170                        destfile.copyFile(srcfile);
171                    }
172                }
173            } else if (srcfile.isDirectory()) {
174                if (options.copyDocfileSubdirs()
175                    && !configuration
176                        .shouldExcludeDocFileDir(srcfile.getName())) {
177                    DocPath dirDocPath = dstDocPath.resolve(srcfile.getName());
178                    copyDirectory(srcfile, dirDocPath, first);
179                }
180            }
181        }
182    }
183
184    private boolean isValidFilename(DocFile f) {
185        try {
186            String n = f.getName();
187            URI u = new URI(n);
188            return u.getPath().equals(n);
189        } catch (URISyntaxException e) {
190            return false;
191        }
192    }
193
194    private void handleHtmlFile(DocFile srcfile, DocPath dstPath)
195            throws DocFileIOException {
196        Utils utils = configuration.utils;
197        FileObject fileObject = srcfile.getFileObject();
198        DocFileElement dfElement
199            = new DocFileElement(utils, element, fileObject);
200
201        DocPath dfilePath = dstPath.resolve(srcfile.getName());
202        PackageElement pkg = dfElement.getPackageElement();
203
204        HtmlDocletWriter docletWriter
205            = new DocFileWriter(configuration, dfilePath, element, pkg);
206
207        List<? extends DocTree> localTags
208            = getLocalHeaderTags(utils.getPreamble(dfElement));
209        Content localTagsContent
210            = docletWriter.commentTagsToContent(dfElement, localTags, false);
211
212        String title = getWindowTitle(docletWriter, dfElement).trim();
213        HtmlTree htmlContent = docletWriter.getBody(title);
214
215        List<? extends DocTree> fullBody = utils.getFullBody(dfElement);
216        Content pageContent
217            = docletWriter.commentTagsToContent(dfElement, fullBody, false);
218        docletWriter.addTagsInfo(dfElement, pageContent);
219
220        htmlContent.add(new BodyContents()
221            .setHeader(docletWriter.getHeader(PageMode.DOC_FILE, element))
222            .addMainContent(pageContent)
223            .setFooter(docletWriter.getFooter()));
224        docletWriter.printHtmlDocument(List.of(), null, localTagsContent,
225            List.of(), htmlContent);
226    }
227
228    private List<? extends DocTree>
229            getLocalHeaderTags(List<? extends DocTree> dtrees) {
230        List<DocTree> localTags = new ArrayList<>();
231        DocTreeFactory docTreeFactory
232            = configuration.docEnv.getDocTrees().getDocTreeFactory();
233        boolean inHead = false;
234        boolean inTitle = false;
235        loop: for (DocTree dt : dtrees) {
236            switch (dt.getKind()) {
237            case START_ELEMENT:
238                StartElementTree startElem = (StartElementTree) dt;
239                switch (HtmlTag.get(startElem.getName())) {
240                case HEAD:
241                    inHead = true;
242                    break;
243                case META:
244                    break;
245                case TITLE:
246                    inTitle = true;
247                    break;
248                default:
249                    if (inHead) {
250                        localTags.add(startElem);
251                        localTags.add(docTreeFactory.newTextTree("\n"));
252                    }
253                }
254                break;
255            case END_ELEMENT:
256                EndElementTree endElem = (EndElementTree) dt;
257                switch (HtmlTag.get(endElem.getName())) {
258                case HEAD:
259                    inHead = false;
260                    break loop;
261                case TITLE:
262                    inTitle = false;
263                    break;
264                default:
265                    if (inHead) {
266                        localTags.add(endElem);
267                        localTags.add(docTreeFactory.newTextTree("\n"));
268                    }
269                }
270                break;
271            case ENTITY:
272            case TEXT:
273                if (inHead && !inTitle) {
274                    localTags.add(dt);
275                }
276                break;
277            }
278        }
279        return localTags;
280    }
281
282    private String getWindowTitle(HtmlDocletWriter docletWriter,
283            Element element) {
284        String t = configuration.utils.getHTMLTitle(element);
285        return docletWriter.getWindowTitle(t);
286    }
287
288    private static class DocFileWriter extends HtmlDocletWriter {
289        private final PackageElement pkg;
290
291        /**
292         * Constructor to construct the HtmlDocletWriter object.
293         *
294         * @param configuration the configuration of this doclet
295         * @param path          the file to be generated
296         * @param e             the anchoring element
297         * @param pkg           the package containing the doc file
298         */
299        public DocFileWriter(HtmlConfiguration configuration, DocPath path,
300                Element e, PackageElement pkg) {
301            super(configuration, path);
302            switch (e.getKind()) {
303            case PACKAGE:
304            case MODULE:
305                break;
306            default:
307                throw new AssertionError("unsupported element: " + e.getKind());
308            }
309            this.pkg = pkg;
310        }
311
312        @Override
313        protected Navigation getNavBar(PageMode pageMode, Element element) {
314            Content mdleLinkContent
315                = getModuleLink(utils.elementUtils.getModuleOf(element),
316                    contents.moduleLabel);
317            Content pkgLinkContent = getPackageLink(pkg, contents.packageLabel);
318            return super.getNavBar(pageMode, element)
319                .setNavLinkModule(mdleLinkContent)
320                .setNavLinkPackage(pkgLinkContent);
321        }
322    }
323}