jp.ikedam.jenkins.plugins.viewcopy_builder.ViewcopyBuilder.java Source code

Java tutorial

Introduction

Here is the source code for jp.ikedam.jenkins.plugins.viewcopy_builder.ViewcopyBuilder.java

Source

/*
 * The MIT License
 * 
 * Copyright (c) 2013 IKEDA Yasuyuki
 * 
 * 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 jp.ikedam.jenkins.plugins.viewcopy_builder;

import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintStream;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.List;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import jenkins.model.Jenkins;

import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
import org.w3c.dom.Document;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

import com.thoughtworks.xstream.io.xml.DomDriver;

import hudson.DescriptorExtensionList;
import hudson.EnvVars;
import hudson.Extension;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.AllView;
import hudson.model.BuildListener;
import hudson.model.Descriptor;
import hudson.model.View;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.Builder;
import hudson.util.ComboBoxModel;
import hudson.util.FormValidation;
import hudson.util.XStream2;

/**
 * A build step to copy a view.
 * 
 * You can specify additional operations that is performed when copying,
 * and the operations can be extended with plugins using Extension Points.
 */
public class ViewcopyBuilder extends Builder implements Serializable {
    private static final long serialVersionUID = 1598118718029050622L;

    private String fromViewName;

    /**
     * Returns the name of view to be copied from.
     * 
     * Variable expressions will be expanded.
     * 
     * @return the name of view to be copied from
     */
    public String getFromViewName() {
        return fromViewName;
    }

    private String toViewName;

    /**
     * Returns the name of view to be copied to.
     * 
     * Variable expressions will be expanded.
     * 
     * @return the name of view to be copied to
     */
    public String getToViewName() {
        return toViewName;
    }

    private boolean overwrite = false;

    /**
     * Returns whether to overwrite an existing view.
     * 
     * If the copied-to view is already exists,
     * "copy view" build step works as following depending on this value.
     * <table>
     *     <tr>
     *         <th>isOverwrite</th>
     *         <th>behavior</th>
     *     </tr>
     *     <tr>
     *         <td>true</td>
     *         <td>Overwrite the configuration of the existing view.</td>
     *     </tr>
     *     <tr>
     *         <td>false</td>
     *         <td>Build fails.</td>
     *     </tr>
     * </table>
     * 
     * @return whether to overwrite an existing view.
     */
    public boolean isOverwrite() {
        return overwrite;
    }

    private List<ViewcopyOperation> viewcopyOperationList;

    /**
     * Returns the list of operations.
     * 
     * @return the list of operations
     */
    public List<ViewcopyOperation> getViewcopyOperationList() {
        return viewcopyOperationList;
    }

    /**
     * Constructor to instantiate from parameters in the job configuration page.
     * 
     * When instantiating from the saved configuration,
     * the object is directly serialized with XStream,
     * and no constructor is used.
     * 
     * @param fromViewName   a name of a view to be copied from. may contains variable expressions.
     * @param toViewName     a name of a view to be copied to. may contains variable expressions.
     * @param overwrite     whether to overwrite if the view to be copied to is already existing.
     * @param viewcopyOperationList
     *                      the list of operations to be performed when copying.
     */
    @DataBoundConstructor
    public ViewcopyBuilder(String fromViewName, String toViewName, boolean overwrite,
            List<ViewcopyOperation> viewcopyOperationList) {
        this.fromViewName = StringUtils.trim(fromViewName);
        this.toViewName = StringUtils.trim(toViewName);
        this.overwrite = overwrite;
        this.viewcopyOperationList = viewcopyOperationList;
    }

