All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.maven.tools.plugin.javadoc.JavadocSite Maven / Gradle / Ivy

Go to download

The Maven Plugin Tools Extractor API provides an API to extract descriptor information from Maven Plugins.

The newest version!
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.maven.tools.plugin.javadoc;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.AbstractMap;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.regex.Pattern;

import org.apache.http.HttpHeaders;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.CookieSpecs;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicHeader;
import org.apache.maven.settings.Proxy;
import org.apache.maven.settings.Settings;
import org.apache.maven.tools.plugin.javadoc.FullyQualifiedJavadocReference.MemberType;
import org.apache.maven.wagon.proxy.ProxyInfo;
import org.apache.maven.wagon.proxy.ProxyUtils;
import org.codehaus.plexus.util.StringUtils;

/**
 * Allows to create links to a site generated by javadoc (incl. deep-linking).
 * The site may be either accessible (online) or non-accessible (offline) when using this class.
 */
class JavadocSite {
    private static final String PREFIX_MODULE = "module:";

    final URI baseUri;

    final Settings settings;

    final Map containedPackageNamesAndModules; // empty in case this an offline site

    final boolean requireModuleNameInPath;

    static final EnumMap<
                    FullyQualifiedJavadocReference.MemberType, EnumSet>
            VERSIONS_PER_TYPE;

    static {
        VERSIONS_PER_TYPE = new EnumMap<>(FullyQualifiedJavadocReference.MemberType.class);
        VERSIONS_PER_TYPE.put(
                MemberType.CONSTRUCTOR,
                EnumSet.of(
                        JavadocLinkGenerator.JavadocToolVersionRange.JDK7_OR_LOWER,
                        JavadocLinkGenerator.JavadocToolVersionRange.JDK8_OR_9,
                        JavadocLinkGenerator.JavadocToolVersionRange.JDK10_OR_HIGHER));
        VERSIONS_PER_TYPE.put(
                MemberType.METHOD,
                EnumSet.of(
                        JavadocLinkGenerator.JavadocToolVersionRange.JDK7_OR_LOWER,
                        JavadocLinkGenerator.JavadocToolVersionRange.JDK8_OR_9,
                        JavadocLinkGenerator.JavadocToolVersionRange.JDK10_OR_HIGHER));
        VERSIONS_PER_TYPE.put(
                MemberType.FIELD,
                EnumSet.of(
                        JavadocLinkGenerator.JavadocToolVersionRange.JDK7_OR_LOWER,
                        JavadocLinkGenerator.JavadocToolVersionRange.JDK8_OR_9));
    }

    JavadocLinkGenerator.JavadocToolVersionRange version; // null in case not yet known for online sites

    /**
     * Constructor for online sites having an accessible {@code package-list} or {@code element-list}.
     * @param url
     * @param settings
     * @throws IOException
     */
    JavadocSite(final URI url, final Settings settings) throws IOException {
        Map containedPackageNamesAndModules;
        boolean requireModuleNameInPath = false;
        try {
            // javadoc > 1.2 && < 10
            containedPackageNamesAndModules = getPackageListWithModules(url.resolve("package-list"), settings);
        } catch (FileNotFoundException e) {
            try {
                // javadoc 10+
                containedPackageNamesAndModules = getPackageListWithModules(url.resolve("element-list"), settings);

                Optional firstModuleName = containedPackageNamesAndModules.values().stream()
                        .filter(StringUtils::isNotBlank)
                        .findFirst();
                if (firstModuleName.isPresent()) {
                    // are module names part of the URL (since JDK11)?
                    try (Reader reader = getReader(
                            url.resolve(firstModuleName.get() + "/module-summary.html")
                                    .toURL(),
                            null)) {
                        requireModuleNameInPath = true;
                    } catch (IOException ioe) {
                        // ignore
                    }
                }
            } catch (FileNotFoundException e2) {
                throw new IOException("Found neither 'package-list' nor 'element-list' below url " + url
                        + ". The given URL does probably not specify the root of a javadoc site or has been generated with"
                        + " javadoc 1.2 or older.");
            }
        }
        this.containedPackageNamesAndModules = containedPackageNamesAndModules;
        this.baseUri = url;
        this.settings = settings;
        this.version = null;
        this.requireModuleNameInPath = requireModuleNameInPath;
    }

