package hudson.plugins.build_publisher;
import hudson.Functions;
import hudson.Util;
import hudson.matrix.MatrixConfiguration;
import hudson.maven.MavenModule;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpMethodBase;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.methods.FileRequestEntity;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.methods.PostMethod;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.tar.TarEntry;
import org.apache.tools.tar.TarOutputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.TimeZone;
import java.util.logging.Level;
import org.apache.commons.httpclient.HttpException;
/**
* Sends build result via HTTP protocol.
*
*/
public class HTTPBuildTransmitter implements BuildTransmitter {
private PostMethod method;
private boolean aborted = false;
public void sendBuild(AbstractBuild build, HudsonInstance hudsonInstance)
throws ServerFailureException {
aborted = false;
AbstractProject project = build.getProject();
String jobUrl = "job/";
if (project instanceof MavenModule) {
jobUrl += ((MavenModule) project).getParent().getName()
+ "/"
+ ((MavenModule) project).getModuleName()
.toFileSystemName();
} else if (project instanceof MatrixConfiguration) {
jobUrl += ((MatrixConfiguration)project).getParent().getName()
+ "/"
+ ((MatrixConfiguration)project).getCombination().toString();
} else {
jobUrl += project.getName();
}
method = new PostMethod(hudsonInstance.getUrl()
+ encodeURI(jobUrl) + "/postBuild/acceptBuild");
File tempFile = null;
try {
tempFile = File.createTempFile("hudson_bp", ".tar");
OutputStream out = new FileOutputStream(tempFile);
writeToTar(out, build);
method.setRequestEntity(new FileRequestEntity(tempFile,
"application/x-tar"));
method.setRequestHeader("X-Publisher-Timezone", TimeZone.getDefault().getID());
method.setRequestHeader("X-Build-ID", build.getId());
executeMethod(method, hudsonInstance);
//Check if remote side really accepted the build
Header responseHeader = method.getResponseHeader("X-Build-Recieved");
if((responseHeader == null) ||
!project.getName().equals(responseHeader.getValue().trim())) {
throw new HttpException("Remote instance didn't confirm recieving this build");
}
} catch (IOException e) {
// May be caused by premature call of HttpMethod.abort()
if (!aborted) {
throw new ServerFailureException(method,e);
}
} catch (RuntimeException e1) {
if (!aborted) {
throw (e1);
}
} finally {
if (!tempFile.delete()) {
HudsonInstance.LOGGER.log(Level.SEVERE, "Failed to delete temporary file "
+ tempFile.getAbsolutePath()
+ ". Please delete the file manually.");
}
}
}
public void abortTransmission() {
aborted = true;
if (method != null) {
method.abort();
}
}
/**
* Executes the given method, with authenticates if necessary,
* and follow any redirects.
*
* @return
* Final {@link HttpMethod} that successfully executed,
* after possible redirects. The status code of this is always <300 because
* this method handles redirection and errors are thrown as exceptions.
*
* <p>
* The return value is useful if the caller wants to read the response.
*
* @throws ServerFailureException
* If we encounter >400 error code from the server.
* @throws IOException
* Other generic communication exception.
*/
static HttpMethod executeMethod(HttpMethodBase method,
HudsonInstance hudsonInstance) throws ServerFailureException {
hudsonInstance.getHttpClient().getState().clear();
if ((hudsonInstance.requiresAuthentication())) {
// We need to get authenticated.
// On some containers and depending on the security configuration,
// simply sending HTTP BASIC auth would work, but in legacy authentication
// with some containers in particular, the behavior tends to be
// different.
// So while lengthy, let's emulate the user behavior when
// they clock the login link, which is most stable across different
// environment
GetMethod loginMethod = new GetMethod(hudsonInstance.getUrl()
+ "loginEntry");
followRedirects(loginMethod, hudsonInstance);
try {
login("j_security_check", hudsonInstance);
} catch (ServerFailureException e) {
// Here if the servlet authentication is not available.
login("j_acegi_security_check", hudsonInstance);
}
}
return followRedirects(method, hudsonInstance);
}
private static void login(String type, HudsonInstance hudsonInstance)
throws ServerFailureException {
PostMethod servletSecurityMethod = new PostMethod(hudsonInstance.getUrl() + type);
servletSecurityMethod.addParameter("j_username", hudsonInstance.getLogin());
servletSecurityMethod.addParameter("j_password", hudsonInstance.getPassword());
servletSecurityMethod.addParameter("action", "login");
followRedirects(servletSecurityMethod, hudsonInstance);
}
// see executeMethod for contracts
private static HttpMethod followRedirects(HttpMethodBase method,
HudsonInstance hudsonInstance) throws ServerFailureException {
int statusCode;
HttpClient client = hudsonInstance.getHttpClient();
try {
statusCode = client.executeMethod(method);
if(statusCode<300)
return method;
if(statusCode<400) {
Header locationHeader = method.getResponseHeader("location");
if (locationHeader != null) {
String redirectLocation = locationHeader.getValue();
method.setURI(new org.apache.commons.httpclient.URI(/*method.getURI(),*/ redirectLocation,
true));
return followRedirects(method, hudsonInstance);
}
}
// failure
throw new ServerFailureException(method);
} catch(IOException ioe) {
throw new ServerFailureException(method, ioe);
} finally {
method.releaseConnection();
}
}
/**
* Writes to a tar stream and stores obtained files to the base dir.
*
* @return number of files/directories that are written.
*/
// most of this is taken from somewhere of Hudson code. Perhaps it would be
// good idea to put it in one place.
private Integer writeToTar(OutputStream out, AbstractBuild build)
throws IOException {
File buildDir = build.getRootDir();
File baseDir = buildDir.getParentFile();
String buildXmlFile = buildDir.getName() + "/build.xml";
FileSet fileSet = new FileSet();
fileSet.setDir(baseDir);
fileSet.setIncludes(buildDir.getName() + "/**");
fileSet.setExcludes(buildXmlFile);
byte[] buffer = new byte[8192];
TarOutputStream tar = new TarOutputStream(new BufferedOutputStream(out));
tar.setLongFileMode(TarOutputStream.LONGFILE_GNU);
DirectoryScanner dirScanner = fileSet
.getDirectoryScanner(new org.apache.tools.ant.Project());
String[] files = dirScanner.getIncludedFiles();
for (String fileName : files) {
if (aborted) {
break;
}
if (Functions.isWindows()) {
fileName = fileName.replace('\\', '/');
}
File file = new File(baseDir, fileName);
if (!file.isDirectory()) {
writeStreamToTar(tar, new FileInputStream(file), fileName, file
.length(), buffer);
}
}
File buildFile = new File(build.getRootDir(), "build.xml");
String buildXml = Util.loadFile(buildFile);
byte[] bytes = buildXml.getBytes();
writeStreamToTar(tar, new ByteArrayInputStream(bytes), buildXmlFile,
bytes.length, buffer);
tar.close();
return files.length;
}
private void writeStreamToTar(TarOutputStream tar, InputStream in,
String fileName, long length, byte[] buf) throws IOException {
TarEntry te = new TarEntry(fileName);
te.setSize(length);
tar.putNextEntry(te);
int len;
while ((len = in.read(buf)) >= 0) {
if (aborted) {
break;
}
tar.write(buf, 0, len);
}
tar.closeEntry();
in.close();
}
public static String encodeURI(String uri) {
try {
return new URI(null,uri,null).toASCIIString();
} catch (URISyntaxException e) {
e.printStackTrace();
return uri;
}
}
}