    /**
     * Performs the build step.
     * 
     * @param build
     * @param launcher
     * @param listener
     * @return  whether the process succeeded.
     * @throws IOException
     * @throws InterruptedException
     * @see hudson.tasks.BuildStepCompatibilityLayer#perform(hudson.model.AbstractBuild, hudson.Launcher, hudson.model.BuildListener)
     */
    @Override
    public boolean perform(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener)
            throws IOException, InterruptedException {
        EnvVars env = build.getEnvironment(listener);
        PrintStream logger = listener.getLogger();

        if (StringUtils.isBlank(getFromViewName())) {
            logger.println("From View Name is not specified");
            return false;
        }
        if (StringUtils.isBlank(getToViewName())) {
            logger.println("To View Name is not specified");
            return false;
        }

        // Expand the variable expressions in view names.
        String fromViewNameExpanded = env.expand(getFromViewName());
        String toViewNameExpanded = env.expand(getToViewName());

        if (StringUtils.isBlank(fromViewNameExpanded)) {
            logger.println("From View Name got to a blank");
            return false;
        }
        if (StringUtils.isBlank(toViewNameExpanded)) {
            logger.println("To View Name got to a blank");
            return false;
        }

        logger.println(String.format("Copying %s to %s", fromViewNameExpanded, toViewNameExpanded));

        // Retrieve the view to be copied from.
        View fromView = Jenkins.getInstance().getView(fromViewNameExpanded);

        if (fromView == null) {
            logger.println("Error: Copying view is not found.");
            return false;
        }

        // Check whether the view to be copied to is already exists.
        View toView = Jenkins.getInstance().getView(toViewNameExpanded);
        if (toView != null) {
            logger.println(String.format("Already exists: %s", toViewNameExpanded));
            if (!isOverwrite()) {
                return false;
            }
        }

        // Create the config.xml of the view copied from.
        logger.println(String.format("Fetching configuration of %s...", fromViewNameExpanded));

        Document doc;
        try {
            doc = getViewConfigXmlDocument(fromView, logger);
        } catch (Exception e) {
            logger.println("Failed to retrieve configuration.");
            e.printStackTrace(logger);
            return false;
        }

        try {
            String xml = getDocumentXmlString(doc);
            logger.println("Original xml:");
            logger.println(xml);
        } catch (Exception e) {
            logger.println("Failed to convert the configuration to a string.");
            e.printStackTrace(logger);
            return false;
        }

        // Apply additional operations to the retrieved XML.
        if (getViewcopyOperationList() != null) {
            for (ViewcopyOperation operation : getViewcopyOperationList()) {
                if (!operation.isApplicable(fromView.getClass())) {
                    logger.println(String.format("Operation %s cannot be applicable to %s(%s)",
                            operation.getClass().getName(), fromView.getViewName(), fromView.getClass().getName()));
                    return false;
                }
                doc = operation.perform(doc, env, logger);
                if (doc == null) {
                    return false;
                }
            }
        }

        try {
            String xml = getDocumentXmlString(doc);
            logger.println("Copied xml:");
            logger.println(xml);
        } catch (Exception e) {
            logger.println("Failed to convert the configuration to a string.");
            e.printStackTrace(logger);
            return false;
        }

        InputStream sin;
        try {
            sin = getInputStreamFromDocument(doc);
        } catch (Exception e) {
            logger.println("Failed to create the stream from converted document.");
            e.printStackTrace(logger);
            return false;
        }

        if (toView == null) {
            logger.println(String.format("Creating %s", toViewNameExpanded));
            toView = View.createViewFromXML(toViewNameExpanded, sin);
            Jenkins.getInstance().addView(toView);
        } else {
            logger.println(String.format("Updating %s", toViewNameExpanded));
            toView.updateByXml(new StreamSource(sin));
        }

        // add the information of views copied from and to to the build.
        build.addAction(new CopiedviewinfoAction(fromView, toView));

        return true;
    }

    /**
     * Returns the configuration XML document of a view
     * 
     * @param view
     * @param logger
     * @return
     * @throws IOException 
     * @throws SAXException 
     * @throws ParserConfigurationException 
     */
    private Document getViewConfigXmlDocument(View view, final PrintStream logger)
            throws IOException, SAXException, ParserConfigurationException {
        XStream2 xStream2 = new XStream2(new DomDriver("UTF-8"));
        xStream2.omitField(View.class, "owner");
        xStream2.omitField(View.class, "name"); // this field causes disaster when overwriting.

        PipedOutputStream sout = new PipedOutputStream();
        PipedInputStream sin = new PipedInputStream(sout);

        xStream2.toXML(view, sout);
        sout.close();

        DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = domFactory.newDocumentBuilder();
        builder.setErrorHandler(new ErrorHandler() {
            @Override
            public void warning(SAXParseException exception) throws SAXException {
                exception.printStackTrace(logger);
            }

            @Override
            public void error(SAXParseException exception) throws SAXException {
                exception.printStackTrace(logger);
            }

            @Override
            public void fatalError(SAXParseException exception) throws SAXException {
                exception.printStackTrace(logger);
            }
        });
        return builder.parse(sin);
    }

    /**
     * Returns a InputStream of a XML document.
     * 
     * @param doc
     * @return
     * @throws TransformerException
     * @throws IOException
     */
    private InputStream getInputStreamFromDocument(Document doc) throws TransformerException, IOException {
        TransformerFactory tfactory = TransformerFactory.newInstance();
        Transformer transformer = tfactory.newTransformer();
        PipedOutputStream sout = new PipedOutputStream();
        PipedInputStream sin = new PipedInputStream(sout);
        transformer.transform(new DOMSource(doc), new StreamResult(sout));
        sout.close();

        return sin;
    }

    /**
     * Returns a XML string representing the passed XML document.
     * 
     * @param doc
     * @return
     * @throws TransformerException
     */
    private String getDocumentXmlString(Document doc) throws TransformerException {
        TransformerFactory tfactory = TransformerFactory.newInstance();
        Transformer transformer = tfactory.newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");

        StringWriter sw = new StringWriter();
        transformer.transform(new DOMSource(doc), new StreamResult(sw));

        return sw.toString();
    }