    /** Constructor for offline sites. This throws {@link UnsupportedOperationException}
     *  for {@link #hasEntryFor(Optional, Optional)}. */
    JavadocSite(final URI url, JavadocLinkGenerator.JavadocToolVersionRange version, boolean requireModuleNameInPath) {
        Objects.requireNonNull(url);
        this.baseUri = url;
        Objects.requireNonNull(version);
        this.version = version;
        this.settings = null;
        this.containedPackageNamesAndModules = Collections.emptyMap();
        this.requireModuleNameInPath = requireModuleNameInPath;
    }

    static Map getPackageListWithModules(final URI url, final Settings settings) throws IOException {
        Map containedPackageNamesAndModules = new HashMap<>();
        try (BufferedReader reader = getReader(url.toURL(), settings)) {
            String line;
            String module = null;
            while ((line = reader.readLine()) != null) {
                // each line starting with "module:" contains the module name afterwards
                if (line.startsWith(PREFIX_MODULE)) {
                    module = line.substring(PREFIX_MODULE.length());
                } else {
                    containedPackageNamesAndModules.put(line, module);
                }
            }
            return containedPackageNamesAndModules;
        }
    }

    static boolean findLineContaining(final URI url, final Settings settings, Pattern pattern) throws IOException {
        try (BufferedReader reader = getReader(url.toURL(), settings)) {
            return reader.lines().anyMatch(pattern.asPredicate());
        }
    }

    public URI getBaseUri() {
        return baseUri;
    }

    public boolean hasEntryFor(Optional moduleName, Optional packageName) {
        if (containedPackageNamesAndModules.isEmpty()) {
            throw new UnsupportedOperationException(
                    "Operation hasEntryFor(...) is not supported for offline " + "javadoc sites");
        }
        if (packageName.isPresent()) {
            if (moduleName.isPresent()) {
                String actualModuleName = containedPackageNamesAndModules.get(packageName.get());
                if (!moduleName.get().equals(actualModuleName)) {
                    return false;
                }
            } else {
                if (!containedPackageNamesAndModules.containsKey(packageName.get())) {
                    return false;
                }
            }
        } else if (moduleName.isPresent()) {
            if (!containedPackageNamesAndModules.containsValue(moduleName.get())) {
                return false;
            }
        } else {
            throw new IllegalArgumentException("Either module name or package name must be set!");
        }
        return true;
    }

    /**
     * Generates a link to a javadoc html page below the javadoc site represented by this object.
     * The link is not validated (i.e. might point to a non-existing page)
     * @param
     * @return the (deep-)link towards a javadoc page
     * @throws IllegalArgumentException if no link can be created
     */
    public URI createLink(String packageName, String className) {
        try {
            if (className.endsWith("[]")) {
                // url must point to simple class
                className = className.substring(0, className.length() - 2);
            }
            return createLink(baseUri, Optional.empty(), Optional.of(packageName), Optional.of(className));
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException("Could not create link for " + packageName + "." + className, e);
        }
    }

