org.forgerock.maven.plugins.LinkTester.java Source code

Java tutorial

Introduction

Here is the source code for org.forgerock.maven.plugins.LinkTester.java

Source

/*
 * The contents of this file are subject to the terms of the Common Development and
 * Distribution License (the License). You may not use this file except in compliance with the
 * License.
 *
 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
 * specific language governing permission and limitations under the License.
 *
 * When distributing Covered Software, include this CDDL Header Notice in each file and include
 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
 * Header, with the fields enclosed by brackets [] replaced by your own identifying
 * information: "Portions copyright [year] [name of copyright owner]".
 *
 * Copyright 2011-2014 ForgeRock AS.
 */
package org.forgerock.maven.plugins;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.commons.collections.map.MultiValueMap;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.util.DirectoryScanner;
import org.forgerock.maven.plugins.model.DocSource;
import org.forgerock.maven.plugins.utils.LoggingErrorHandler;
import org.forgerock.maven.plugins.utils.TrustAllHostnameVerifier;
import org.forgerock.maven.plugins.utils.XmlNamespaceContext;
import org.forgerock.maven.plugins.utils.TrustAllCertsX509TrustManager;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

/**
 * This goal will check the selected DocBook documents for XML validity and will also verify the validity of external
 * links and interdocument olinks that can be found throughout the project.
 */
@Mojo(name = "check")
public class LinkTester extends AbstractMojo {

    private static final String DOCBOOK_XSD = "http://docbook.org/xml/5.0/xsd/docbook.xsd";
    private static final String DOCBOOK_NS = "http://docbook.org/ns/docbook";
    private static final String OLINK_ROLE = "http://docbook.org/xlink/role/olink";
    private static final SSLSocketFactory TRUST_ALL_SOCKET_FACTORY;

    /**
     * The list of {@link DocSource} elements that will be used to locate all the documentation files.
     */
    @Parameter
    private List<DocSource> docSources;
    /**
     * Access to the Maven Project settings.
     */
    @Parameter(defaultValue = "${project}", required = true, readonly = true)
    protected MavenProject project = new MavenProject();
    /**
     * Whether to validate the XML against the DocBook XML Schema.
     */
    @Parameter(defaultValue = "false")
    private boolean validating;
    /**
     * Whether to resolve xinclude:include tags and inline the referred documents into the processed XML content.
     */
    @Parameter(defaultValue = "false")
    private boolean xIncludeAware;
    /**
     * Set to <code>true</code> if you want to disable olink checks in your DocBook document.
     */
    @Parameter(defaultValue = "false")
    private boolean skipOlinks;
    /**
     * Set to <code>true</code> if you want to disable checks for external links.
     */
    @Parameter(defaultValue = "false")
    private boolean skipUrls;
    /**
     * Valid {@link java.util.regex.Pattern Pattern} enumeration. URLs matching either one of the patterns won't get
     * validated.
     */
    @Parameter
    private String[] skipUrlPatterns;
    /**
     * Set to <code>true</code> if you want to fail the build upon validation error or invalid links.
     */
    @Parameter(defaultValue = "false")
    private boolean failOnError;
    /**
     * The location of the file where the plugin report is written.
     */
    @Parameter
    private File outputFile;
    private FileWriter fileWriter;
    private boolean failure;
    private String currentPath;
    private final List<Pattern> patterns = new ArrayList<Pattern>();
    private final MultiValueMap failedUrls = new MultiValueMap();
    private final MultiValueMap timedOutUrls = new MultiValueMap();
    private final MultiValueMap xmlIds = new MultiValueMap();
    private final MultiValueMap olinks = new MultiValueMap();
    private final Set<String> tested = new HashSet<String>();

    static {
        TrustManager[] trustAllCerts = new TrustManager[] { new TrustAllCertsX509TrustManager() };

        try {
            SSLContext sc = SSLContext.getInstance("SSL");
            sc.init(null, trustAllCerts, new SecureRandom());
            TRUST_ALL_SOCKET_FACTORY = sc.getSocketFactory();
        } catch (GeneralSecurityException gse) {
            throw new IllegalStateException("Unable to initialize trustAllCerts SSLSocketFactory", gse);
        }
    }

