package hudson.plugins.accurev.delegates;
import hudson.EnvVars;
import hudson.FilePath;
import hudson.Launcher;
import hudson.model.AbstractBuild;
import hudson.model.Job;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.plugins.accurev.*;
import hudson.plugins.accurev.cmd.*;
import hudson.scm.PollingResult;
import hudson.scm.SCMRevisionState;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.math.NumberUtils;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* Performs actual SCM operations
*/
public abstract class AbstractModeDelegate {
protected static final String ACCUREV_WORKSPACE = "ACCUREV_WORKSPACE";
protected static final String ACCUREV_REFTREE = "ACCUREV_REFTREE";
private static final Logger logger = Logger.getLogger(AbstractModeDelegate.class.getName());
private static final String ACCUREV_DEPOT = "ACCUREV_DEPOT";
private static final String ACCUREV_STREAM = "ACCUREV_STREAM";
private static final String ACCUREV_SERVER = "ACCUREV_SERVER";
private static final String ACCUREV_SERVER_HOSTNAME = "ACCUREV_SERVER_HOSTNAME";
private static final String ACCUREV_SERVER_PORT = "ACCUREV_SERVER_PORT";
private static final String ACCUREV_SUBPATH = "ACCUREV_SUBPATH";
private static final String ACCUREV_LATEST_TRANSACTION_ID = "ACCUREV_LATEST_TRANSACTION_ID";
private static final String ACCUREV_LATEST_TRANSACTION_DATE = "ACCUREV_LATEST_TRANSACTION_DATE";
private static final String ACCUREV_HOME = "ACCUREV_HOME";
private static final String ACCUREVLASTTRANSFILENAME = "AccurevLastTrans.txt";
private static final String POPULATE_FILES = "PopulateFiles.txt";
public final AccurevSCM scm;
protected Launcher launcher;
protected AccurevSCM.AccurevServer server;
protected EnvVars accurevEnv;
protected FilePath jenkinsWorkspace;
protected TaskListener listener;
protected FilePath accurevWorkingSpace;
protected String localStream;
protected Date startDateOfPopulate;
protected String accurevTool;
public AbstractModeDelegate(AccurevSCM scm) {
this.scm = scm;
}
public void setup(Launcher launcher, FilePath jenkinsWorkspace, TaskListener listener) throws IOException, IllegalArgumentException, InterruptedException {
this.launcher = launcher;
this.jenkinsWorkspace = jenkinsWorkspace;
this.listener = listener;
server = scm.getServer();
accurevEnv = new EnvVars();
if (jenkinsWorkspace != null) {
accurevWorkingSpace = new FilePath(jenkinsWorkspace, scm.getDirectoryOffset() == null ? "" : scm.getDirectoryOffset());
if (!accurevWorkingSpace.exists()) {
accurevWorkingSpace.mkdirs();
}
if (!Login.ensureLoggedInToAccurev(scm, server, accurevEnv, jenkinsWorkspace, listener, launcher)) {
throw new IllegalArgumentException("Authentication failure");
}
if (scm.isSynctime()) {
listener.getLogger().println("Synchronizing clock with the server...");
if (!Synctime.synctime(scm, server, accurevEnv, jenkinsWorkspace, listener, launcher)) {
throw new IllegalArgumentException("Synchronizing clock failure");
}
}
}
}
public PollingResult compareRemoteRevisionWith(Job<?, ?> project, Launcher launcher, FilePath jenkinsWorkspace, TaskListener listener, SCMRevisionState state) throws IOException, InterruptedException {
if (project.isInQueue()) {
listener.getLogger().println("Project build is currently in queue.");
return PollingResult.NO_CHANGES;
}
if (jenkinsWorkspace == null) {
listener.getLogger().println("No workspace required.");
// If we're claiming not to need a workspace in order to poll, then
// workspace will be null. In that case, we need to run directly
// from the project folder on the master.
final File projectDir = project.getRootDir();
jenkinsWorkspace = new FilePath(projectDir);
launcher = Jenkins.getInstance().createLauncher(listener);
}
listener.getLogger().println("Running commands from folder \"" + jenkinsWorkspace + '"');
setup(launcher, jenkinsWorkspace, listener);
return checkForChanges(project);
}
protected abstract PollingResult checkForChanges(Job<?, ?> project) throws IOException, InterruptedException;
public boolean checkout(Run<?, ?> build, Launcher launcher, FilePath jenkinsWorkspace, TaskListener listener,
File changelogFile) throws IOException, InterruptedException {
setup(launcher, jenkinsWorkspace, listener);
if (StringUtils.isEmpty(scm.getDepot())) {
throw new IllegalStateException("Must specify a depot");
}
if (StringUtils.isEmpty(scm.getStream())) {
throw new IllegalStateException("Must specify a stream");
}
final EnvVars environment = build.getEnvironment(listener);
accurevEnv.putAll(environment);
localStream = environment.expand(scm.getStream());
listener.getLogger().println("Getting a list of streams...");
final Map<String, AccurevStream> streams = ShowStreams.getStreams(scm, localStream, server, accurevEnv, jenkinsWorkspace, listener,
launcher);
if (streams == null) {
throw new IllegalStateException("Stream(s) not found");
}
if (!streams.containsKey(localStream)) {
listener.fatalError("The specified stream, '" + localStream + "' does not appear to exist!");
throw new IllegalStateException("Specified stream not found");
}
if (server.isUseColor()) {
setStreamColor();
}
return checkout(build, changelogFile) && populate(build) && captureChangeLog(build, changelogFile, streams);
}
private boolean captureChangeLog(Run<?, ?> build, File changelogFile, Map<String, AccurevStream> streams) throws IOException {
try {
AccurevTransaction latestTransaction = getLatestTransactionFromStreams(streams);
if (latestTransaction == null) {
throw new NullPointerException("The 'hist' command did not return a transaction. Does this stream have any history yet?");
}
String latestTransactionID = latestTransaction.getId();
SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
String latestTransactionDate = formatter.format(latestTransaction.getDate());
listener.getLogger().println("Latest Transaction ID: " + latestTransactionID);
listener.getLogger().println("Latest transaction Date: " + latestTransactionDate);
EnvVars envVars = new EnvVars();
envVars.put(ACCUREV_LATEST_TRANSACTION_ID, latestTransactionID);
envVars.put(ACCUREV_LATEST_TRANSACTION_DATE, latestTransactionDate);
AccurevPromoteTrigger.setLastTransaction(build.getParent(), latestTransactionID);
build.addAction(new AccuRevHiddenParametersAction(envVars));
} catch (Exception e) {
listener.error("There was a problem getting the latest transaction info from the stream.");
e.printStackTrace(listener.getLogger());
}
listener.getLogger().println(
"Calculating changelog" + (scm.isIgnoreStreamParent() ? ", ignoring changes in parent" : "") + "...");
final Calendar startTime;
Run<?, ?> previousBuild = null;
if (build != null) previousBuild = build.getPreviousBuild();
if (previousBuild != null) startTime = previousBuild.getTimestamp();
else {
Calendar c = Calendar.getInstance();
c.add(Calendar.DAY_OF_YEAR, -7);
startTime = c;
}
Map<String, GetConfigWebURL> webURL = ChangeLogCmd.retrieveWebURL(server, accurevEnv, accurevWorkingSpace, listener, launcher, logger, scm);
AccurevStream stream = streams == null ? null : streams.get(localStream);
if (stream == null) {
// if there was a problem, fall back to simple stream check
return ChangeLogCmd.captureChangelog(server, accurevEnv, accurevWorkingSpace, listener, launcher,
startDateOfPopulate, startTime.getTime(),
localStream, changelogFile, logger, scm, webURL);
}
// Too confusing reading it
// This check is for whether to catch changes for one or multiple streams
// ALso if it should ignore changes on Parent
// Doing too much in too few lines to make it apparant
// High potential for simple rewrite!
if (!getChangesFromStreams(startTime, stream, changelogFile, webURL)) {
return ChangeLogCmd.captureChangelog(server, accurevEnv, accurevWorkingSpace, listener, launcher, startDateOfPopulate,
startTime.getTime(), localStream, changelogFile, logger, scm, webURL);
}
return true;
}
protected String getChangeLogStream() {
return localStream;
}
private boolean getChangesFromStreams(final Calendar startTime, AccurevStream stream, File changelogFile, Map<String, GetConfigWebURL> webURL) throws IOException {
List<String> changedStreams = new ArrayList<>();
// Capture changes in all streams and parents
boolean capturedChangelog;
do {
File streamChangeLog = XmlConsolidateStreamChangeLog.getStreamChangeLogFile(changelogFile, stream);
capturedChangelog = ChangeLogCmd.captureChangelog(server, accurevEnv, accurevWorkingSpace, listener, launcher,
startDateOfPopulate, startTime == null ? null : startTime.getTime(), stream.getName(), streamChangeLog, logger, scm, webURL);
if (capturedChangelog) {
changedStreams.add(streamChangeLog.getName());
}
stream = stream.getParent();
}
while (stream != null && stream.isReceivingChangesFromParent() && capturedChangelog && startTime != null && !scm.isIgnoreStreamParent());
XmlConsolidateStreamChangeLog.createChangeLog(changedStreams, changelogFile, getUpdateFileName());
return capturedChangelog;
}
private AccurevTransaction getLatestTransactionFromStreams(Map<String, AccurevStream> streams) throws Exception {
AccurevTransaction transaction = null;
AccurevStream stream = streams.get(getChangeLogStream());
do {
AccurevTransaction other = History.getLatestTransaction(scm,
server, accurevEnv, accurevWorkingSpace, listener, launcher, stream.getName(), null);
if (null == transaction && null != other) transaction = other;
else if ((null != transaction && null != other) && Integer.parseInt(other.getId()) > Integer.parseInt(transaction.getId())) {
transaction = other;
}
stream = stream.getParent();
} while (stream != null && stream.isReceivingChangesFromParent() && !scm.isIgnoreStreamParent());
return transaction;
}
public EnvVars getAccurevEnv() {
return accurevEnv;
}
protected String getUpdateFileName() {
return null;
}
protected abstract boolean checkout(Run<?, ?> build, File changeLogFile) throws IOException, InterruptedException;
protected abstract String getPopulateFromMessage();
protected abstract String getPopulateStream();
protected boolean isPopulateRequired() {
return !scm.isDontPopContent();
}
protected boolean isSteamColorEnabled() {
return false;
}
protected String getStreamColor() {
return "";
}
protected String getStreamColorStream() {
return null;
}
private void setStreamColor() throws IOException {
if (isSteamColorEnabled()) {
SetProperty.setproperty(scm, accurevWorkingSpace, listener, launcher, accurevEnv, server, getStreamColorStream(),
getStreamColor(), getStreamTypeParameter());
}
}
protected boolean populate(Run<?, ?> build, boolean populateRequired) throws IOException, InterruptedException {
if (populateRequired) {
String stream = getPopulateStream();
int lastTransaction = NumberUtils.toInt(getLastBuildTransaction(build), 0);
PopulateCmd pop = new PopulateCmd();
if (lastTransaction == 0 || accurevWorkingSpace.list("*", ".accurev").length == 0) {
if (pop.populate(scm, launcher, listener, server, stream, true, getPopulateFromMessage(), accurevWorkingSpace, accurevEnv,
null))
startDateOfPopulate = pop.get_startDateOfPopulate();
else
return false;
} else {
String filePath = getFileRevisionsTobePopulated(build, lastTransaction, getChangeLogStream());
logger.info("populate file path " + filePath);
if (filePath != null) {
if (pop.populate(scm, launcher, listener, server, stream, true, getPopulateFromMessage(), accurevWorkingSpace,
accurevEnv, filePath)) {
startDateOfPopulate = pop.get_startDateOfPopulate();
deletePopulateFile(filePath);
} else {
deletePopulateFile(filePath);
return false;
}
}
startDateOfPopulate = new Date();
}
} else {
startDateOfPopulate = new Date();
}
return true;
}
protected boolean populate(Run<?, ?> build) throws IOException, InterruptedException {
return populate(build, isPopulateRequired());
}
public void buildEnvVars(AbstractBuild<?, ?> build, Map<String, String> env) {
try {
setup(null, null, TaskListener.NULL);
} catch (IOException | InterruptedException ex) {
logger.log(Level.SEVERE, "buildEnvVars", ex);
}
if (scm.getDepot() != null) {
env.put(ACCUREV_DEPOT, scm.getDepot());
} else {
env.put(ACCUREV_DEPOT, "");
}
if (scm.getStream() != null) {
env.put(ACCUREV_STREAM, scm.getStream());
} else {
env.put(ACCUREV_STREAM, "");
}
if (server != null && server.getName() != null) {
env.put(ACCUREV_SERVER, server.getName());
} else {
env.put(ACCUREV_SERVER, "");
}
if (server != null && server.getHost() != null) {
env.put(ACCUREV_SERVER_HOSTNAME, server.getHost());
} else {
env.put(ACCUREV_SERVER_HOSTNAME, "");
}
if (server != null && server.getPort() > 0) {
env.put(ACCUREV_SERVER_PORT, Integer.toString(server.getPort()));
} else {
env.put(ACCUREV_SERVER_PORT, "");
}
env.put(ACCUREV_WORKSPACE, "");
env.put(ACCUREV_REFTREE, "");
if (scm.getSubPath() != null) {
env.put(ACCUREV_SUBPATH, scm.getSubPath());
} else {
env.put(ACCUREV_SUBPATH, "");
}
// ACCUREV_HOME is added to the build env variables
if (System.getenv(ACCUREV_HOME) != null) {
env.put(ACCUREV_HOME, System.getenv(ACCUREV_HOME));
}
buildEnvVarsCustom(build, env);
}
protected void buildEnvVarsCustom(AbstractBuild<?, ?> build, Map<String, String> env) {
// override to put implementation specific values
}
/**
* get color type parameter from the AccuRev Version If version less than 6 or equal to 6.0.x the color parameter will be style if
* version greater than 6 like 6.1.x or 7 then color parameter will streamStyle
*
* @return String
*/
private String getStreamTypeParameter() {
String fullVersion = GetAccuRevVersion.getAccuRevVersion().trim();
String partialversion = fullVersion.substring(fullVersion.indexOf(" ") + 1);
String version = partialversion.substring(0, partialversion.indexOf(" "));
String[] versionSplits = version.split("\\.");
String type = ((Integer.parseInt(versionSplits[0]) < 6) || (Integer.parseInt(versionSplits[0]) == 6 && Integer
.parseInt(versionSplits[1]) < 1)) ? "style" : "streamStyle";
logger.info("Current AccuRev version " + fullVersion + " color type parameter " + type);
return type;
}
/**
* Get last transaction build from the jenkins for the currently running project
*
* @return String
* @throws IOException Failing to read file
*/
private String getLastBuildTransaction(Run<?, ?> build) throws IOException {
File f = new File(build.getParent().getRootDir(), ACCUREVLASTTRANSFILENAME);
if (!f.exists()) {
return null;
}
try (BufferedReader br = Files.newBufferedReader(f.toPath(), UTF_8)) {
return br.readLine();
}
}
/**
* Get list of new files to be added into the jenkins build from a given a transaction.
*
* @param lastTransaction the last transaction stored
* @param stream stream populate from
* @return String
* @throws IOException failed to open file
*/
private String getFileRevisionsTobePopulated(Run<?, ?> build, int lastTransaction, String stream) throws IOException {
List<AccurevTransaction> transactions = History.getTransactionsAfterLastTransaction(scm, server, accurevEnv,
accurevWorkingSpace, listener, launcher, stream, lastTransaction);
// collect all the files from the list of transactions and remove duplicates from the list of files.
List<String> fileRevisions = transactions
.stream()
.filter(t -> t != null && !(t.getAction().equals("defunct")))
.map(AccurevTransaction::getAffectedPaths)
.flatMap(Collection::stream)
.collect(Collectors.toList())
.parallelStream()
.distinct()
.collect(Collectors.toList());
return (!fileRevisions.isEmpty()) ? getPopulateFilePath(build, fileRevisions) : null;
}
/**
* Create a text file to keep the list of files to be populated.
*
* @param fileRevisions current file revisions
* @return String
*/
private String getPopulateFilePath(Run<?, ?> build, List<String> fileRevisions) {
BufferedWriter bw = null;
File populateFile;
String filepath = null;
try {
populateFile = new File(build.getParent().getRootDir(), POPULATE_FILES);
filepath = populateFile.getAbsolutePath();
logger.info("populate file path is " + populateFile.getAbsolutePath());
bw = Files.newBufferedWriter(populateFile.toPath(), UTF_8);
for (String filePath : fileRevisions) {
bw.write(filePath);
bw.newLine();
}
} catch (IOException exe) {
logger.info("Exception happend to write in a file." + exe);
} finally {
try {
if (bw != null)
bw.close();
} catch (IOException ex) {
logger.info("Exception happend to close the buffered writer." + ex);
}
}
return filepath;
}
private void deletePopulateFile(String filePath) {
File populateFile = new File(filePath);
boolean deleted = populateFile.delete();
logger.info("temporary file deleted " + deleted);
}
}