    /**
     * Splits up a given binary class name into package name and simple class name part.
     * @param binaryName a binary name according to
     * JLS 13.1
     * @return a key value pair where the key is the package name and the value the class name
     * @throws IllegalArgumentException if no link can be created
     */
    static Map.Entry getPackageAndClassName(String binaryName) {
        // assume binary name according to https://docs.oracle.com/javase/specs/jls/se8/html/jls-13.html#jls-13.1
        int indexOfDollar = binaryName.indexOf('$');
        int indexOfDotBetweenPackageAndClass;
        if (indexOfDollar >= 0) {
            // check following character
            if (Character.isDigit(binaryName.charAt(indexOfDollar + 1))) {
                // emit some warning, as non resolvable: unclear which type of member follows if it is non digit
                throw new IllegalArgumentException(
                        "Can only resolve binary names of member classes, " + "but not local or anonymous classes");
            }
            // member is class, field or method....
            indexOfDotBetweenPackageAndClass = binaryName.lastIndexOf('.', indexOfDollar);
            // replace dollar by dot
            binaryName = binaryName.replace('$', '.');
        } else {
            indexOfDotBetweenPackageAndClass = binaryName.lastIndexOf('.');
        }
        if (indexOfDotBetweenPackageAndClass < 0) {
            throw new IllegalArgumentException("Resolving primitives is not supported. "
                    + "Binary name must contain at least one dot: " + binaryName);
        }
        if (indexOfDotBetweenPackageAndClass == binaryName.length() - 1) {
            throw new IllegalArgumentException("Invalid binary name ending with a dot: " + binaryName);
        }
        String packageName = binaryName.substring(0, indexOfDotBetweenPackageAndClass);
        String className = binaryName.substring(indexOfDotBetweenPackageAndClass + 1, binaryName.length());
        return new AbstractMap.SimpleEntry<>(packageName, className);
    }

    /**
     * Generates a link to a javadoc html page below the javadoc site represented by this object.
     * The link is not validated (i.e. might point to a non-existing page)
     * @param javadocReference a code reference from a javadoc tag
     * @return  the (deep-)link towards a javadoc page
     * @throws IllegalArgumentException if no link can be created
     */
    public URI createLink(FullyQualifiedJavadocReference javadocReference) throws IllegalArgumentException {
        final Optional moduleName;
        if (!requireModuleNameInPath) {
            moduleName = Optional.empty();
        } else {
            moduleName = Optional.ofNullable(javadocReference
                    .getModuleName()
                    .orElse(containedPackageNamesAndModules.get(
                            javadocReference.getPackageName().orElse(null))));
        }
        return createLink(javadocReference, baseUri, this::appendMemberAsFragment, moduleName);
    }

    static URI createLink(
            FullyQualifiedJavadocReference javadocReference,
            URI baseUri,
            BiFunction fragmentAppender,
            Optional resolvedModuleName)
            throws IllegalArgumentException {
        try {
            URI uri = createLink(
                    baseUri,
                    javadocReference.getModuleName().isPresent()
                            ? javadocReference.getModuleName()
                            : resolvedModuleName,
                    javadocReference.getPackageName(),
                    javadocReference.getClassName());
            return fragmentAppender.apply(uri, javadocReference);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException("Could not create link for " + javadocReference, e);
        }
    }

    static URI createLink(
            URI baseUri, Optional moduleName, Optional packageName, Optional className)
            throws URISyntaxException {
        StringBuilder link = new StringBuilder();
        if (moduleName.isPresent()) {
            link.append(moduleName.get() + "/");
        }
        if (packageName.isPresent()) {
            link.append(packageName.get().replace('.', '/'));
        }
        if (!className.isPresent()) {
            if (packageName.isPresent()) {
                link.append("/package-summary.html");
            } else if (moduleName.isPresent()) {
                link.append("/module-summary.html");
            }
        } else {
            link.append('/').append(className.get()).append(".html");
        }
        return baseUri.resolve(new URI(null, link.toString(), null));
    }

    URI appendMemberAsFragment(URI url, FullyQualifiedJavadocReference reference) {
        try {
            return appendMemberAsFragment(url, reference.getMember(), reference.getMemberType());
        } catch (URISyntaxException | IOException e) {
            throw new IllegalArgumentException("Could not create link for " + reference, e);
        }
    }