    @Override()
    public void execute() throws MojoExecutionException, MojoFailureException {
        if (outputFile != null) {
            if (!outputFile.isAbsolute()) {
                outputFile = new File(project.getBasedir(), outputFile.getPath());
            }
            if (outputFile.exists()) {
                debug("Deleting existing outputFile: " + outputFile);
                outputFile.delete();
            }
            try {
                outputFile.createNewFile();
                fileWriter = new FileWriter(outputFile);
            } catch (IOException ioe) {
                error("Error while creating output file", ioe);
            }
        }
        initializeSkipUrlPatterns();

        //Initialize XML parsers and XPath expressions
        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
        dbf.setNamespaceAware(true);
        dbf.setExpandEntityReferences(false);
        dbf.setXIncludeAware(xIncludeAware);

        if (validating) {
            SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
            try {
                Schema schema = sf.newSchema(new URL(DOCBOOK_XSD));
                dbf.setSchema(schema);
            } catch (MalformedURLException murle) {
                error("Invalid URL provided as schema source", murle);
            } catch (SAXException saxe) {
                error("Parsing error occurred while constructing schema for validation", saxe);
            }
        }
        DocumentBuilder db;
        try {
            db = dbf.newDocumentBuilder();
            db.setErrorHandler(new LoggingErrorHandler(this));
        } catch (ParserConfigurationException pce) {
            throw new MojoExecutionException("Unable to create new DocumentBuilder", pce);
        }

        XPathFactory xpf = XPathFactory.newInstance();
        XPath xpath = xpf.newXPath();
        xpath.setNamespaceContext(new XmlNamespaceContext());
        XPathExpression expr;
        try {
            expr = xpath.compile("//@xml:id");
        } catch (XPathExpressionException xpee) {
            throw new MojoExecutionException("Unable to compile Xpath expression", xpee);
        }

        if (docSources != null) {
            for (DocSource docSource : docSources) {
                processDocSource(docSource, db, expr);
            }
        }

        try {
            if (!skipOlinks) {
                //we can only check olinks after going through all the documents, otherwise we would see false
                //positives, because of the not yet processed files
                for (Map.Entry<String, Collection<String>> entry : (Set<Map.Entry<String, Collection<String>>>) olinks
                        .entrySet()) {
                    for (String val : entry.getValue()) {
                        checkOlink(entry.getKey(), val);
                    }
                }
            }
            if (!failedUrls.isEmpty()) {
                error("The following files had invalid URLs:\n" + failedUrls.toString());
            }
            if (!timedOutUrls.isEmpty()) {
                warn("The following files had unavailable URLs (connection or read timed out):\n"
                        + timedOutUrls.toString());
            }
            if (failedUrls.isEmpty() && timedOutUrls.isEmpty() && !failure) {
                //there are no failed URLs and the parser didn't encounter any errors either
                info("DocBook links successfully tested, no errors reported.");
            }
        } finally {
            flushReport();
        }
        if (failOnError) {
            if (failure || !failedUrls.isEmpty()) {
                throw new MojoFailureException("One or more error occurred during plugin execution");
            }
        }
    }

    private void processDocSource(DocSource docSource, DocumentBuilder db, XPathExpression expr) {
        DirectoryScanner scanner = new DirectoryScanner();

        File baseDir = docSource.getDirectory();
        if (baseDir == null) {
            baseDir = project.getBasedir();
        }
        scanner.setBasedir(baseDir);
        scanner.setIncludes(docSource.getIncludes());
        scanner.setExcludes(docSource.getExcludes());
        scanner.scan();

        String[] files = scanner.getIncludedFiles();
        for (String relativePath : files) {
            setCurrentPath(relativePath);
            try {
                Document doc = db.parse(new File(baseDir, relativePath));
                if (!skipOlinks) {
                    extractXmlIds(expr, doc, relativePath);
                }
                NodeList nodes = doc.getElementsByTagNameNS(DOCBOOK_NS, "link");
                for (int i = 0; i < nodes.getLength(); i++) {
                    Node node = nodes.item(i);
                    NamedNodeMap attrs = node.getAttributes();
                    String url = null;
                    boolean isOlink = false;
                    for (int j = 0; j < attrs.getLength(); j++) {
                        Node attr = attrs.item(j);
                        if (attr.getLocalName().equals("href")) {
                            url = attr.getNodeValue();
                        } else if (attr.getLocalName().equals("role")
                                && attr.getNodeValue().equalsIgnoreCase(OLINK_ROLE)) {
                            isOlink = true;
                        }
                    }
                    if (url != null) {
                        if (isOlink && !skipOlinks) {
                            olinks.put(relativePath, url);
                        } else if (!isOlink && !skipUrls) {
                            checkUrl(relativePath, url);
                        }
                    }
                }
            } catch (Exception ex) {
                error("Error while processing file: " + relativePath + ". Error: " + ex.getMessage(), ex);
            }
        }
    }

