/*
* The MIT License
*
* Copyright 2013 Lucie Votypkova.
*
* 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 java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.SortedMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import hudson.XmlFile;
import hudson.model.Api;
import hudson.model.Computer;
import hudson.model.Node;
import hudson.model.Slave;
import hudson.plugins.jobConfigHistory.SideBySideView.Line;
import hudson.security.AccessControlled;
import jenkins.model.Jenkins;
/**
*
* @author Lucie Votypkova
*/
@ExportedBean(defaultVisibility = -1)
public class ComputerConfigHistoryAction extends JobConfigHistoryBaseAction {
/** The logger. */
private static final Logger LOG = Logger
.getLogger(ComputerConfigHistoryAction.class.getName());
/**
* The slave.
*/
private Slave slave;
/**
* The jenkins instance.
*/
private final Jenkins jenkins;
/**
* Standard constructor using instance.
*
* * @param slave Slave.
*/
public ComputerConfigHistoryAction(Slave slave) {
this.slave = slave;
jenkins = Jenkins.getInstance();
}
@Override
public final String getDisplayName() {
return Messages.agentDisplayName();
}
@Override
public String getUrlName() {
return JobConfigHistoryConsts.URLNAME;
}
/**
* Returns the slave.
*
* @return the slave.
*/
public Slave getSlave() {
return slave;
}
@Override
protected AccessControlled getAccessControlledObject() {
return slave;
}
@Override
protected void checkConfigurePermission() {
getAccessControlledObject().checkPermission(Computer.CONFIGURE);
}
@Override
public boolean hasConfigurePermission() {
return getAccessControlledObject().hasPermission(Computer.CONFIGURE);
}
@Override
public final String getIconFileName() {
if (!hasConfigurePermission()) {
return null;
}
return JobConfigHistoryConsts.ICONFILENAME;
}
/**
* Returns the configuration history entries for one {@link Slave}.
*
* @return history list for one {@link Slave}.
* @throws IOException
* if {@link JobConfigHistoryConsts#HISTORY_FILE} might not be
* read or the path might not be urlencoded.
*/
public final List<ConfigInfo> getSlaveConfigs() throws IOException {
checkConfigurePermission();
final ArrayList<ConfigInfo> configs = new ArrayList<ConfigInfo>();
final ArrayList<HistoryDescr> values = new ArrayList<HistoryDescr>(
getHistoryDao().getRevisions(slave).values());
for (final HistoryDescr historyDescr : values) {
final String timestamp = historyDescr.getTimestamp();
final XmlFile oldRevision = getHistoryDao().getOldRevision(slave,
timestamp);
if (oldRevision.getFile() != null) {
configs.add(ConfigInfo.create(slave.getNodeName(), true,
historyDescr, true));
} else if ("Deleted".equals(historyDescr.getOperation())) {
configs.add(ConfigInfo.create(slave.getNodeName(), false,
historyDescr, true));
}
}
Collections.sort(configs, ParsedDateComparator.DESCENDING);
return configs;
}
/**
* Returns the configuration history entries for one {@link Slave} for the
* REST API.
*
* @return history list for one {@link Slave}, or an empty list if not
* authorized.
* @throws IOException
* if {@link JobConfigHistoryConsts#HISTORY_FILE} might not be
* read or the path might not be urlencoded.
*/
@Exported(name = "jobConfigHistory", visibility = 1)
public final List<ConfigInfo> getSlaveConfigsREST() throws IOException {
List<ConfigInfo> configs = null;
try {
configs = getSlaveConfigs();
} catch (org.acegisecurity.AccessDeniedException e) {
configs = new ArrayList<ConfigInfo>();
}
return configs;
}
/**
* Used in the Difference jelly only. Returns one of the two timestamps that
* have been passed to the Difference page as parameter. timestampNumber
* must be 1 or 2.
*
* @param timestampNumber
* 1 for timestamp1 and 2 for timestamp2
* @return the timestamp as String.
*/
public final String getTimestamp(int timestampNumber) {
checkConfigurePermission();
String timeStamp = this
.getRequestParameter("timestamp" + timestampNumber);
SimpleDateFormat format = new java.text.SimpleDateFormat(
"yyyy-MM-dd_HH-mm-ss");
try {
format.setLenient(false);
format.parse(timeStamp);
return timeStamp;
} catch (ParseException e) {
return null;
}
}
/**
* Used in the Difference jelly only. Returns the user that made the change
* in one of the Files shown in the Difference view(A or B). timestampNumber
* decides between File A and File B.
*
* @param timestampNumber
* 1 for File A and 2 for File B
* @return the user as String.
*/
public final String getUser(int timestampNumber) {
checkConfigurePermission();
return getHistoryDao().getRevisions(this.slave)
.get(getTimestamp(timestampNumber)).getUser();
}
/**
* Used in the Difference jelly only. Returns the operation made on one of
* the two Files A and B. timestampNumber decides which file exactly.
*
* @param timestampNumber
* 1 for File A, 2 for File B
* @return the operation as String.
*/
public final String getOperation(int timestampNumber) {
checkConfigurePermission();
return getHistoryDao().getRevisions(this.slave)
.get(getTimestamp(timestampNumber)).getOperation();
}
/**
* Used in the Difference jelly only. Returns the next timestamp of the next
* entry of the two Files A and B. timestampNumber decides which file
* exactly.
*
* @param timestampNumber
* 1 for File A, 2 for File B
* @return the timestamp of the next entry as String.
*/
public final String getNextTimestamp(int timestampNumber) {
checkConfigurePermission();
final String timestamp = this
.getRequestParameter("timestamp" + timestampNumber);
final SortedMap<String, HistoryDescr> revisions = getHistoryDao()
.getRevisions(this.slave);
final Iterator<Entry<String, HistoryDescr>> itr = revisions.entrySet()
.iterator();
while (itr.hasNext()) {
if (itr.next().getValue().getTimestamp().equals((String) timestamp)
&& itr.hasNext()) {
return itr.next().getValue().getTimestamp();
}
}
// no next entry found
return timestamp;
}
/**
* Used in the Difference jelly only. Returns the previous timestamp of the
* next entry of the two Files A and B. timestampNumber decides which file
* exactly.
*
* @param timestampNumber
* 1 for File A, 2 for File B
* @return the timestamp of the preious entry as String.
*/
public final String getPrevTimestamp(int timestampNumber) {
checkConfigurePermission();
final String timestamp = this
.getRequestParameter("timestamp" + timestampNumber);
final SortedMap<String, HistoryDescr> revisions = getHistoryDao()
.getRevisions(this.slave);
final Iterator<Entry<String, HistoryDescr>> itr = revisions.entrySet()
.iterator();
String prevTimestamp = timestamp;
while (itr.hasNext()) {
final String checkTimestamp = itr.next().getValue().getTimestamp();
if (checkTimestamp.equals((String) timestamp)) {
return prevTimestamp;
} else {
prevTimestamp = checkTimestamp;
}
}
// no previous entry found
return timestamp;
}
/**
* 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 {
checkConfigurePermission();
final String timestamp = getRequestParameter("timestamp");
final XmlFile xmlFile = getOldConfigXml(timestamp);
return xmlFile.asString();
}
/**
* Takes the two timestamp request parameters and returns the diff between
* the corresponding config files of this slave as a list of single lines.
*
* @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 {
checkConfigurePermission();
final String timestamp1 = getRequestParameter("timestamp1");
final String timestamp2 = getRequestParameter("timestamp2");
final XmlFile configXml1 = getOldConfigXml(timestamp1);
final String[] configXml1Lines = configXml1.asString().split("\\n");
final XmlFile configXml2 = getOldConfigXml(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);
}
/**
* Gets the version of the config.xml that was saved at a certain time.
*
* @param timestamp
* The timestamp as String.
* @return The config file as XmlFile.
*/
private XmlFile getOldConfigXml(String timestamp) {
checkConfigurePermission();
final XmlFile oldRevision = getHistoryDao().getOldRevision(slave,
timestamp);
if (oldRevision.getFile() != null) {
return oldRevision;
} else {
throw new IllegalArgumentException(
"Non existent timestamp " + timestamp);
}
}
/**
* Action when 'restore' button is pressed: Replace current config file by
* older version.
*
* @param req
* Incoming StaplerRequest
* @param rsp
* Outgoing StaplerResponse
* @throws IOException
* If something goes wrong
*/
public final void doRestore(StaplerRequest req, StaplerResponse rsp)
throws IOException {
checkConfigurePermission();
final String timestamp = req.getParameter("timestamp");
final XmlFile xmlFile = getHistoryDao().getOldRevision(slave,
timestamp);
final Slave newSlave = (Slave) Jenkins.XSTREAM2
.fromXML(xmlFile.getFile());
final List<Node> nodes = new ArrayList<Node>();
nodes.addAll(jenkins.getNodes());
nodes.remove(slave);
nodes.add(newSlave);
slave = newSlave;
jenkins.setNodes(nodes);
try {
rsp.sendRedirect(
jenkins.getRootUrl() + slave.toComputer().getUrl());
} catch (NullPointerException e) {
LOG.log(Level.WARNING, "Failed to redirect to agent url. ", e);
}
}
/**
* Action when 'restore' button in showDiffFiles.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 XML file can't be read
*/
public final void doForwardToRestoreQuestion(StaplerRequest req,
StaplerResponse rsp) throws IOException {
final String timestamp = req.getParameter("timestamp");
rsp.sendRedirect("restoreQuestion?timestamp=" + timestamp);
}
public Api getApi() {
return new Api(this);
}
}