    // CHECKSTYLE_OFF: LineLength
    /**
     * @param url
     * @param optionalMember
     * @param optionalMemberType
     * @return
     * @throws URISyntaxException
     * @throws IOException
     * @see 
     *      Name generation in Javadoc8
     * @see Javadoc
     *      Tools Source since JDK10
     * @see Javadoc
     *      Tools Source JDK9
     * @see Javadoc
     *      Tools Source JDK8
     */
    // CHECKSTYLE_ON: LineLength
    URI appendMemberAsFragment(URI url, Optional optionalMember, Optional optionalMemberType)
            throws URISyntaxException, IOException {
        if (!optionalMember.isPresent()) {
            return url;
        }
        MemberType memberType = optionalMemberType.orElse(null);
        final String member = optionalMember.get();
        String fragment = member;
        if (version != null) {
            fragment = getFragmentForMember(version, member, memberType == MemberType.CONSTRUCTOR);
        } else {
            // try out all potential formats
            for (JavadocLinkGenerator.JavadocToolVersionRange potentialVersion : VERSIONS_PER_TYPE.get(memberType)) {
                fragment = getFragmentForMember(potentialVersion, member, memberType == MemberType.CONSTRUCTOR);
                if (findAnchor(url, fragment)) {
                    // only derive javadoc version if there is no ambiguity
                    if (memberType == MemberType.CONSTRUCTOR || memberType == MemberType.METHOD) {
                        version = potentialVersion;
                    }
                    break;
                }
            }
        }
        return new URI(url.getScheme(), url.getSchemeSpecificPart(), fragment);
    }

    /**
     * canonical format given by member is using parentheses and comma.
     *
     * @param version
     * @param member
     * @param isConstructor
     * @return the anchor
     */
    static String getFragmentForMember(
            JavadocLinkGenerator.JavadocToolVersionRange version, String member, boolean isConstructor) {
        String fragment = member;
        switch (version) {
            case JDK7_OR_LOWER:
                // separate argument by spaces
                fragment = fragment.replace(",", ", ");
                break;
            case JDK8_OR_9:
                // replace [] by ":A"
                fragment = fragment.replace("[]", ":A");
                // separate arguments by "-", enclose all arguments in "-" for javadoc 8
                fragment = fragment.replace('(', '-').replace(')', '-').replace(',', '-');
                break;
            case JDK10_OR_HIGHER:
                if (isConstructor) {
                    int indexOfOpeningParenthesis = fragment.indexOf('(');
                    if (indexOfOpeningParenthesis >= 0) {
                        fragment = "<init>" + fragment.substring(indexOfOpeningParenthesis);
                    } else {
                        fragment = "<init>";
                    }
                }
                break;
            default:
                throw new IllegalArgumentException("No valid version range given");
        }
        return fragment;
    }

    boolean findAnchor(URI uri, String anchorNameOrId) throws MalformedURLException, IOException {
        return findLineContaining(uri, settings, getAnchorPattern(anchorNameOrId));
    }