    private void extractXmlIds(XPathExpression expr, Document doc, String path) throws XPathExpressionException {
        NodeList ids = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
        if (ids != null) {
            for (int i = 0; i < ids.getLength(); i++) {
                Node node = ids.item(i);
                File file = new File(path);
                xmlIds.put(file.getParentFile().getName(), node.getNodeValue());
            }
        }
    }

    private void checkUrl(String path, String docUrl) {
        if (shouldSkipUrl(docUrl)) {
            debug("Skipping " + docUrl + " since it matches a skipUrlPattern");
            return;
        }
        if (tested.contains(docUrl)) {
            if (failedUrls.containsValue(docUrl)) {
                failedUrls.put(path, docUrl);
            }
            return;
        }
        debug("Checking " + docUrl + " from file: " + path);
        try {
            URL url = new URL(docUrl);
            URLConnection urlConn = url.openConnection();
            if (urlConn instanceof HttpURLConnection) {
                HttpURLConnection conn = (HttpURLConnection) urlConn;
                if (conn instanceof HttpsURLConnection) {
                    HttpsURLConnection httpsConn = (HttpsURLConnection) conn;
                    httpsConn.setHostnameVerifier(new TrustAllHostnameVerifier());
                    httpsConn.setSSLSocketFactory(TRUST_ALL_SOCKET_FACTORY);
                }

                conn.setConnectTimeout(1000);
                //if we don't get anything back within 15 seconds it is safe to assume that something is really wrong
                //with that site..
                conn.setReadTimeout(15000);
                int responseCode = conn.getResponseCode();
                if (responseCode >= 400) {
                    warn(docUrl + ": received unexpected response code: " + responseCode);
                    failedUrls.put(path, docUrl);
                }
            }
        } catch (SocketTimeoutException ste) {
            warn(docUrl + ": " + ste.getClass().getName() + " " + ste.getMessage());
            timedOutUrls.put(path, docUrl);
        } catch (Exception ex) {
            warn(docUrl + ": " + ex.getClass().getName() + " " + ex.getMessage());
            failedUrls.put(path, docUrl);
        }
        tested.add(docUrl);
    }

    private void checkOlink(String path, String olink) {
        String[] parts = olink.split("#");
        if (parts.length != 2) {
            failedUrls.put(path, olink);
            return;
        }
        Collection coll = xmlIds.getCollection(parts[0]);
        if (coll == null || !coll.contains(parts[1])) {
            failedUrls.put(path, olink);
        }
    }

    public String getCurrentPath() {
        return currentPath;
    }

    private void setCurrentPath(String path) {
        currentPath = path;
    }

    public void fail(String errorMessage) {
        failure = true;
        error(errorMessage);
    }

    public final void debug(String line) {
        if (getLog().isDebugEnabled()) {
            getLog().debug(line);
            report("[DEBUG] " + line);
        }
    }

    public final void warn(String line) {
        getLog().warn(line);
        report("[WARNING] " + line);
    }

    public final void info(String line) {
        getLog().info(line);
        report("[INFO] " + line);
    }

    public final void error(String line) {
        getLog().error(line);
        report("[ERROR] " + line);
    }

    public final void error(String line, Throwable throwable) {
        getLog().error(line, throwable);
        report("[ERROR] " + line, throwable);
    }

    private void report(String line) {
        try {
            if (fileWriter != null) {
                fileWriter.write(line);
                fileWriter.write("\n");
            }
        } catch (IOException ioe) {
            getLog().error("Error while writing to outputFile: " + ioe.getMessage());
        }
    }

    private void report(String line, Throwable throwable) {
        if (fileWriter != null) {
            report(line);
            throwable.printStackTrace(new PrintWriter(fileWriter));
        }
    }

    private void flushReport() {
        if (fileWriter != null) {
            try {
                fileWriter.flush();
                fileWriter.close();
            } catch (IOException ioe) {
                getLog().error("Error while flushing report: " + ioe.getMessage());
            }
        }
    }

    private void initializeSkipUrlPatterns() {
        if (skipUrlPatterns != null) {
            for (String pattern : skipUrlPatterns) {
                patterns.add(Pattern.compile(pattern));
            }
        }
    }

    private boolean shouldSkipUrl(String docUrl) {
        for (Pattern pattern : patterns) {
            if (pattern.matcher(docUrl).matches()) {
                return true;
            }
        }
        //if the skipUrlPattern list is empty or there was no match, then we should check the URL
        return false;
    }
}