hudson.plugins.jobConfigHistory.JobConfigHistoryRootAction.java Source code

Java tutorial

Introduction

Here is the source code for hudson.plugins.jobConfigHistory.JobConfigHistoryRootAction.java

Source

/*
 * The MIT License
 *
 * Copyright 2013 Stefan Brausch, Mirko Friedenhagen.
 *
 * 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 hudson.plugins.jobConfigHistory;

import hudson.Extension;
import hudson.XmlFile;
import hudson.model.AbstractItem;
import hudson.model.Api;
import hudson.model.Item;
import hudson.model.Project;
import hudson.model.RootAction;
import hudson.model.TopLevelItem;
import hudson.plugins.jobConfigHistory.SideBySideView.Line;
import hudson.security.AccessControlled;
import hudson.security.Permission;
import hudson.util.MultipartFormDataParser;

import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static java.util.logging.Level.*;

import java.util.logging.Logger;
import javax.servlet.ServletException;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;

import org.apache.commons.io.FileUtils;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.bind.JavaScriptMethod;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.export.Exported;

import org.kohsuke.stapler.QueryParameter;
import hudson.util.FormValidation;

/**
 * Get config history needed to rollback to specific commit and
 * set commit message
 * @author Stefan Brausch
 * @author Mirko Friedenhagen
 */

@ExportedBean(defaultVisibility = -1)
@Extension
public class JobConfigHistoryRootAction extends JobConfigHistoryBaseAction implements RootAction {

    /** Our logger. */
    private static final Logger LOG = Logger.getLogger(JobConfigHistoryRootAction.class.getName());

    /**
     * Constructor necessary for testing.
     */
    public JobConfigHistoryRootAction() {
        super();
    }

    /**
     * {@inheritDoc}
     *
     * This actions always starts from the context directly, so prefix
     * {@link JobConfigHistoryConsts#URLNAME} with a slash.
     */
    @Override
    public final String getUrlName() {
        return "/" + JobConfigHistoryConsts.URLNAME;
    }

    /**
     * {@inheritDoc}
     *
     * Make method final, as we always want the same icon file. Returns
     * {@literal null} to hide the icon if the user is not allowed to configure
     * jobs.
     */
    public final String getIconFileName() {
        if (hasConfigurePermission() || hasJobConfigurePermission() || hasReadExtensionPermission()) {
            return JobConfigHistoryConsts.ICONFILENAME;
        } else {
            return null;
        }
    }

    /**
     * Returns the configuration history entries for either {@link AbstractItem}
     * s or system changes or deleted jobs or all of the above.
     *
     * @return list of configuration histories (as ConfigInfo)
     * @throws IOException
     *             if one of the history entries might not be read.
     */
    @Exported(visibility = 1)
    public final List<ConfigInfo> getConfigs() throws IOException {
        final String filter = getRequestParameter("filter");
        List<ConfigInfo> configs = null;

        if (filter == null || "system".equals(filter)) {
            configs = getSystemConfigs();
        } else if ("all".equals(filter)) {
            configs = getJobConfigs("jobs");
            configs.addAll(getJobConfigs("deleted"));
            configs.addAll(getSystemConfigs());
        } else {
            configs = getJobConfigs(filter);
        }

        Collections.sort(configs, ParsedDateComparator.DESCENDING);
        return configs;
    }

    /**
     * Returns revision history
     *
     * @return list of revisiosn
     */
    List<ConfigInfo> getRevHistory() throws IOException {
        //String,boolean,String,String,String,String,boolean
        List<ConfigInfo> revisions = new ArrayList<ConfigInfo>();
        HistoryDescr test_desc = new HistoryDescr("Darko", "2", "Add", "2015-11-01_16-18-49");
        ConfigInfo test_config = ConfigInfo.create("Darko", false, test_desc, false);
        //test_config.create("Darko", false, test ,false);
        revisions.add(test_config);
        return revisions;
    }

    /**
     * Returns the configuration history entries for all system files in this
     * Hudson instance.
     *
     * @return List of config infos.
     * @throws IOException
     *             if one of the history entries might not be read.
     */
    List<ConfigInfo> getSystemConfigs() throws IOException {
        final List<ConfigInfo> configs = new ArrayList<ConfigInfo>();
        if (!hasConfigurePermission()) {
            return configs;
        }

        final File[] itemDirs = getOverviewHistoryDao().getSystemConfigs();
        for (final File itemDir : itemDirs) {
            final String itemName = itemDir.getName();
            configs.addAll(HistoryDescrToConfigInfo.convert(itemName, true,
                    getOverviewHistoryDao().getSystemHistory(itemName).values(), false));

        }
        return configs;
    }

