package hudson.plugins.build_publisher;
import hudson.Util;
import hudson.maven.MavenModule;
import hudson.model.AbstractProject;
import hudson.model.ItemGroup;
import hudson.model.Job;
import hudson.model.JobProperty;
import hudson.model.JobPropertyDescriptor;
import hudson.model.Project;
import hudson.model.ProminentProjectAction;
import hudson.model.Run;
import hudson.model.AbstractBuild;
import hudson.tasks.ArtifactArchiver;
import hudson.tasks.LogRotator;
import hudson.util.IOException2;
import net.sf.json.JSONObject;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.Untar;
import org.apache.tools.ant.types.Resource;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.DateFormat;
import java.text.ParseException;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
import java.util.logging.Level;
import java.util.logging.Logger;
import hudson.triggers.TriggerDescriptor;
/**
* Recieves builds submitted remotely via HTTP.
*
* @author dvrzalik@redhat.com
*
*/
public class ExternalProjectProperty extends JobProperty<Job<?, ?>> implements
ProminentProjectAction {
private static final Logger LOGGER = Logger.getLogger(ExternalProjectProperty.class.getName());
private transient AbstractProject<?,?> project;
@Override
public JobPropertyDescriptor getDescriptor() {
return DESCRIPTOR;
}
@Override
public ProminentProjectAction getJobAction(Job<?, ?> job) {
this.project = (AbstractProject) job;
return this;
}
/**
* Accepts incoming MavenModule, provided that current project is
* MavenModuleSet
*/
public void doAcceptMavenModule(StaplerRequest req, StaplerResponse rsp)
throws IOException {
acceptChildProject(req, rsp, project, "modules");
}
/**
* Accepts nested project (like maven module or matrix configuration).
*/
private static void acceptChildProject(StaplerRequest req, StaplerResponse rsp,
AbstractProject project, String subDir)
throws IOException {
project.checkPermission(Job.CONFIGURE);
String name = req.getParameter("name").trim();
File modulesDir = new File(project.getRootDir(), subDir);
File moduleDir = new File(modulesDir, name);
moduleDir.mkdirs();
File configFile = new File(moduleDir, "config.xml");
try {
FileOutputStream fos = new FileOutputStream(configFile);
try {
Util.copyStream(req.getInputStream(), fos);
project.onLoad(project.getParent(), project.getName());
} finally {
fos.close();
}
} catch (IOException e) {
LOGGER.severe("Failed to accept child project " + name
+ " for " + project.getName() + e.getMessage());
// This is questionable
// Util.deleteRecursive(moduleDir);
throw e;
}
}
/**
* "Collecting basket" for incoming builds.
*/
public void doAcceptBuild(StaplerRequest req, StaplerResponse rsp)
throws IOException, InterruptedException {
project.checkPermission(Job.CONFIGURE);
// Don't send notifications for old builds
Set<String> oldBuildIDs = new HashSet<String>();
for (Run run : project.getBuilds()) {
oldBuildIDs.add(run.getId());
}
File buildsDir = new File(project.getRootDir(), "builds");
//Untar incoming builds unto the build directory
Untar untar = new Untar();
untar.setProject(new org.apache.tools.ant.Project());
untar.add(new InputStreamResource(project.getName(),
new BufferedInputStream(req.getInputStream())));
untar.setDest(buildsDir);
untar.setOverwrite(true);
if (BuildPublisher.DESCRIPTOR.getRemoveTriggers()) {
removeTriggers(project);
}
String publisherTimezoneID = (String)req.getHeader("X-Publisher-Timezone");
LOGGER.info("Got remote timezone " + publisherTimezoneID);
TimeZone publisherTimezone = null;
String buildId = null;
String newId = null;
DateFormat dateFormatter = null;
DateFormat oldDateFormatter = null;
if(publisherTimezoneID!=null) {
publisherTimezone = TimeZone.getTimeZone(publisherTimezoneID);
dateFormatter = Run.getIDFormatter();
buildId = (String)req.getHeader("X-Build-ID");
oldDateFormatter = (DateFormat)dateFormatter.clone();
oldDateFormatter.setTimeZone(publisherTimezone);
LOGGER.fine("Local timezone " + dateFormatter.getTimeZone());
LOGGER.fine("Remote timezone " + publisherTimezone);
}
try {
if(publisherTimezone!=null) {
try {
LOGGER.fine("Original build time " + oldDateFormatter.parse(buildId));
newId = dateFormatter.format(oldDateFormatter.parse(buildId));
LOGGER.fine("New build ID " + newId);
} catch (ParseException e) {
throw new BuildException("Failed to parse buildId", e);
}
}
untar.execute();
if(publisherTimezone!=null) {
File oldBuildDir = new File(buildsDir, buildId);
File newBuildDir = new File(buildsDir, newId);
LOGGER.info("Renaming: " + oldBuildDir.getCanonicalPath() + " to " + newBuildDir.getCanonicalPath());
oldBuildDir.renameTo(newBuildDir);
} else {
LOGGER.info("No remote timezone found");
}
//Load incoming builds from disk
reloadProject(project);
//Remove publishing status actions (so that they don't confuse users).
//We don't know which (or how many) builds arrive - need to check them all
for(Run build: project.getBuilds()) {
StatusAction statusAction = build.getAction(StatusAction.class);
if(statusAction != null) {
build.getActions().remove(statusAction);
build.save();
}
}
//Update next build number
Run lastBuild = project.getLastBuild();
int nextBuildNumber = (lastBuild != null ? lastBuild.number : 0) + 1;
project.updateNextBuildNumber(nextBuildNumber);
//Add confirmation header
rsp.addHeader("X-Build-Recieved",project.getName());
try {
tidyUp();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Cleaning project " + project.getName()
+ "failed: " + e.getMessage(),e);
}
} catch (BuildException e) {
LOGGER.log(Level.SEVERE, "Failed to read the remote stream "
+ project.getName() + e.getMessage(),e);
throw new IOException2("Failed to read the remote stream "
+ project.getName(), e);
}
}
private void removeTriggers(AbstractProject<?,?> project) throws IOException {
for(TriggerDescriptor trigger: project.getTriggers().keySet()) {
project.removeTrigger(trigger);
}
project.save();
}
private void tidyUp() throws IOException, InterruptedException {
// delete old builds
//reflect plugin-specific settings
BuildPublisher publisher = (BuildPublisher) project.getPublishersList().get(BuildPublisher.DESCRIPTOR);
if (publisher != null) {
LogRotator rotator = publisher.getLogRotator();
if (rotator != null) {
rotator.perform(project);
} else {
project.logRotate();
}
}
// keep artifacts of last successful build only
// (taken from ArtifactArchiver)
if (project instanceof Project) {
ArtifactArchiver archiver = project.getPublishersList().get(ArtifactArchiver.class);
if ((archiver != null) && archiver.isLatestOnly()) {
AbstractBuild<?, ?> build = project.getLastSuccessfulBuild();
if (build != null) {
while (true) {
build = build.getPreviousBuild();
if (build == null)
break;
// remove old artifacts
File ad = build.getArtifactsDir();
if (ad.exists()) {
LOGGER.info("Deleting old artifacts from "
+ build.getDisplayName());
Util.deleteRecursive(ad);
}
}
}
}
}
}
private static void reloadProject(AbstractProject project)
throws IOException {
if (project instanceof MavenModule) {
project.onLoad(project.getParent(), ((MavenModule) project)
.getModuleName().toFileSystemName());
} else {
project.onLoad(project.getParent(), project.getName());
}
}
private static class InputStreamResource extends Resource {
private final InputStream in;
public InputStreamResource(String name, InputStream in) {
this.in = in;
setName(name);
}
public InputStream getInputStream() throws IOException {
return in;
}
}
/*
* Descriptor, etc..
*/
public static final ExternalProjectPropertyDescriptor DESCRIPTOR = new ExternalProjectPropertyDescriptor();
public static class ExternalProjectPropertyDescriptor extends
JobPropertyDescriptor {
public ExternalProjectPropertyDescriptor() {
super(ExternalProjectProperty.class);
}
@Override
public boolean isApplicable(Class<? extends Job> jobType) {
//This property shall be added only programmaticaly
return false;
}
@Override
public String getDisplayName() {
return "Post remote build";
}
@Override
public JobProperty<?> newInstance(StaplerRequest req, JSONObject formData) throws FormException {
return null;
}
}
/**
* Checks if the given project already has this property and posibly adds it (recursive for ItemGroups).
*/
public static void applyToProject(Job<?,?> job) throws IOException {
if(job instanceof ItemGroup) {
for(Object item: ((ItemGroup) job).getItems()) {
applyToProject((Job /*too optimistic assumption?*/) item);
}
}
//Could not use getProperty(...)
for (JobProperty prop : job.getProperties().values()) {
//hem... >:-|
if (prop.getClass().getName().equals("hudson.plugins.build_publisher.ExternalProjectProperty")) {
return;
}
}
job.addProperty(new ExternalProjectProperty());
}
public String getDisplayName() {
return DESCRIPTOR.getDisplayName();
}
public String getIconFileName() {
return null;
}
public String getUrlName() {
return "postBuild";
}
}