    static Pattern getAnchorPattern(String anchorNameOrId) {
        // javadoc 17 uses"
>" return Pattern.compile(".*(name|NAME|id)=\\\"" + Pattern.quote(anchorNameOrId) + "\\\""); } // --------------- // CHECKSTYLE_OFF: LineLength // the following methods are copies from private methods contained in // https://github.com/apache/maven-javadoc-plugin/blob/231316be785782b61d96783fad111325868cfa1f/src/main/java/org/apache/maven/plugins/javadoc/JavadocUtil.java // CHECKSTYLE_ON: LineLength // --------------- /** The default timeout used when fetching url, i.e. 2000. */ public static final int DEFAULT_TIMEOUT = 2000; /** * Creates a new {@code HttpClient} instance. * * @param settings The settings to use for setting up the client or {@code null}. * @param url The {@code URL} to use for setting up the client or {@code null}. * @return A new {@code HttpClient} instance. * @see #DEFAULT_TIMEOUT * @since 2.8 */ private static CloseableHttpClient createHttpClient(Settings settings, URL url) { HttpClientBuilder builder = HttpClients.custom(); Registry csfRegistry = RegistryBuilder.create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", SSLConnectionSocketFactory.getSystemSocketFactory()) .build(); builder.setConnectionManager(new PoolingHttpClientConnectionManager(csfRegistry)); builder.setDefaultRequestConfig(RequestConfig.custom() .setSocketTimeout(DEFAULT_TIMEOUT) .setConnectTimeout(DEFAULT_TIMEOUT) .setCircularRedirectsAllowed(true) .setCookieSpec(CookieSpecs.IGNORE_COOKIES) .build()); // Some web servers don't allow the default user-agent sent by httpClient builder.setUserAgent("Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.0)"); // Some server reject requests that do not have an Accept header builder.setDefaultHeaders(Arrays.asList(new BasicHeader(HttpHeaders.ACCEPT, "*/*"))); if (settings != null && settings.getActiveProxy() != null) { Proxy activeProxy = settings.getActiveProxy(); ProxyInfo proxyInfo = new ProxyInfo(); proxyInfo.setNonProxyHosts(activeProxy.getNonProxyHosts()); if (StringUtils.isNotEmpty(activeProxy.getHost()) && (url == null || !ProxyUtils.validateNonProxyHosts(proxyInfo, url.getHost()))) { HttpHost proxy = new HttpHost(activeProxy.getHost(), activeProxy.getPort()); builder.setProxy(proxy); if (StringUtils.isNotEmpty(activeProxy.getUsername()) && activeProxy.getPassword() != null) { Credentials credentials = new UsernamePasswordCredentials(activeProxy.getUsername(), activeProxy.getPassword()); CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials(AuthScope.ANY, credentials); builder.setDefaultCredentialsProvider(credentialsProvider); } } } return builder.build(); } static BufferedReader getReader(URL url, Settings settings) throws IOException { BufferedReader reader = null; if ("file".equals(url.getProtocol())) { // Intentionally using the platform default encoding here since this is what Javadoc uses internally. reader = new BufferedReader(new InputStreamReader(url.openStream())); } else { // http, https... final CloseableHttpClient httpClient = createHttpClient(settings, url); final HttpGet httpMethod = new HttpGet(url.toString()); HttpResponse response; HttpClientContext httpContext = HttpClientContext.create(); try { response = httpClient.execute(httpMethod, httpContext); } catch (SocketTimeoutException e) { // could be a sporadic failure, one more retry before we give up response = httpClient.execute(httpMethod, httpContext); } int status = response.getStatusLine().getStatusCode(); if (status != HttpStatus.SC_OK) { throw new FileNotFoundException( "Unexpected HTTP status code " + status + " getting resource " + url.toExternalForm() + "."); } else { int pos = url.getPath().lastIndexOf('/'); List redirects = httpContext.getRedirectLocations(); if (pos >= 0 && isNotEmpty(redirects)) { URI location = redirects.get(redirects.size() - 1); String suffix = url.getPath().substring(pos); // Redirections shall point to the same file, e.g. /package-list if (!location.getPath().endsWith(suffix)) { throw new FileNotFoundException(url.toExternalForm() + " redirects to " + location.toURL().toExternalForm() + "."); } } } // Intentionally using the platform default encoding here since this is what Javadoc uses internally. reader = new BufferedReader( new InputStreamReader(response.getEntity().getContent())) { @Override public void close() throws IOException { super.close(); if (httpMethod != null) { httpMethod.releaseConnection(); } if (httpClient != null) { httpClient.close(); } } }; } return reader; } /** * Convenience method to determine that a collection is not empty or null. * * @param collection the collection to verify * @return {@code true} if not {@code null} and not empty, otherwise {@code false} */ public static boolean isNotEmpty(final Collection collection) { return collection != null && !collection.isEmpty(); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy