/*
* Created on May 24, 2007
*
* This code belongs to Jonathan Fuerth
*/
package org.moxie.ant;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSession;
import org.apache.tools.ant.BuildException;
import org.moxie.utils.Base64;
/**
* GoogleCodeUploadTask is an Ant task for uploading files to a Google Code
* project's downloads area.
* <p>
* The following parameters must be set before this task is executed:
* <ul>
* <li>userName
* <li>password
* <li>projectName
* <li>fileName
* <li>targetFileName
* <li>summary
* </ul>
* <p>
* The following parameters are optional, and default to nothing/excluded:
* <ul>
* <li>labels (a comma-separated list of labels to apply to the uploaded item)
* </ul>
* <p>
* The following parameters have reasonable defaults, and do not normally need to be set:
* <ul>
* <li>uploadUrl
* <li>ignoreSslCertificateHostname
* </ul>
*
* @version $Id$
*/
public class MxGCUpload extends MxTask {
/**
* Google user name to authenticate as (this is just the username part;
* don't include the @gmail.com part).
*/
private String userName;
/**
* Coogle Code password (not the same as the gmail password).
*/
private String password;
/**
* Google Code project name to upload to.
*/
private String projectName;
/**
* The local path of the file to upload.
*/
private String fileName;
/**
* The file name that this file will be given on Google Code.
*/
private String targetFileName;
/**
* Summary of the upload.
*/
private String summary;
/**
* Overrides the default upload URL. This parameter is only useful for
* testing this Ant task without uploading to the live server.
*/
private String uploadUrl;
/**
* If set to true, this task will print debugging information to System.out
* as it progresses through its job.
*/
private boolean verbose;
/**
* The labels that the download should have, separated by commas. Extra
* whitespace before and after each label name will not be considered part
* of the label name.
*/
private String labels;
/**
* In case of using https connection, some certificate could not be
* validated by ssl security layer. To disable the ssl security layer, set
* this parameter to true. Use this option only if you understand the risks.
*/
private boolean ignoreSslCertificateHostname = false;
public MxGCUpload() {
super();
setTaskName("mx:GCUpload");
}
/**
* Just calls {@link #upload()}, wrapping any exceptions it might throw in a
* BuildException.
*/
@Override
public void execute() throws BuildException {
try {
upload();
} catch (Exception ex) {
augmentAndRethrow(ex);
}
}
/**
* Many developers getting started with this task run into simple problems
* that they can't solve on their own. These get reported as bugs.
* <p>
* This method provides a kind of "context-sensitive FAQ" which can be
* augmented over time as more easy problems with simple solutions get
* reported as bugs.
*
* @param ex
* The exception that caused this task to fail.
* @throws BuildException
* always; the new exception may have additional hints attached
* to it, and will be initialized with <code>ex</code> as its
* cause.
*/
private void augmentAndRethrow(Exception ex) throws BuildException {
Throwable rootCause = ex;
while (rootCause.getCause() != null) {
rootCause = rootCause.getCause();
}
String message = rootCause.getMessage();
List<String> hints = new ArrayList<String>();
if (rootCause instanceof IOException) {
if (message.matches(".*40[13].*")) {
hints.add("This is often caused by use of the wrong password. Find your password at http://code.google.com/hosting/settings (you have to be signed in)");
}
}
hints.add("Invoke ant with the \"-v\" argument to see the full stack trace");
StringBuilder hintedMessage = new StringBuilder();
hintedMessage.append("Original Message: ").append(message);
for (String hint: hints) {
hintedMessage.append("\r\nHint: ").append(hint);
}
throw new BuildException(hintedMessage.toString(), ex);
}
/**
* Uploads the contents of the file {@link #fileName} to the project's
* Google Code upload url. Performs the basic http authentication required
* by Google Code.
*/
private void upload() throws IOException {
System.clearProperty("javax.net.ssl.trustStoreProvider"); // fixes open-jdk-issue
System.clearProperty("javax.net.ssl.trustStoreType");
final String BOUNDARY = "CowMooCowMooCowCowCow";
URL url = createUploadURL();
log("The upload URL is " + url);
InputStream in = new BufferedInputStream(new FileInputStream(fileName));
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
if (this.isIgnoreSslCertificateHostname()) {
if (conn instanceof HttpsURLConnection) {
HttpsURLConnection secure = (HttpsURLConnection) conn;
secure.setHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
boolean result = true;
log("SSL verification ignored for current session and hostname: "
+ hostname);
return result;
}
});
}
}
conn.setDoOutput(true);
conn.setRequestProperty("Authorization", "Basic " + createAuthToken(userName, password));
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
conn.setRequestProperty("User-Agent", "Google Code Upload Ant Task 0.1");
log("Attempting to connect (username is " + userName + ")...");
conn.connect();
log("Sending request parameters...");
OutputStream out = conn.getOutputStream();
sendLine(out, "--" + BOUNDARY);
sendLine(out, "content-disposition: form-data; name=\"summary\"");
sendLine(out, "");
sendLine(out, summary);
if (labels != null) {
String[] labelArray = labels.split("\\,");
if (labelArray != null && labelArray.length > 0) {
log("Setting "+labelArray.length+" label(s)");
for (int n = 0, i = labelArray.length; n < i; n++) {
sendLine(out, "--" + BOUNDARY);
sendLine(out, "content-disposition: form-data; name=\"label\"");
sendLine(out, "");
sendLine(out, labelArray[n].trim());
}
}
}
log("Sending file... "+targetFileName);
sendLine(out, "--" + BOUNDARY);
sendLine(out, "content-disposition: form-data; name=\"filename\"; filename=\"" + targetFileName + "\"");
sendLine(out, "Content-Type: application/octet-stream");
sendLine(out, "");
int count;
byte[] buf = new byte[8192];
while ( (count = in.read(buf)) >= 0 ) {
out.write(buf, 0, count);
}
in.close();
sendLine(out, "");
sendLine(out, "--" + BOUNDARY + "--");
out.flush();
out.close();
// For whatever reason, you have to read from the input stream before
// the url connection will start sending
in = conn.getInputStream();
log("Upload finished. Reading response.");
log("HTTP Response Headers: " + conn.getHeaderFields());
StringBuilder responseBody = new StringBuilder();
while ( (count = in.read(buf)) >= 0 ) {
responseBody.append(new String(buf, 0, count, "ascii"));
}
log(responseBody.toString());
in.close();
conn.disconnect();
}
/**
* Just sends an ASCII version of the given string, followed by a CRLF line terminator,
* to the given output stream.
*/
private void sendLine(OutputStream out, String string) throws IOException {
out.write(string.getBytes("ascii"));
out.write("\r\n".getBytes("ascii"));
}
/**
* Creates a (base64-encoded) HTTP basic authentication token for the
* given user name and password.
*/
private static String createAuthToken(String userName, String password) {
String string = (userName + ":" + password);
try {
return Base64.encodeBytes(string.getBytes("UTF-8"));
}
catch (java.io.UnsupportedEncodingException notreached){
throw new InternalError(notreached.toString());
}
}
/**
* Creates the correct URL for uploading to the named google code project.
* If uploadUrl is not set (this is the standard case), the correct URL will
* be generated based on the {@link #projectName}. Otherwise, if uploadUrl
* is set, it will be used and the project name setting will be ignored.
*/
private URL createUploadURL() throws MalformedURLException {
if (uploadUrl != null) {
return new URL(uploadUrl);
} else {
if (projectName == null) {
throw new NullPointerException("projectName must be set");
}
return new URL("https", projectName + ".googlecode.com", "/files");
}
}
// ============ Getters and Setters ==============
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getProjectName() {
return projectName;
}
public void setProjectName(String projectName) {
this.projectName = projectName;
}
public String getSummary() {
return summary;
}
public void setSummary(String summary) {
this.summary = summary;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getTargetFileName() {
return targetFileName;
}
public void setTargetFileName(String targetFileName) {
this.targetFileName = targetFileName;
}
public String getUploadUrl() {
return uploadUrl;
}
public void setUploadUrl(String uploadUrl) {
this.uploadUrl = uploadUrl;
}
public boolean isVerbose() {
return verbose;
}
public void setVerbose(boolean verbose) {
this.verbose = verbose;
}
public String getLabels() {
return labels;
}
public void setLabels(String labels) {
this.labels = labels;
}
public boolean isIgnoreSslCertificateHostname() {
return this.ignoreSslCertificateHostname;
}
public void setIgnoreSslCertificateHostname(boolean ignoreSslCertHostname) {
this.ignoreSslCertificateHostname = ignoreSslCertHostname;
}
}