    /**
     * Returns the configuration history entries for all jobs or deleted jobs in
     * this Hudson instance.
     *
     * @param type
     *            Whether we want to see all jobs or just the deleted jobs.
     * @return List of config infos.
     * @throws IOException
     *             if one of the history entries might not be read.
     */
    List<ConfigInfo> getJobConfigs(String type) throws IOException {
        if (!hasJobConfigurePermission() && !hasReadExtensionPermission()) {
            return Collections.EMPTY_LIST;
        } else {
            return new ConfigInfoCollector(type, getOverviewHistoryDao()).collect("");
        }
    }

    /**
     * Returns the configuration history entries for one group of system files
     * or deleted jobs.
     *
     * @param name
     *            of the item.
     * @return Configs list for one group of system configuration files.
     * @throws IOException
     *             if one of the history entries might not be read.
     */
    public final List<ConfigInfo> getSingleConfigs(String name) throws IOException {
        final Collection<HistoryDescr> historyDescriptions;
        if (name.contains(JobConfigHistoryConsts.DELETED_MARKER)) {
            historyDescriptions = getOverviewHistoryDao().getJobHistory(name).values();
        } else {
            historyDescriptions = getOverviewHistoryDao().getSystemHistory(name).values();
        }
        final List<ConfigInfo> configs = HistoryDescrToConfigInfo.convert(name, true, historyDescriptions, false);
        Collections.sort(configs, ParsedDateComparator.DESCENDING);
        return configs;
    }

    /**
     * Returns {@link JobConfigHistoryBaseAction#getConfigXml(String)} as
     * String.
     *
     * @return content of the {@literal config.xml} found in directory given by
     *         the request parameter {@literal file}.
     * @throws IOException
     *             if the config file could not be read or converted to an xml
     *             string.
     */
    public final String getFile() throws IOException {
        final String name = getRequestParameter("name");
        if ((name.contains(JobConfigHistoryConsts.DELETED_MARKER) && hasJobConfigurePermission())
                || hasConfigurePermission()) {
            final String timestamp = getRequestParameter("timestamp");
            final XmlFile xmlFile = getOldConfigXml(name, timestamp);
            return xmlFile.asString();
        } else {
            return "No permission to view config files";
        }
    }

    /**
     * Creates links to the correct configOutput.jellys for job history vs.
     * system history and for xml vs. plain text.
     *
     * @param config
     *            ConfigInfo.
     * @param type
     *            Output type ('xml' or 'plain').
     * @return The link as String.
     */
    public final String createLinkToFiles(ConfigInfo config, String type) {
        String link = null;
        final String name = config.getJob();
        String timestamp = config.getDate();

        if (name.contains(JobConfigHistoryConsts.DELETED_MARKER)) {
            // last config.xml for deleted job usually doesn't exist
            try {
                if (getSingleConfigs(name).size() > 1) {
                    timestamp = getSingleConfigs(name).get(1).getDate();
                    link = "configOutput?type=" + type + "&name=" + name + "&timestamp=" + timestamp;
                }
            } catch (IOException ex) {
                LOG.log(FINEST, "Unable to get config for {0}", name);
            }
        } else if (config.getIsJob()) {
            link = getHudson().getRootUrl() + "job/" + name + getUrlName() + "/configOutput?type=" + type
                    + "&timestamp=" + timestamp;
        } else {
            link = "configOutput?type=" + type + "&name=" + name + "&timestamp=" + timestamp;
        }

        return link;
    }

    @Override
    protected AccessControlled getAccessControlledObject() {
        return getHudson();
    }

    @Override
    protected void checkConfigurePermission() {
        getAccessControlledObject().checkPermission(Permission.CONFIGURE);
    }

    /**
     * Returns whetheror not the user may have permission
     *
     * @return true if the current user may configure jobs.
     */
    @Override
    public boolean hasConfigurePermission() {
        return getAccessControlledObject().hasPermission(Permission.CONFIGURE);
    }

    /**
     * Returns whether the current user may configure jobs.
     *
     * @return true if the current user may configure jobs.
     */
    public boolean hasJobConfigurePermission() {
        return getAccessControlledObject().hasPermission(Item.CONFIGURE);
    }

    /**
     * Returns whether the current user may read configure jobs.
     *
     * @return true if the current user may read configure jobs.
     */
    public boolean hasReadExtensionPermission() {
        return getAccessControlledObject().hasPermission(Item.EXTENDED_READ);
    }

