001/*
002 * Copyright (c) 1998, 2023, 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.net.HttpURLConnection;
033import java.net.MalformedURLException;
034import java.net.URISyntaxException;
035import java.net.URL;
036import java.net.URLConnection;
037import java.util.HashMap;
038import java.util.Map;
039import java.util.Properties;
040import java.util.TreeMap;
041
042import javax.lang.model.SourceVersion;
043import javax.lang.model.element.Element;
044import javax.lang.model.element.ModuleElement;
045import javax.lang.model.element.PackageElement;
046import javax.tools.Diagnostic;
047import javax.tools.Diagnostic.Kind;
048
049import org.jdrupes.mdoclet.internal.doclets.toolkit.AbstractDoclet;
050import org.jdrupes.mdoclet.internal.doclets.toolkit.BaseConfiguration;
051import org.jdrupes.mdoclet.internal.doclets.toolkit.Resources;
052
053import javax.tools.DocumentationTool;
054
055import jdk.javadoc.doclet.Reporter;
056
057/**
058 * Process and manage "-link" and "-linkoffline" to external packages. The
059 * options "-link" and "-linkoffline" both depend on the fact that Javadoc now
060 * generates "package-list"(lists all the packages which are getting
061 * documented) file in the current or the destination directory, while
062 * generating the documentation.
063 */
064public class Extern {
065
066    /**
067     * Map element names onto Extern Item objects.
068     * Lazily initialized.
069     */
070    private Map<String, Item> moduleItems = new HashMap<>();
071    private Map<String, Map<String, Item>> packageItems = new HashMap<>();
072
073    /**
074     * The global configuration information for this run.
075     */
076    private final BaseConfiguration configuration;
077
078    private final Resources resources;
079
080    private final Utils utils;
081
082    /**
083     * True if we are using -linkoffline and false if -link is used instead.
084     */
085    private boolean linkoffline = false;
086
087    /**
088     * Stores the info for one external doc set
089     */
090    private static class Item {
091
092        /**
093         * Element name, found in the "element-list" file in the {@link #path}.
094         */
095        final String elementName;
096
097        /**
098         * The URL or the directory path at which the element documentation will be
099         * available.
100         */
101        final DocPath path;
102
103        /**
104         * If given path is directory path then true else if it is a URL then false.
105         */
106        final boolean relative;
107
108        /**
109         * Indicates that docs use old-form of anchors.
110         */
111        final boolean useOldFormId;
112
113        /**
114         * Constructor to build a Extern Item object and map it with the element name.
115         * If the same element name is found in the map, then the first mapped
116         * Item object or offline location will be retained.
117         *
118         * @param elementName Element name found in the "element-list" file.
119         * @param path        URL or Directory path from where the "element-list"
120         * file is picked.
121         * @param relative    True if path is URL, false if directory path.
122         */
123        Item(String elementName, DocPath path, boolean relative,
124                boolean useOldFormId) {
125            this.elementName = elementName;
126            this.path = path;
127            this.relative = relative;
128            this.useOldFormId = useOldFormId;
129        }
130
131        /**
132         * String representation of "this" with elementname and the path.
133         */
134        @Override
135        public String toString() {
136            return elementName + (relative ? " -> " : " => ") + path.getPath();
137        }
138    }
139
140    public Extern(BaseConfiguration configuration) {
141        this.configuration = configuration;
142        this.resources = configuration.getDocResources();
143        this.utils = configuration.utils;
144    }
145
146    /**
147     * Determine if a element item is externally documented.
148     *
149     * @param element an Element.
150     * @return true if the element is externally documented
151     */
152    public boolean isExternal(Element element) {
153        if (packageItems.isEmpty()) {
154            return false;
155        }
156        PackageElement pe = utils.containingPackage(element);
157        if (pe.isUnnamed()) {
158            return false;
159        }
160
161        return findElementItem(pe) != null;
162    }
163
164    /**
165     * Determine if a element item is a module or not.
166     *
167     * @param elementName name of the element.
168     * @return true if the element is a module
169     */
170    public boolean isModule(String elementName) {
171        Item elem = moduleItems.get(elementName);
172        return elem != null;
173    }
174
175    /**
176     * Convert a link to be an external link if appropriate.
177     *
178     * @param element The element .
179     * @param relativepath    The relative path.
180     * @param filename    The link to convert.
181     * @return if external return converted link else return null
182     */
183    public DocLink getExternalLink(Element element, DocPath relativepath,
184            String filename) {
185        return getExternalLink(element, relativepath, filename, null);
186    }
187
188    public DocLink getExternalLink(Element element, DocPath relativepath,
189            String filename,
190            String memberName) {
191        Item fnd = findElementItem(element);
192        if (fnd == null)
193            return null;
194
195        // The following is somewhat questionable since we are using DocPath
196        // to contain external URLs!
197        DocPath p
198            = fnd.relative ? relativepath.resolve(fnd.path).resolve(filename)
199                : fnd.path.resolve(filename);
200        return new DocLink(p,
201            fnd.useOldFormId ? getOldFormHtmlName(memberName) : memberName);
202    }
203
204    /**
205     * Build the extern element list from given URL or the directory path,
206     * as specified with the "-link" flag.
207     * Flag error if the "-link" or "-linkoffline" option is already used.
208     *
209     * @param url        URL or Directory path.
210     * @param reporter   The <code>DocErrorReporter</code> used to report errors.
211     * @return true if successful, false otherwise
212     * @throws DocFileIOException if there is a problem reading a element list file
213     */
214    public boolean link(String url, Reporter reporter)
215            throws DocFileIOException {
216        return link(url, url, reporter, false);
217    }
218
219    /**
220     * Build the extern element list from given URL or the directory path,
221     * as specified with the "-linkoffline" flag.
222     * Flag error if the "-link" or "-linkoffline" option is already used.
223     *
224     * @param url        URL or Directory path.
225     * @param elemlisturl This can be another URL for "element-list" or ordinary
226     *                   file.
227     * @param reporter   The <code>DocErrorReporter</code> used to report errors.
228     * @return true if successful, false otherwise
229     * @throws DocFileIOException if there is a problem reading the element list file
230     */
231    public boolean link(String url, String elemlisturl, Reporter reporter)
232            throws DocFileIOException {
233        return link(url, elemlisturl, reporter, true);
234    }
235
236    /**
237     * Check whether links to platform documentation are configured. If not then configure
238     * links using the documentation URL defined in {@code linkPlatformProperties} or the
239     * default documentation URL if that parameter is {@code null}.
240     *
241     * @param linkPlatformProperties path or URL to properties file containing
242     *                               platform documentation URLs, or null
243     * @param reporter the {@code DocErrorReporter} used to report errors
244     */
245    public void checkPlatformLinks(String linkPlatformProperties,
246            Reporter reporter) {
247        PackageElement javaLang
248            = utils.elementUtils.getPackageElement("java.lang");
249        if (utils.isIncluded(javaLang)) {
250            return;
251        }
252        DocLink link = getExternalLink(javaLang, DocPath.empty,
253            DocPaths.PACKAGE_SUMMARY.getPath());
254        if (link != null) {
255            // Links to platform docs are already configure, nothing to do here.
256            return;
257        }
258        try {
259            int versionNumber = getSourceVersionNumber();
260            String docUrl;
261
262            if (linkPlatformProperties != null) {
263                docUrl = getCustomPlatformDocs(versionNumber,
264                    linkPlatformProperties);
265            } else {
266                docUrl = getDefaultPlatformDocs(versionNumber);
267            }
268            if (docUrl == null) {
269                return;
270            }
271            DocPath elementListPath = getPlatformElementList(versionNumber);
272            URL elementListUrl
273                = jdk.javadoc.internal.doclets.toolkit.AbstractDoclet.class
274                    .getResource(elementListPath.getPath());
275            if (elementListUrl == null) {
276                reporter.print(Kind.WARNING, resources.getText(
277                    "doclet.Resource_error", elementListPath.getPath()));
278            } else {
279                try (InputStream in = open(elementListUrl)) {
280                    readElementList(in, docUrl, false, versionNumber,
281                        isOldFormPlatformDocs(versionNumber));
282                } catch (IOException exc) {
283                    throw new Fault(resources.getText(
284                        "doclet.Resource_error", elementListPath.getPath()),
285                        exc);
286                }
287            }
288        } catch (Fault f) {
289            reporter.print(Kind.ERROR, f.getMessage());
290        }
291    }
292
293    /**
294     * Checks if platform docs for the specified version use old-form anchors.
295     * Old-form anchors are used by Oracle docs for JDKs 8 and 9.
296     * It can be checked on https://docs.oracle.com/javase/<version>/docs/api
297     *
298     * @param version
299     * @return True if docs use old-form anchors
300     */
301    private boolean isOldFormPlatformDocs(int version) {
302        return 8 == version || 9 == version;
303    }
304
305    /**
306     * Return the resource path for the package or element list for the given {@code version}.
307     * @param version the platform version number
308     * @return the resource path
309     */
310    private DocPath getPlatformElementList(int version) {
311        String filename = version <= 8
312            ? "package-list-" + version + ".txt"
313            : "element-list-" + version + ".txt";
314        return DocPaths.RESOURCES.resolve("releases").resolve(filename);
315    }
316
317    /**
318     * Return the default URL for the platform API documentation for the given {@code version}.
319     * @param version the platform version number
320     * @return the URL as String
321     */
322    private String getDefaultPlatformDocs(int version) {
323        Resources resources = configuration.getDocResources();
324        return version <= 10
325            ? resources.getText("doclet.platform.docs.old", version)
326            : isPrerelease(version)
327                ? resources.getText("doclet.platform.docs.ea", version)
328                : resources.getText("doclet.platform.docs.new", version);
329    }
330
331    /**
332     * Retrieve and return the custom URL for the platform API documentation for the given
333     * {@code version} from the properties file at {@code linkPlatformProperties}.
334     * @param version the platform version number
335     * @param linkPlatformProperties path pointing to a properties file
336     * @return the custom URL as String
337     */
338    private String getCustomPlatformDocs(int version,
339            String linkPlatformProperties) throws Fault {
340        String url;
341        try {
342            Properties props = new Properties();
343            InputStream inputStream;
344            if (isUrl(linkPlatformProperties)) {
345                inputStream = toURL(linkPlatformProperties).openStream();
346            } else {
347                inputStream = DocFile
348                    .createFileForInput(configuration, linkPlatformProperties)
349                    .openInputStream();
350            }
351            try (inputStream) {
352                props.load(inputStream);
353            }
354            url = props.getProperty("doclet.platform.docs." + version);
355        } catch (MalformedURLException exc) {
356            throw new Fault(resources.getText("doclet.MalformedURL",
357                linkPlatformProperties), exc);
358        } catch (IOException exc) {
359            throw new Fault(
360                resources.getText("doclet.URL_error", linkPlatformProperties),
361                exc);
362        } catch (DocFileIOException exc) {
363            throw new Fault(
364                resources.getText("doclet.File_error", linkPlatformProperties),
365                exc);
366        }
367        return url;
368    }
369
370    /**
371     * Return the source version number used in the current execution of javadoc.
372     * @return the source version number
373     */
374    private int getSourceVersionNumber() {
375        SourceVersion sourceVersion = configuration.docEnv.getSourceVersion();
376        // TODO it would be nice if this was provided by SourceVersion
377        String versionNumber = sourceVersion.name().substring(8);
378        assert SourceVersion
379            .valueOf("RELEASE_" + versionNumber) == sourceVersion;
380        return Integer.parseInt(versionNumber);
381    }
382
383    /**
384     * Return true if the given {@code sourceVersion} is the same as the current doclet version
385     * and is a pre-release version.
386     * @param sourceVersion the source version number
387     * @return true if it is a pre-release version
388     */
389    private boolean isPrerelease(int sourceVersion) {
390        Runtime.Version docletVersion = configuration.getDocletVersion();
391        return docletVersion.feature() == sourceVersion
392            && docletVersion.pre().isPresent();
393    }
394
395    /*
396     * Build the extern element list from given URL or the directory path.
397     * Flag error if the "-link" or "-linkoffline" option is already used.
398     *
399     * @param url URL or Directory path.
400     * 
401     * @param elemlisturl This can be another URL for "element-list" or ordinary
402     * file.
403     * 
404     * @param reporter The <code>DocErrorReporter</code> used to report errors.
405     * 
406     * @param linkoffline True if -linkoffline is used and false if -link is
407     * used.
408     * 
409     * @return true if successful, false otherwise
410     * 
411     * @throws DocFileIOException if there is a problem reading the element list
412     * file
413     */
414    private boolean link(String url, String elemlisturl, Reporter reporter,
415            boolean linkoffline)
416            throws DocFileIOException {
417        this.linkoffline = linkoffline;
418        try {
419            url = adjustEndFileSeparator(url);
420            if (isUrl(elemlisturl)) {
421                readElementListFromURL(url,
422                    toURL(adjustEndFileSeparator(elemlisturl)));
423            } else {
424                readElementListFromFile(url,
425                    DocFile.createFileForInput(configuration, elemlisturl));
426            }
427            return true;
428        } catch (Fault f) {
429            reporter.print(Diagnostic.Kind.ERROR, f.getMessage());
430            return false;
431        }
432    }
433
434    private static class Fault extends Exception {
435        private static final long serialVersionUID = 0;
436
437        Fault(String msg, Exception cause) {
438            super(msg + (cause == null ? "" : " (" + cause + ")"), cause);
439        }
440    }
441
442    /**
443     * Get the Extern Item object associated with this element name.
444     *
445     * @param element Element
446     */
447    private Item findElementItem(Element element) {
448        Item item = null;
449        if (element instanceof ModuleElement me) {
450            item = moduleItems.get(utils.getModuleName(me));
451        } else if (element instanceof PackageElement pkg) {
452            ModuleElement moduleElement = utils.containingModule(pkg);
453            Map<String, Item> pkgMap
454                = packageItems.get(utils.getModuleName(moduleElement));
455            item = (pkgMap != null) ? pkgMap.get(utils.getPackageName(pkg))
456                : null;
457        }
458        return item;
459    }
460
461    /**
462     * If the URL or Directory path is missing end file separator, add that.
463     */
464    private String adjustEndFileSeparator(String url) {
465        return url.endsWith("/") ? url : url + '/';
466    }
467
468    /**
469     * Fetch the URL and read the "element-list" file.
470     *
471     * @param urlpath        Path to the elements.
472     * @param elemlisturlpath URL or the path to the "element-list" file.
473     */
474    private void readElementListFromURL(String urlpath, URL elemlisturlpath)
475            throws Fault {
476        try {
477            URL link = elemlisturlpath.toURI()
478                .resolve(DocPaths.ELEMENT_LIST.getPath()).toURL();
479            try (InputStream in = open(link)) {
480                readElementList(in, urlpath, false, 0, false);
481            }
482        } catch (URISyntaxException | MalformedURLException exc) {
483            throw new Fault(resources.getText("doclet.MalformedURL",
484                elemlisturlpath.toString()), exc);
485        } catch (IOException exc) {
486            readPackageListFromURL(urlpath, elemlisturlpath);
487        }
488    }
489
490    /**
491     * Fetch the URL and read the "package-list" file.
492     *
493     * @param urlpath        Path to the packages.
494     * @param elemlisturlpath URL or the path to the "package-list" file.
495     */
496    private void readPackageListFromURL(String urlpath, URL elemlisturlpath)
497            throws Fault {
498        try {
499            URL link = elemlisturlpath.toURI()
500                .resolve(DocPaths.PACKAGE_LIST.getPath()).toURL();
501            try (InputStream in = open(link)) {
502                readElementList(in, urlpath, false, 0, true);
503            }
504        } catch (URISyntaxException | MalformedURLException exc) {
505            throw new Fault(resources.getText("doclet.MalformedURL",
506                elemlisturlpath.toString()), exc);
507        } catch (IOException exc) {
508            throw new Fault(resources.getText("doclet.URL_error",
509                elemlisturlpath.toString()), exc);
510        }
511    }
512
513    /**
514     * Read the "element-list" file which is available locally.
515     *
516     * @param path URL or directory path to the elements.
517     * @param elemListPath Path to the local "element-list" file.
518     * @throws Fault if an error occurs that can be treated as a warning
519     * @throws DocFileIOException if there is a problem opening the element list file
520     */
521    private void readElementListFromFile(String path, DocFile elemListPath)
522            throws Fault, DocFileIOException {
523        DocFile file = elemListPath.resolve(DocPaths.ELEMENT_LIST);
524        if (!(file.isAbsolute() || linkoffline)) {
525            file = file.resolveAgainst(
526                DocumentationTool.Location.DOCUMENTATION_OUTPUT);
527        }
528        if (file.exists()) {
529            readElementList(file, path, false);
530        } else {
531            DocFile file1 = elemListPath.resolve(DocPaths.PACKAGE_LIST);
532            if (!(file1.isAbsolute() || linkoffline)) {
533                file1 = file1.resolveAgainst(
534                    DocumentationTool.Location.DOCUMENTATION_OUTPUT);
535            }
536            if (file1.exists()) {
537                readElementList(file1, path, true);
538            } else {
539                throw new Fault(
540                    resources.getText("doclet.File_error", file.getPath()),
541                    null);
542            }
543        }
544    }
545
546    private void readElementList(DocFile file, String path,
547            boolean isOldFormDoc) throws Fault, DocFileIOException {
548        try {
549            if (file.canRead()) {
550                boolean pathIsRelative
551                    = !isUrl(path)
552                        && !DocFile.createFileForInput(configuration, path)
553                            .isAbsolute();
554                readElementList(file.openInputStream(), path, pathIsRelative, 0,
555                    isOldFormDoc);
556            } else {
557                throw new Fault(
558                    resources.getText("doclet.File_error", file.getPath()),
559                    null);
560            }
561        } catch (IOException exc) {
562            throw new Fault(
563                resources.getText("doclet.File_error", file.getPath()), exc);
564        }
565    }
566
567    /**
568     * Read the file "element-list" and for each element name found, create
569     * Extern object and associate it with the element name in the map.
570     *
571     * @param input     InputStream from the "element-list" file.
572     * @param path     URL or the directory path to the elements.
573     * @param relative Is path relative?
574     * @param platformVersion The version of platform libraries the element list belongs to,
575     *                        or {@code 0} if it does not belong to a platform libraries doc bundle.
576     * @throws IOException if there is a problem reading or closing the stream
577     */
578    private void readElementList(InputStream input, String path,
579            boolean relative, int platformVersion,
580            boolean isOldFormDoc)
581            throws IOException {
582        try (BufferedReader in
583            = new BufferedReader(new InputStreamReader(input))) {
584            String elemname;
585            DocPath elempath;
586            String moduleName = null;
587            DocPath basePath = DocPath.create(path);
588            boolean showDiagnostic = true;
589            while ((elemname = in.readLine()) != null) {
590                if (elemname.length() > 0) {
591                    elempath = basePath;
592                    if (elemname.startsWith(DocletConstants.MODULE_PREFIX)) {
593                        moduleName = elemname
594                            .replace(DocletConstants.MODULE_PREFIX, "");
595                        Item item = new Item(moduleName, elempath, relative,
596                            isOldFormDoc);
597                        moduleItems.put(moduleName, item);
598                    } else {
599                        DocPath pkgPath
600                            = DocPath.create(elemname.replace('.', '/'));
601                        // Although being modular, JDKs 9 and 10 do not use
602                        // module names in javadoc URL paths.
603                        if (moduleName != null && platformVersion != 9
604                            && platformVersion != 10) {
605                            elempath = elempath.resolve(
606                                DocPath.create(moduleName).resolve(pkgPath));
607                        } else {
608                            elempath = elempath.resolve(pkgPath);
609                        }
610                        String actualModuleName;
611                        // For user provided libraries we check whether
612                        // modularity matches the actual library.
613                        // We trust modularity to be correct for platform
614                        // library element lists.
615                        if (platformVersion == 0) {
616                            actualModuleName = checkLinkCompatibility(elemname,
617                                moduleName, path, showDiagnostic);
618                        } else {
619                            actualModuleName = moduleName == null
620                                ? DocletConstants.DEFAULT_ELEMENT_NAME
621                                : moduleName;
622                        }
623                        Item item = new Item(elemname, elempath, relative,
624                            isOldFormDoc);
625                        packageItems
626                            .computeIfAbsent(actualModuleName,
627                                k -> new TreeMap<>())
628                            .putIfAbsent(elemname, item); // first-one-wins
629                                                          // semantics
630                        showDiagnostic = false;
631                    }
632                }
633            }
634        }
635    }
636
637    /**
638     * Check if the external documentation format matches our internal model of the code.
639     * Returns the module name to use for external reference lookup according to the actual
640     * modularity of the external package (and regardless of modularity of documentation).
641     *
642     * @param packageName the package name
643     * @param moduleName the module name or null
644     * @param path the documentation path
645     * @param showDiagnostic whether to print a diagnostic message in case of modularity mismatch
646     * @return the module name to use according to actual modularity of the package
647     */
648    private String checkLinkCompatibility(String packageName, String moduleName,
649            String path, boolean showDiagnostic) {
650        PackageElement pe = utils.elementUtils.getPackageElement(packageName);
651        if (pe != null) {
652            ModuleElement me = (ModuleElement) pe.getEnclosingElement();
653            if (me == null || me.isUnnamed()) {
654                if (moduleName != null && showDiagnostic) {
655                    printModularityMismatchDiagnostic(
656                        "doclet.linkMismatch_PackagedLinkedtoModule", path);
657                }
658                // library is not modular, ignore module name even if
659                // documentation is modular
660                return DocletConstants.DEFAULT_ELEMENT_NAME;
661            } else if (moduleName == null) {
662                // suppress the diagnostic message in the case of automatic
663                // modules
664                if (!utils.elementUtils.isAutomaticModule(me)
665                    && showDiagnostic) {
666                    printModularityMismatchDiagnostic(
667                        "doclet.linkMismatch_ModuleLinkedtoPackage", path);
668                }
669                // library is modular, use module name for lookup even though
670                // documentation is not
671                return utils.getModuleName(me);
672            }
673        }
674        return moduleName == null ? DocletConstants.DEFAULT_ELEMENT_NAME
675            : moduleName;
676    }
677
678    public boolean isUrl(String urlCandidate) {
679        try {
680            @SuppressWarnings("deprecation")
681            var _unused = new URL(urlCandidate);
682            // No exception was thrown, so this must really be a URL.
683            return true;
684        } catch (MalformedURLException e) {
685            // Since exception is thrown, this must be a directory path.
686            return false;
687        }
688    }
689
690    @SuppressWarnings("deprecation")
691    private URL toURL(String url) throws Fault {
692        try {
693            return new URL(url);
694        } catch (MalformedURLException e) {
695            throw new Fault(resources.getText("doclet.MalformedURL", url), e);
696        }
697    }
698
699    /**
700     * Open a stream to a URL, following a limited number of redirects
701     * if necessary.
702     *
703     * @param url the URL
704     * @return the stream
705     * @throws IOException if an error occurred accessing the URL
706     */
707    private InputStream open(URL url) throws IOException {
708        URLConnection conn = url.openConnection();
709
710        boolean redir;
711        int redirects = 0;
712        InputStream in;
713
714        do {
715            // Open the input stream before getting headers,
716            // because getHeaderField() et al swallow IOExceptions.
717            in = conn.getInputStream();
718            redir = false;
719
720            if (conn instanceof HttpURLConnection http) {
721                int stat = http.getResponseCode();
722                // See:
723                // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
724                // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#3xx_Redirection
725                switch (stat) {
726                case 300: // Multiple Choices
727                case 301: // Moved Permanently
728                case 302: // Found (previously Moved Temporarily)
729                case 303: // See Other
730                case 307: // Temporary Redirect
731                case 308: // Permanent Redirect
732                    URL base = http.getURL();
733                    String loc = http.getHeaderField("Location");
734                    URL target = null;
735                    if (loc != null) {
736                        @SuppressWarnings("deprecation")
737                        var _unused = target = new URL(base, loc);
738                    }
739                    http.disconnect();
740                    if (target == null || redirects >= 5) {
741                        throw new IOException("illegal URL redirect");
742                    }
743                    redir = true;
744                    conn = target.openConnection();
745                    redirects++;
746                }
747            }
748        } while (redir);
749
750        if (!url.equals(conn.getURL())) {
751            configuration.getReporter().print(Kind.WARNING,
752                resources.getText("doclet.urlRedirected", url, conn.getURL()));
753        }
754
755        return in;
756    }
757
758    private void printModularityMismatchDiagnostic(String key, Object arg) {
759        switch (configuration.getOptions().linkModularityMismatch()) {
760        case INFO -> configuration.getMessages().notice(key, arg);
761        case WARN -> configuration.getMessages().warning(key, arg);
762        }
763    }
764
765    /**
766     * Converts a name to an old-form HTML name (old-form id).
767     *
768     * @param name the string that needs to be converted to a valid HTML name
769     * @return old-form HTML name
770     */
771    private String getOldFormHtmlName(String name) {
772        /*
773         * The HTML 4 spec at http://www.w3.org/TR/html4/types.html#h-6.2
774         * mentions
775         * that the name/id should begin with a letter followed by other valid
776         * characters.
777         * The HTML 5 spec (draft) is more permissive on names/ids where the
778         * only restriction
779         * is that it should be at least one character long and should not
780         * contain spaces.
781         * The spec draft is @
782         * http://www.w3.org/html/wg/drafts/html/master/dom.html#the-id-
783         * attribute.
784         *
785         * For HTML 4, we need to check for non-characters at the beginning of
786         * the name and
787         * substitute it accordingly, "_" and "$" can appear at the beginning of
788         * a member name.
789         * The method substitutes "$" with "Z:Z:D" and will prefix "_" with
790         * "Z:Z".
791         */
792
793        if (null == name)
794            return name;
795
796        StringBuilder sb = new StringBuilder();
797        for (int i = 0; i < name.length(); i++) {
798            char ch = name.charAt(i);
799            switch (ch) {
800            case '(':
801            case ')':
802            case '<':
803            case '>':
804            case ',':
805                sb.append('-');
806                break;
807            case ' ':
808            case '[':
809                break;
810            case ']':
811                sb.append(":A");
812                break;
813            // Any appearance of $ needs to be substituted with ":D" and not
814            // with hyphen
815            // since a field name "P$$ and a method P(), both valid member
816            // names, can end
817            // up as "P--". A member name beginning with $ needs to be
818            // substituted with
819            // "Z:Z:D".
820            case '$':
821                if (i == 0)
822                    sb.append("Z:Z");
823                sb.append(":D");
824                break;
825            // A member name beginning with _ needs to be prefixed with "Z:Z"
826            // since valid anchor
827            // names can only begin with a letter.
828            case '_':
829                if (i == 0)
830                    sb.append("Z:Z");
831                sb.append(ch);
832                break;
833            default:
834                sb.append(ch);
835            }
836        }
837        return sb.toString();
838    }
839}