    /**
     * The internal class to work with views.
     * 
     * The following files are used (put in main/resource directory in the source tree).
     * <dl>
     *     <dt>config.jelly</dt>
     *         <dd>shown as a part of a job configuration page.</dd>
     * </dl>
     */
    @Extension
    public static final class DescriptorImpl extends BuildStepDescriptor<Builder> {
        /**
         * Returns the display name
         * 
         * Displayed in the "Add build step" dropdown in a job configuration page. 
         * 
         * @return the display name
         * @see hudson.model.Descriptor#getDisplayName()
         */
        @Override
        public String getDisplayName() {
            return Messages.ViewcopyBuilder_DisplayName();
        }

        /**
         * Test whether this build step can be applied to the specified job type.
         * 
         * This build step works for any type of jobs, for always returns true.
         * 
         * @param jobType the type of the job to be tested.
         * @return true
         * @see hudson.tasks.BuildStepDescriptor#isApplicable(java.lang.Class)
         */
        @SuppressWarnings("rawtypes")
        @Override
        public boolean isApplicable(Class<? extends AbstractProject> jobType) {
            return true;
        }

        /**
         * Returns all the available ViewcopyOperation.
         * 
         * Used for the contents of "Add Copy Operation" dropdown.
         * 
         * @return the list of ViewcopyOperation
         */
        public DescriptorExtensionList<ViewcopyOperation, Descriptor<ViewcopyOperation>> getViewcopyOperationDescriptors() {
            return ViewcopyOperation.all();
        }

        /**
         * Returns the list of views.
         * 
         * Used for the autocomplete of From View Name.
         * 
         * @return the list of views
         */
        public ComboBoxModel doFillFromViewNameItems() {
            ComboBoxModel ret = FixedComboBoxModel.createComboBoxModel();
            for (View view : Jenkins.getInstance().getViews()) {
                if (view instanceof AllView) {
                    continue;
                }
                ret.add(view.getViewName());
            }

            return ret;
        }

        /**
         * Returns whether the value contains variable.
         * 
         * @param value value to be tested.
         * @return whether the value contains variable
         */
        private boolean containsVariable(String value) {
            if (StringUtils.isBlank(value) || !value.contains("$")) {
                // apparently contains no variable.
                return false;
            }

            return true;
        }

        /**
         * Validate "From View Name" or "To View Name" field.
         * 
         * Returns as following:
         * <table>
         *     <tr>
         *         <th>viewName</th>
         *         <th>warnIfExists</th>
         *         <th>warnIfNotExists</th>
         *         <th>Returns</th>
         *     </tr>
         *     <tr>
         *         <td>Blank</th>
         *         <th>any</th>
         *         <th>any</th>
         *         <td>error</td>
         *     </tr>
         *     <tr>
         *         <td>value containing variables</th>
         *         <th>any</th>
         *         <th>any</th>
         *         <td>ok</td>
         *     </tr>
         *     <tr>
         *         <td>existing view</th>
         *         <th>false</th>
         *         <th>any</th>
         *         <td>ok</td>
         *     </tr>
         *     <tr>
         *         <td>existing view</th>
         *         <th>true</th>
         *         <th>any</th>
         *         <td>warning</td>
         *     </tr>
         *     <tr>
         *         <td>non existing view</th>
         *         <th>any</th>
         *         <th>false</th>
         *         <td>ok</td>
         *     </tr>
         *     <tr>
         *         <td>non existing view</th>
         *         <th>any</th>
         *         <th>true</th>
         *         <td>warning</td>
         *     </tr>
         * </table>
         * 
         * @param viewName
         * @param warnIfExists
         * @param warnIfNotExists
         * @return
         */
        protected FormValidation doCheckViewName(String viewName, boolean warnIfExists, boolean warnIfNotExists) {
            viewName = StringUtils.trim(viewName);

            if (StringUtils.isBlank(viewName)) {
                return FormValidation.error(Messages.ViewcopyBuilder_ViewName_empty());
            }
            if (containsVariable(viewName)) {
                return FormValidation.ok();
            }

            View view = Jenkins.getInstance().getView(viewName);
            if (view != null) {
                // view exists
                if (warnIfExists) {
                    return FormValidation.warning(Messages.ViewcopyBuilder_ViewName_exists());
                }
            } else {
                // view does not exist
                if (warnIfNotExists) {
                    return FormValidation.warning(Messages.ViewcopyBuilder_ViewName_notExists());
                }
            }

            return FormValidation.ok();
        }

        /**
         * Validate "From View Name" field.
         * 
         * @param fromViewName
         * @return
         */
        public FormValidation doCheckFromViewName(@QueryParameter String fromViewName) {
            return doCheckViewName(fromViewName, false, true);
        }

        /**
         * Validate "To View Name" field.
         * 
         * @param toViewName
         * @param overwrite
         * @return FormValidation object.
         */
        public FormValidation doCheckToViewName(@QueryParameter String toViewName,
                @QueryParameter boolean overwrite) {
            return doCheckViewName(toViewName, !overwrite, false);
        }
    }
}