    /**
     * Parses the incoming {@literal POST} request and redirects as
     * {@literal GET showDiffFiles}.
     *
     * @param req
     *            incoming request
     * @param rsp
     *            outgoing response
     * @throws ServletException
     *             when parsing the request as {@link MultipartFormDataParser}
     *             does not succeed.
     * @throws IOException
     *             when the redirection does not succeed.
     */
    @Override
    public final void doDiffFiles(StaplerRequest req, StaplerResponse rsp) throws ServletException, IOException {
        final MultipartFormDataParser parser = new MultipartFormDataParser(req);
        rsp.sendRedirect("showDiffFiles?name=" + parser.get("name") + "&timestamp1=" + parser.get("timestamp1")
                + "&timestamp2=" + parser.get("timestamp2"));
    }

    /**
     * Returns the diff between two config files as a list of single lines.
     * Takes the two timestamps and the name of the system property or the
     * deleted job from the url parameters.
     *
     * @return Differences between two config versions as list of lines.
     * @throws IOException
     *             If diff doesn't work or xml files can't be read.
     */
    public final List<Line> getLines() throws IOException {
        final String name = getRequestParameter("name");
        if ((name.contains(JobConfigHistoryConsts.DELETED_MARKER) && hasJobConfigurePermission())
                || hasConfigurePermission()) {
            final String timestamp1 = getRequestParameter("timestamp1");
            final String timestamp2 = getRequestParameter("timestamp2");

            final XmlFile configXml1 = getOldConfigXml(name, timestamp1);
            final String[] configXml1Lines = configXml1.asString().split("\\n");
            final XmlFile configXml2 = getOldConfigXml(name, timestamp2);
            final String[] configXml2Lines = configXml2.asString().split("\\n");

            final String diffAsString = getDiffAsString(configXml1.getFile(), configXml2.getFile(), configXml1Lines,
                    configXml2Lines);

            final List<String> diffLines = Arrays.asList(diffAsString.split("\n"));
            return getDiffLines(diffLines);
        } else {
            return Collections.emptyList();
        }
    }

    /**
     * Gets the version of the config.xml that was saved at a certain time.
     *
     * @param name
     *            The name of the system property or deleted job.
     * @param timestamp
     *            The timestamp as String.
     * @return The config file as XmlFile.
     */
    protected XmlFile getOldConfigXml(String name, String timestamp) {
        if (checkParameters(name, timestamp)) {
            if (name.contains(JobConfigHistoryConsts.DELETED_MARKER)) {
                return getHistoryDao().getOldRevision("jobs/" + name, timestamp);
            } else {
                if (!hasConfigurePermission() && !hasReadExtensionPermission() && !hasJobConfigurePermission()) {
                    checkConfigurePermission();
                    return null;
                }
                return getHistoryDao().getOldRevision(name, timestamp);
            }
        } else {
            throw new IllegalArgumentException("Unable to get history from: " + name);
        }
    }

    /**
     * Checks the url parameters 'name' and 'timestamp' and returns true if they
     * are neither null nor suspicious.
     *
     * @param name
     *            Name of deleted job or system property.
     * @param timestamp
     *            Timestamp of config change.
     * @return True if parameters are okay.
     */
    boolean checkParameters(String name, String timestamp) {
        checkTimestamp(timestamp);
        if (name == null || "null".equals(name)) {
            return false;
        }
        if (name.contains("..")) {
            throw new IllegalArgumentException("Invalid directory name because of '..': " + name);
        }
        return true;
    }

    /**
     * Action when 'restore' button is pressed: Restore deleted project.
     *
     * @param req Incoming StaplerRequest
     * @param rsp Outgoing StaplerResponse
     * @throws IOException If something goes wrong
     */
    public final void doRestore(StaplerRequest req, StaplerResponse rsp) throws IOException {
        getAccessControlledObject().checkPermission(Item.CONFIGURE);

        final String deletedName = req.getParameter("name");
        final String newName = deletedName.split("_deleted_")[0];

        final XmlFile configXml = getLastAvailableConfigXml(deletedName);

        final InputStream is = new ByteArrayInputStream(configXml.asString().getBytes("UTF-8"));
        final String calculatedNewName = findNewName(newName);
        final TopLevelItem project = getHudson().createProjectFromXML(calculatedNewName, is);
        // TODO: Casting here should be removed.
        ((FileHistoryDao) getHistoryDao()).copyHistoryAndDelete(deletedName, calculatedNewName);

        rsp.sendRedirect(getHudson().getRootUrl() + project.getUrl());
    }

