com.github.cukedoctor.jenkins.CukedoctorPublisher.java Source code

Java tutorial

Introduction

Here is the source code for com.github.cukedoctor.jenkins.CukedoctorPublisher.java

Source

/*
 * The MIT License
 *
 * Copyright 2016 rmpestano.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package com.github.cukedoctor.jenkins;

import static com.github.cukedoctor.util.Assert.hasText;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintStream;
import java.nio.file.Paths;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import com.github.cukedoctor.config.GlobalConfig;
import org.apache.commons.io.IOUtils;
import org.asciidoctor.Asciidoctor;
import org.asciidoctor.OptionsBuilder;
import org.asciidoctor.SafeMode;
import org.kohsuke.stapler.DataBoundConstructor;

import com.github.cukedoctor.Cukedoctor;
import com.github.cukedoctor.api.CukedoctorConverter;
import com.github.cukedoctor.api.DocumentAttributes;
import com.github.cukedoctor.api.model.Feature;
import com.github.cukedoctor.extension.CukedoctorExtensionRegistry;
import com.github.cukedoctor.jenkins.model.FormatType;
import com.github.cukedoctor.jenkins.model.TocType;
import com.github.cukedoctor.parser.FeatureParser;
import com.github.cukedoctor.util.FileUtil;

import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.Result;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.util.ListBoxModel;
import jenkins.tasks.SimpleBuildStep;

/**
 * @author rmpestano
 */
public class CukedoctorPublisher extends Recorder implements SimpleBuildStep {

    private String featuresDir;

    private final boolean numbered;

    private final boolean sectAnchors;

    private final TocType toc;

    private final FormatType format;

    private String title;

    private final boolean hideFeaturesSection;

    private final boolean hideSummary;

    private final boolean hideScenarioKeyword;

    private final boolean hideStepTime;

    private final boolean hideTags;

    private PrintStream logger;

    @DataBoundConstructor
    public CukedoctorPublisher(String featuresDir, FormatType format, TocType toc, boolean numbered,
            boolean sectAnchors, String title, boolean hideFeaturesSection, boolean hideSummary,
            boolean hideScenarioKeyword, boolean hideStepTime, boolean hideTags) {
        this.featuresDir = featuresDir;
        this.numbered = numbered;
        this.toc = toc;
        this.format = format;
        this.sectAnchors = sectAnchors;
        this.title = title;
        this.hideFeaturesSection = hideFeaturesSection;
        this.hideSummary = hideSummary;
        this.hideScenarioKeyword = hideScenarioKeyword;
        this.hideStepTime = hideStepTime;
        this.hideTags = hideTags;
    }

