package org.openflexo.ws.jira; import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import javax.activation.FileTypeMap; import org.apache.commons.codec.binary.Base64; import org.openflexo.ws.jira.action.JIRAAction; import org.openflexo.ws.jira.model.JIRAIssue; import org.openflexo.ws.jira.result.JIRAResult; public class JIRAClient { public static interface Progress { public void setProgress(double percentage); } public enum Method { GET, POST, PUT, DELETE; } private static final String REST_API_ROOT = "/rest/api/2"; private static final String SET_COOKIE_HEADER = "Set-Cookie"; // list of name=value; private static final String COOKIE_HEADER = "Cookie"; // list of name=value; private static final String BASIC_AUTH_HEADER = "Authorization"; private static final String CONTENT_TYPE_HEADER = "Content-Type"; private static final String X_ATLASSIAN_TOKEN_HEADER = "X-Atlassian-Token"; private static final String SUBMIT_ISSUE_REST_API = "/issue"; private static final String SUBMIT_ISSUE_ATTACHMENT_REST_API = "/attachments"; private static final String TWO_HYPHENS = "--"; private static final String BOUNDARY = "cestquoicebazaryeuxbraguette"; private static final String SUB_BOUNDARY = "chaudecommeunebarraqueafrite"; private static final String CR_LF = "\r\n"; private URL jiraBaseURL; private String username; private String password; private int timeout; public JIRAClient(String jiraBaseURL, String username, String password) throws MalformedURLException { super(); if (!jiraBaseURL.toLowerCase().startsWith("http://") && !jiraBaseURL.toLowerCase().startsWith("https://")) { throw new MalformedURLException("The JIRA Client only supports http or https protocols"); } this.timeout = 30 * 1000; this.jiraBaseURL = new URL(jiraBaseURL); this.username = username; this.password = password; } public <A extends JIRAAction<R>, R extends JIRAResult> R submit(A submit, Method method) throws IOException { return submit(submit, method, null); } public <A extends JIRAAction<R>, R extends JIRAResult> R submit(A submit, Method method, Progress progress) throws IOException { URL url = new URL(jiraBaseURL, REST_API_ROOT + SUBMIT_ISSUE_REST_API); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setChunkedStreamingMode(4096); connection.setConnectTimeout(timeout); connection.setReadTimeout(timeout); connection.setDoOutput(true); connection.setRequestMethod(method.name()); connection.addRequestProperty(BASIC_AUTH_HEADER, "Basic " + getBase64EncodedAuthentication()); connection.addRequestProperty(CONTENT_TYPE_HEADER, "application/json"); connection.connect(); String json = JIRAGson.getInstance().toJson(submit); byte[] bytes = json.getBytes("UTF-8"); for (int i = 0; i < bytes.length;) { connection.getOutputStream().write(bytes, i, Math.min(4096, bytes.length - i)); i += 4096; if (progress != null) { progress.setProgress((double) i / bytes.length); } } switch (connection.getResponseCode()) { case 401: case 403: throw new UnauthorizedJIRAAccessException(); } InputStream is; boolean isErrorStatus = connection.getResponseCode() > 399; if (isErrorStatus) { is = new BufferedInputStream(connection.getErrorStream()); } else { is = new BufferedInputStream(connection.getInputStream()); } try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); int read = -1; byte[] b = new byte[4096]; while ((read = is.read(b)) > 0) { baos.write(b, 0, read); } String json2 = new String(baos.toByteArray(), "UTF-8"); if (isErrorStatus) { throw new IOException(json2 + "\n(Status: " + connection.getResponseCode() + ")"); } return JIRAGson.getInstance().fromJson(json2, submit.getResultClass()); } finally { is.close(); } } public void attachFilesToIssue(JIRAIssue issue, File file) throws IOException { attachFilesToIssue(issue, null, file); } public void attachFilesToIssue(JIRAIssue issue, Progress progress, File file) throws IOException { attachFilesToIssue(issue, progress, new File[] { file }); } public void attachFilesToIssue(JIRAIssue issue, Progress progress, File... files) throws IOException { String idOrKey = issue.getId() != null ? issue.getId() : issue.getKey(); if (idOrKey == null) { throw new NullPointerException("Issue has no id nor key"); } URL url = new URL(jiraBaseURL, REST_API_ROOT + SUBMIT_ISSUE_REST_API + "/" + idOrKey + SUBMIT_ISSUE_ATTACHMENT_REST_API); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setChunkedStreamingMode(4096); connection.setConnectTimeout(timeout); connection.setDoOutput(true); connection.setDoInput(true); connection.setRequestMethod(Method.POST.name()); connection.addRequestProperty(BASIC_AUTH_HEADER, "Basic " + getBase64EncodedAuthentication()); connection.addRequestProperty(X_ATLASSIAN_TOKEN_HEADER, "nocheck"); connection.addRequestProperty(CONTENT_TYPE_HEADER, "multipart/form-data; boundary=" + BOUNDARY); connection.addRequestProperty("Connection", "Keep-Alive"); // connection.addRequestProperty("Content-Length", String.valueOf(SINGLE_FILE_OVERHEAD + files[0].length())); connection.connect(); boolean isMultipleFiles = files.length > 1; UTF8OutputStream os = new UTF8OutputStream(connection.getOutputStream()); writeBoundary(os, BOUNDARY).write(CR_LF); os.write("content-disposition: form-data; name=\"file\""); // Attachment is linked to a field name 'file' if (isMultipleFiles) { os.write(CR_LF); os.write("Content-type: multipart/mixed, boundary=").write(SUB_BOUNDARY).write(CR_LF).write(CR_LF); writeBoundary(os, SUB_BOUNDARY).write(CR_LF); } else { os.write("; filename=\"" + files[0].getName() + "\"").write(CR_LF); writeContentType(os, files[0]); os.write(CR_LF); // Close part header } long count = 0; long total = 0; for (File file : files) { total += file.length(); } for (File file : files) { if (isMultipleFiles) { os.write("Content-disposition: attachment; filename=\"").write(file.getName()).write("\"").write(CR_LF); writeContentType(os, file); os.write(CR_LF);// Close part header } long length = file.length();// This is necessary if we are sending a log file and that more logs are getting appended. InputStream is = new FileInputStream(file); try { int read; byte[] b = new byte[8192]; while ((read = is.read(b)) > 0 && length > 0) { os.write(b, 0, (int) Math.min(read, length)); length -= read; count += read; if (progress != null) { progress.setProgress((double) count / total); } } if (isMultipleFiles) { os.write(CR_LF); writeBoundary(os, SUB_BOUNDARY).write(TWO_HYPHENS).write(CR_LF); } } finally { is.close(); } } // End of transfer os.write(CR_LF); writeBoundary(os, BOUNDARY).write(TWO_HYPHENS).write(CR_LF).write(CR_LF); InputStream is = new BufferedInputStream(connection.getInputStream()); try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); int read = -1; while ((read = is.read()) > -1) { baos.write(read); } // System.err.println(new String(baos.toByteArray(), "UTF-8")); } finally { is.close(); } } private void writeContentType(UTF8OutputStream os, File file) throws UnsupportedEncodingException, IOException { String type = FileTypeMap.getDefaultFileTypeMap().getContentType(file); if (type == null) { type = "attachment/octet-stream"; } os.write("Content-Type: ").write(type).write(CR_LF); } public UTF8OutputStream writeBoundary(UTF8OutputStream os, String boundary) throws IOException, UnsupportedEncodingException { os.write(TWO_HYPHENS); os.write(boundary); return os; } private String getBase64EncodedAuthentication() throws UnsupportedEncodingException { // Ok, it took me a while to find out but ISO-8859-1 is the one used by JIRA return Base64.encodeBase64String((username + ":" + password).getBytes("ISO-8859-1")); } public int getTimeout() { return timeout; } public void setTimeout(int connectionTimeout) { this.timeout = connectionTimeout; } public static class UTF8OutputStream extends OutputStream { private OutputStream os; protected UTF8OutputStream(OutputStream os) { super(); this.os = os; } @Override public void write(int b) throws IOException { os.write(b); } public UTF8OutputStream write(String string) throws UnsupportedEncodingException, IOException { write(string.getBytes("UTF-8")); return this; } } }