    //function to do rollback
    //when you actually 
    public final void doRollback(StaplerRequest req, StaplerResponse rsp) throws IOException {

        String name = req.getParameter("name");
        String timestamp = req.getParameter("timestamp");
        String url = req.getParameter("url");

        XmlFile xmlFile = getOldConfigXml("jobs/" + name, timestamp);
        if (xmlFile != null) {
            String path_copy_to = System.getenv("HOME") + "/.jenkins/jobs/" + name + "/config.xml";

            FileWriter wr = new FileWriter(path_copy_to);

            try {
                BufferedWriter bufferedWriter = new BufferedWriter(wr);
                bufferedWriter.write(xmlFile.asString());
                bufferedWriter.close();
            } catch (Exception e) {

            }

            @SuppressWarnings("deprecation")
            AbstractItem project = (AbstractItem) getHudson().getJob(name);
            final InputStream is = new ByteArrayInputStream(xmlFile.asString().getBytes("UTF-8"));
            project.updateByXml((Source) new StreamSource(is));
            project.save();

            rsp.sendRedirect(url);
        } else
            throw new IllegalArgumentException("Non existent timestamp " + timestamp);
    }

    public final void doChangeCommitMessage(StaplerRequest req, StaplerResponse rsp) throws IOException {
        getAccessControlledObject().checkPermission(Item.CONFIGURE);

        final String commitMessage = req.getParameter("cmsg");
        final String commitTags = req.getParameter("ctags");

        PluginUtils.getPlugin().setCommitMessageAndTags(commitMessage, commitTags);
        rsp.sendRedirect(getHudson().getRootUrl());
    }

    /**
     * Retrieves the last or second to last config.xml.
     * The latter is necessary when the last config.xml is missing
     * although the history entry exists, which happens when a project is deleted
     * while being disabled.
     *
     * @param name The name of the deleted project.
     * @return The last or second to last config as XmlFile or null.
     */
    public XmlFile getLastAvailableConfigXml(String name) {
        XmlFile configXml = null;
        final List<ConfigInfo> configInfos;
        try {
            configInfos = getSingleConfigs(name);
        } catch (IOException ex) {
            LOG.log(FINEST, "Unable to get config history for {0}", name);
            return configXml;
        }

        if (configInfos.size() > 1) {
            Collections.sort(configInfos, ParsedDateComparator.DESCENDING);
            final ConfigInfo lastChange = configInfos.get(1);
            configXml = getOldConfigXml(name, lastChange.getDate());
        }

        return configXml;
    }

    /**
     * Finds a name for the project to be restored.
     * If the old name is already in use by another project,
     * "_" plus a number is appended to the name until an unused name is found.
     *
     * @param name The old name as String.
     * @return the new name as String.
     */
    String findNewName(String name) {
        String newName = name;
        int i = 1;
        while (getHudson().getItem(newName) != null) {
            newName = name + "_" + String.valueOf(i);
            i++;
        }
        return newName;
    }

    /**
     * Action when 'restore' button in history.jelly is pressed.
     * Gets required parameter and forwards to restoreQuestion.jelly.
     * @param req StaplerRequest created by pressing the button
     * @param rsp Outgoing StaplerResponse
     * @throws IOException If redirect goes wrong
     */
    public final void doForwardToRestoreQuestion(StaplerRequest req, StaplerResponse rsp) throws IOException {
        final String name = req.getParameter("name");
        rsp.sendRedirect("restoreQuestion?name=" + name);
    }

    /**
     * Return history dao for tests.
     *
     * @return historyDao
     */
    HistoryDao getHistoryDao() {
        return PluginUtils.getHistoryDao();
    }

    /**
     * Return overview history dao for tests.
     *
     * @return historyDao
     */
    OverviewHistoryDao getOverviewHistoryDao() {
        return PluginUtils.getHistoryDao();
    }

    /**
     * getApi()
     * Needed in order to correctly use jelly and use function 
     * getDisplayName()
     * @param  none
     * returns a new API object
     */
    public Api getApi() {
        return new Api(this);
    }

    /**
     * Validates the input of the form data
     * @param cmsg commit message to verify
     * @param ctags commit tag to verify
     * @return success if input is fine, error otherwise
     */
    public FormValidation doSubmitMessage(@QueryParameter("cmsg") final String cmsg,
            @QueryParameter("ctags") final String ctags) throws IOException, ServletException {
        try {
            return FormValidation.ok("Success");
        } catch (Exception e) {
            return FormValidation.error("Client error : " + e.getMessage());
        }
    }
}