    @Override
    public void perform(Run<?, ?> build, FilePath workspace, Launcher launcher, TaskListener listener)
            throws IOException, InterruptedException {

        FilePath workspaceJsonSourceDir;//most of the time on slave
        FilePath workspaceJsonTargetDir;//always on master
        if (!hasText(featuresDir)) {
            workspaceJsonSourceDir = workspace;
            workspaceJsonTargetDir = getMasterWorkspaceDir(build);
        } else {
            workspaceJsonSourceDir = new FilePath(workspace, featuresDir);
            workspaceJsonTargetDir = new FilePath(getMasterWorkspaceDir(build), featuresDir);
        }

        logger = listener.getLogger();
        workspaceJsonSourceDir.copyRecursiveTo("**/*.json,**/cukedoctor-intro.adoc,**/cukedoctor.properties",
                workspaceJsonTargetDir);

        System.setProperty("INTRO_CHAPTER_DIR", workspaceJsonTargetDir.getRemote());

        logger.println("");
        logger.println("Generating living documentation for " + build.getFullDisplayName()
                + " with the following arguments: ");
        logger.println("Features dir: " + workspaceJsonSourceDir.getRemote());
        logger.println("Format: " + format.getFormat());
        logger.println("Toc: " + toc.getToc());
        logger.println("Title: " + title);
        logger.println("Numbered: " + Boolean.toString(numbered));
        logger.println("Section anchors: " + Boolean.toString(sectAnchors));
        logger.println("Hide features section: " + Boolean.toString(hideFeaturesSection));
        logger.println("Hide summary: " + Boolean.toString(hideSummary));
        logger.println("Hide scenario keyword: " + Boolean.toString(hideScenarioKeyword));
        logger.println("Hide step time: " + Boolean.toString(hideStepTime));
        logger.println("Hide tags: " + Boolean.toString(hideTags));
        logger.println("");

        Result result = Result.SUCCESS;
        List<Feature> features = FeatureParser.findAndParse(workspaceJsonTargetDir.getRemote());
        if (!features.isEmpty()) {
            if (!hasText(title)) {
                title = "Living Documentation";
            }

            logger.println("Found " + features.size() + " feature(s)...");

            File targetBuildDirectory = new File(build.getRootDir(), CukedoctorBaseAction.BASE_URL);
            if (!targetBuildDirectory.exists()) {
                boolean created = targetBuildDirectory.mkdirs();
                if (!created) {
                    listener.error("Could not create file at location: " + targetBuildDirectory.getAbsolutePath());
                    result = Result.UNSTABLE;
                }
            }

            GlobalConfig globalConfig = GlobalConfig.getInstance();
            DocumentAttributes documentAttributes = globalConfig.getDocumentAttributes().backend(format.getFormat())
                    .toc(toc.getToc()).numbered(numbered).sectAnchors(sectAnchors).docTitle(title);

            globalConfig.getLayoutConfig().setHideFeaturesSection(hideFeaturesSection);

            globalConfig.getLayoutConfig().setHideSummarySection(hideSummary);

            globalConfig.getLayoutConfig().setHideScenarioKeyword(hideScenarioKeyword);

            globalConfig.getLayoutConfig().setHideStepTime(hideStepTime);

            globalConfig.getLayoutConfig().setHideTags(hideTags);

            String outputPath = targetBuildDirectory.getAbsolutePath();
            CukedoctorBuildAction action = new CukedoctorBuildAction(build);
            final ExecutorService pool = Executors.newFixedThreadPool(4);
            if ("all".equals(format.getFormat())) {
                File allHtml = new File(
                        outputPath + System.getProperty("file.separator") + CukedoctorBaseAction.ALL_DOCUMENTATION);
                if (!allHtml.exists()) {
                    boolean created = allHtml.createNewFile();
                    if (!created) {
                        listener.error("Could not create file at location: " + allHtml.getAbsolutePath());
                        result = Result.UNSTABLE;
                    }
                }
                InputStream is = null;
                OutputStream os = null;
                try {
                    is = getClass().getResourceAsStream("/" + CukedoctorBaseAction.ALL_DOCUMENTATION);
                    os = new FileOutputStream(allHtml);

                    int copyResult = IOUtils.copy(is, os);
                    if (copyResult == -1) {
                        listener.error("File is too big.");//will never reach here but findbugs forced it...
                        result = Result.UNSTABLE;
                    }
                } finally {
                    if (is != null) {
                        is.close();
                    }
                    if (os != null) {
                        os.close();
                    }
                }

                action.setDocumentationPage(CukedoctorBaseAction.ALL_DOCUMENTATION);
                pool.execute(runAll(features, documentAttributes, outputPath));
            } else {
                action.setDocumentationPage("documentation." + format.getFormat());
                pool.execute(run(features, documentAttributes, outputPath));
            }

            build.addAction(action);
            pool.shutdown();
            try {
                if (format.equals(FormatType.HTML)) {
                    pool.awaitTermination(5, TimeUnit.MINUTES);
                } else {
                    pool.awaitTermination(15, TimeUnit.MINUTES);
                }
            } catch (final InterruptedException e) {
                Thread.interrupted();
                listener.error(
                        "Your documentation is taking too long to be generated. Halting the generation now to not throttle Jenkins.");
                result = Result.FAILURE;
            }

            if (result.equals(Result.SUCCESS)) {
                listener.hyperlink("../" + CukedoctorBaseAction.BASE_URL, "Documentation generated successfully!");
                logger.println("");
            }

        } else {
            logger.println(String.format("No features Found in %s. %sLiving documentation will not be generated.",
                    workspaceJsonTargetDir.getRemote(), "\n"));

        }

        build.setResult(result);
    }

    /**
     * mainly for findbugs be happy
     * @param build
     * @return
     */
    private FilePath getMasterWorkspaceDir(Run<?, ?> build) {
        if (build != null && build.getRootDir() != null) {
            return new FilePath(build.getRootDir());
        } else {
            return new FilePath(Paths.get("").toFile());
        }
    }

    /**
     * generates html and pdf documentation 'inlined' otherwise if we execute them in separated threads
     * only the last thread content is rendered (cause they work on the same adoc file)
     *
     * @return
     */
    private Runnable runAll(final List<Feature> features, final DocumentAttributes attrs, final String outputPath) {
        return new Runnable() {

            @Override
            public void run() {

                Asciidoctor asciidoctor = null;
                try {
                    /*
                     * this throws: ERROR: org.jruby.exceptions.RaiseException: (LoadError) no such file to load -- jruby/java
                     * asciidoctor = Asciidoctor.Factory.create();
                     */
                    asciidoctor = Asciidoctor.Factory.create(CukedoctorPublisher.class.getClassLoader());
                    attrs.backend("html5");
                    generateDocumentation(features, attrs, outputPath, asciidoctor);
                    attrs.backend("pdf");
                    generateDocumentation(features, attrs, outputPath, asciidoctor);

                } catch (Exception e) {
                    logger.println(
                            String.format("Unexpected error on documentation generation, message %s, cause %s",
                                    e.getMessage(), e.getCause()));
                    e.printStackTrace();
                } finally {
                    if (asciidoctor != null) {
                        asciidoctor.shutdown();
                    }
                }
            }
        };

    }

    private Runnable run(final List<Feature> features, final DocumentAttributes attrs, final String outputPath) {
        return new Runnable() {

            @Override
            public void run() {
                Asciidoctor asciidoctor = null;
                try {
                    asciidoctor = Asciidoctor.Factory.create(CukedoctorPublisher.class.getClassLoader());
                    generateDocumentation(features, attrs, outputPath, asciidoctor);
                } catch (Exception e) {
                    logger.println(
                            String.format("Unexpected error on documentation generation, message %s, cause %s",
                                    e.getMessage(), e.getCause()));
                    e.printStackTrace();
                } finally {
                    if (asciidoctor != null) {
                        asciidoctor.shutdown();
                    }
                }
            }
        };

    }

    protected synchronized void generateDocumentation(List<Feature> features, DocumentAttributes attrs,
            String outputPath, Asciidoctor asciidoctor) {
        asciidoctor.unregisterAllExtensions();
        if (attrs.getBackend().equalsIgnoreCase("pdf")) {
            attrs.pdfTheme(true).docInfo(false);
        } else {
            attrs.docInfo(true).pdfTheme(false);
            new CukedoctorExtensionRegistry().register(asciidoctor);
        }
        CukedoctorConverter converter = Cukedoctor.instance(features, attrs);
        String doc = converter.renderDocumentation();
        File adocFile = FileUtil.saveFile(outputPath + "/documentation.adoc", doc);
        asciidoctor.convertFile(adocFile,
                OptionsBuilder.options().backend(attrs.getBackend()).safe(SafeMode.UNSAFE).asMap());
    }

    @Override
    public BuildStepMonitor getRequiredMonitorService() {
        return BuildStepMonitor.NONE;
    }

    @Extension
    public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {

        public boolean isApplicable(Class<? extends AbstractProject> aClass) {
            // Indicates that this builder can be used with all kinds of project types
            return true;
        }

        /**
         * This human readable name is used in the configuration screen.
         */
        public String getDisplayName() {
            return "Living documentation";
        }

        public ListBoxModel doFillTocItems() {
            ListBoxModel items = new ListBoxModel();
            for (TocType tocType : TocType.values()) {
                items.add(tocType.getToc(), tocType.name());
            }
            return items;
        }

        public ListBoxModel doFillFormatItems() {
            ListBoxModel items = new ListBoxModel();
            for (FormatType formatType : FormatType.values()) {
                items.add(formatType.getFormat(), formatType.name());
            }
            return items;
        }

    }

    public String getFeaturesDir() {
        return featuresDir;
    }

    public boolean isNumbered() {
        return numbered;
    }

    public boolean isSectAnchors() {
        return sectAnchors;
    }

    public TocType getToc() {
        return toc;
    }

    public FormatType getFormat() {
        return format;
    }

    public String getTitle() {
        return title;
    }

    public boolean isHideFeaturesSection() {
        return hideFeaturesSection;
    }

    public boolean isHideSummary() {
        return hideSummary;
    }

    public boolean isHideScenarioKeyword() {
        return hideScenarioKeyword;
    }

    public boolean isHideStepTime() {
        return hideStepTime;
    }

    public boolean isHideTags() {
        return hideTags;
    }
}