Result XML Reporter

- Adds another flag called "result-server" where results
  will be posted to using a HTTP POST request.

- Extracts the multipart form handling code into a separate
  MultipartForm data class.

- Fix a bug in the copying code where the code was writing
  the full buffer all the time rather than how many bytes
  were read.

Change-Id: Ic7a857653b3af7028abc2d564667185e1818904c
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java
index 855c209..686c90a 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/CtsXmlResultReporter.java
@@ -84,6 +84,9 @@
     @Option(name = "quiet-output", description = "Mute display of test results.")
     private boolean mQuietOutput = false;
 
+    @Option(name = "result-server", description = "Server to publish test results.")
+    private String mResultServer;
+
     protected IBuildInfo mBuildInfo;
     private String mStartTime;
     private String mDeviceSerial;
@@ -254,9 +257,18 @@
             CLog.w("Unable to create XML report");
             return;
         }
-        createXmlResult(mReportDir, mStartTime, elapsedTime);
+
+        File reportFile = getResultFile(mReportDir);
+        createXmlResult(reportFile, mStartTime, elapsedTime);
         copyFormattingFiles(mReportDir);
         zipResults(mReportDir);
+
+        try {
+            ResultReporter reporter = new ResultReporter(mResultServer, reportFile);
+            reporter.reportResult();
+        } catch (IOException e) {
+            CLog.e(e);
+        }
     }
 
     private void logResult(String format, Object... args) {
@@ -281,12 +293,11 @@
     /**
      * Creates a report file and populates it with the report data from the completed tests.
      */
-    private void createXmlResult(File reportDir, String startTimestamp, long elapsedTime) {
+    private void createXmlResult(File reportFile, String startTimestamp, long elapsedTime) {
         String endTime = getTimestamp();
-
         OutputStream stream = null;
         try {
-            stream = createOutputResultStream(reportDir);
+            stream = createOutputResultStream(reportFile);
             KXmlSerializer serializer = new KXmlSerializer();
             serializer.setOutput(stream, "UTF-8");
             serializer.startDocument("UTF-8", false);
@@ -331,11 +342,14 @@
         //serializer.endTag(ns, RESULT_TAG);
     }
 
+    private File getResultFile(File reportDir) {
+        return new File(reportDir, TEST_RESULT_FILE_NAME);
+    }
+
     /**
      * Creates the output stream to use for test results. Exposed for mocking.
      */
-    OutputStream createOutputResultStream(File reportDir) throws IOException {
-        File reportFile = new File(reportDir, TEST_RESULT_FILE_NAME);
+    OutputStream createOutputResultStream(File reportFile) throws IOException {
         logResult("Created xml report file at file://%s", reportFile.getAbsolutePath());
         return new FileOutputStream(reportFile);
     }
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/IssueReporter.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/IssueReporter.java
index 8ee9c0f..9d903dd 100644
--- a/tools/tradefed-host/src/com/android/cts/tradefed/result/IssueReporter.java
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/IssueReporter.java
@@ -28,11 +28,6 @@
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.io.OutputStream;
-import java.io.OutputStreamWriter;
-import java.io.PrintWriter;
-import java.net.HttpURLConnection;
-import java.net.URL;
 import java.util.Map;
 import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutorService;
@@ -46,7 +41,6 @@
  */
 public class IssueReporter implements ITestInvocationListener {
 
-    private static final String FORM_DATA_BOUNDARY = "C75I55u3R3p0r73r";
     private static final int BUGREPORT_SIZE = 500 * 1024;
 
     private static final String PRODUCT_NAME_KEY = "buildName";
@@ -87,22 +81,42 @@
      */
     private void setBugReport(InputStreamSource dataStream) throws IOException {
         if (mCurrentIssue != null) {
-            InputStream input = dataStream.createInputStream();
-            ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(BUGREPORT_SIZE);
-            GZIPOutputStream gzipOutput = new GZIPOutputStream(byteOutput);
-            for (byte[] buffer = new byte[1024]; input.read(buffer) >= 0; ) {
-                gzipOutput.write(buffer);
-            }
-            gzipOutput.close();
-
             // Only one bug report can be stored at a time and they are gzipped to
             // about 0.5 MB so there shoudn't be any memory leak bringing down CTS.
-            mCurrentIssue.mBugReport = byteOutput.toByteArray();
+            InputStream input = null;
+            try {
+                input = dataStream.createInputStream();
+                mCurrentIssue.mBugReport = getBytes(input, BUGREPORT_SIZE);
+            } finally {
+                if (input != null) {
+                    input.close();
+                }
+            }
         } else {
             CLog.e("setBugReport is getting called on an empty issue...");
         }
     }
 
+    /**
+     * @param input that will be gzipped and returne as a byte array
+     * @param size of the output expected
+     * @return the byte array with the input's data
+     * @throws IOException
+     */
+    static byte[] getBytes(InputStream input, int size) throws IOException {
+        ByteArrayOutputStream byteOutput = new ByteArrayOutputStream(size);
+        GZIPOutputStream gzipOutput = new GZIPOutputStream(byteOutput);
+        for (byte[] buffer = new byte[1024]; ; ) {
+            int numRead = input.read(buffer);
+            if (numRead < 0) {
+                break;
+            }
+            gzipOutput.write(buffer, 0, numRead);
+        }
+        gzipOutput.close();
+        return byteOutput.toByteArray();
+    }
+
     @Override
     public void testEnded(TestIdentifier test, Map<String, String> testMetrics) {
         if (mCurrentIssue != null) {
@@ -158,32 +172,14 @@
                 return null;
             }
 
-            HttpURLConnection connection = null;
-
-            try {
-                URL url = new URL(mServerUrl);
-                connection = (HttpURLConnection) url.openConnection();
-                connection.setRequestMethod("POST");
-                connection.setDoOutput(true);
-                connection.setRequestProperty("Content-Type",
-                        "multipart/form-data; boundary=" + FORM_DATA_BOUNDARY);
-
-                byte[] body = getContentBody();
-                connection.setRequestProperty("Content-Length", Integer.toString(body.length));
-
-                OutputStream output = connection.getOutputStream();
-                output.write(body);
-                output.close();
-
-                // Open the stream to get a response. Otherwise request will be cancelled.
-                InputStream input = connection.getInputStream();
-                input.close();
-
-            } finally {
-                if (connection != null) {
-                    connection.disconnect();
-                }
-            }
+            new MultipartForm(mServerUrl)
+                    .addFormValue("productName", mProductName)
+                    .addFormValue("buildType", mBuildType)
+                    .addFormValue("buildId", mBuildId)
+                    .addFormValue("testName", mTestName)
+                    .addFormValue("stackTrace", mStackTrace)
+                    .addFormFile("bugReport", "bugreport.txt.gz", mBugReport)
+                    .submit();
 
             return null;
         }
@@ -191,43 +187,6 @@
         private boolean isEmpty(String value) {
             return value == null || value.trim().isEmpty();
         }
-
-        private byte[] getContentBody() throws IOException {
-            ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
-            PrintWriter writer = new PrintWriter(new OutputStreamWriter(byteOutput));
-            writer.println();
-            writeFormField(writer, "productName", mProductName);
-            writeFormField(writer, "buildType", mBuildType);
-            writeFormField(writer, "buildId", mBuildId);
-            writeFormField(writer, "testName", mTestName);
-            writeFormField(writer, "stackTrace", mStackTrace);
-            if (mBugReport != null) {
-                writeFormFileHeader(writer, "bugReport", "bugReport.txt.gz");
-                writer.flush(); // Must flush here before writing to the byte stream!
-                byteOutput.write(mBugReport);
-                writer.println();
-            }
-            writer.append("--").append(FORM_DATA_BOUNDARY).println("--");
-            writer.flush();
-            writer.close();
-            return byteOutput.toByteArray();
-        }
-
-        private void writeFormField(PrintWriter writer, String name, String value) {
-            writer.append("--").println(FORM_DATA_BOUNDARY);
-            writer.append("Content-Disposition: form-data; name=\"").append(name).println("\"");
-            writer.println();
-            writer.println(value);
-        }
-
-        private void writeFormFileHeader(PrintWriter writer, String name, String fileName) {
-            writer.append("--").println(FORM_DATA_BOUNDARY);
-            writer.append("Content-Disposition: form-data; name=\"").append(name);
-            writer.append("\"; filename=\"").append(fileName).println("\"");
-            writer.println("Content-Type: application/x-gzip");
-            writer.println("Content-Transfer-Encoding: binary");
-            writer.println();
-        }
     }
 
     @Override
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/MultipartForm.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/MultipartForm.java
new file mode 100644
index 0000000..f3ef0bb
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/MultipartForm.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.tradefed.result;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.Map;
+
+/** MultipartForm builds a multipart form and submits it. */
+class MultipartForm {
+
+    private static final String FORM_DATA_BOUNDARY = "C75I55u3R3p0r73r";
+
+    private final String mServerUrl;
+
+    private final Map<String, String> mFormValues = new HashMap<String, String>();
+
+    private String mName;
+    private String mFileName;
+    private byte[] mData;
+
+    public MultipartForm(String serverUrl) {
+        mServerUrl = serverUrl;
+    }
+
+    public MultipartForm addFormValue(String name, String value) {
+        mFormValues.put(name, value);
+        return this;
+    }
+
+    public MultipartForm addFormFile(String name, String fileName, byte[] data) {
+        mName = name;
+        mFileName = fileName;
+        mData = data;
+        return this;
+    }
+
+    public void submit() throws IOException {
+        String redirectUrl = submitForm(mServerUrl);
+        if (redirectUrl != null) {
+            submitForm(redirectUrl);
+        }
+    }
+
+    /**
+     * @param serverUrl to post the data to
+     * @return a url if the server redirected to another url
+     * @throws IOException
+     */
+    private String submitForm(String serverUrl) throws IOException {
+        HttpURLConnection connection = null;
+        try {
+            URL url = new URL(serverUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setInstanceFollowRedirects(false);
+            connection.setRequestMethod("POST");
+            connection.setDoOutput(true);
+            connection.setRequestProperty("Content-Type",
+                    "multipart/form-data; boundary=" + FORM_DATA_BOUNDARY);
+
+            byte[] body = getContentBody();
+            connection.setRequestProperty("Content-Length", Integer.toString(body.length));
+
+            OutputStream output = connection.getOutputStream();
+            try {
+                output.write(body);
+            } finally {
+                output.close();
+            }
+
+            // Open the stream to get a response. Otherwise request will be cancelled.
+            InputStream input = connection.getInputStream();
+            input.close();
+
+            if (connection.getResponseCode() == 302) {
+                return connection.getHeaderField("Location");
+            }
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+
+        return null;
+    }
+
+    private byte[] getContentBody() throws IOException {
+        ByteArrayOutputStream byteOutput = new ByteArrayOutputStream();
+        PrintWriter writer = new PrintWriter(new OutputStreamWriter(byteOutput));
+        writer.println();
+
+        for (Map.Entry<String, String> formValue : mFormValues.entrySet()) {
+            writeFormField(writer, formValue.getKey(), formValue.getValue());
+        }
+
+        if (mData != null) {
+            writeFormFileHeader(writer, mName, mFileName);
+            writer.flush(); // Must flush here before writing to the byte stream!
+            byteOutput.write(mData);
+            writer.println();
+        }
+        writer.append("--").append(FORM_DATA_BOUNDARY).println("--");
+        writer.flush();
+        writer.close();
+        return byteOutput.toByteArray();
+    }
+
+    private void writeFormField(PrintWriter writer, String name, String value) {
+        writer.append("--").println(FORM_DATA_BOUNDARY);
+        writer.append("Content-Disposition: form-data; name=\"").append(name).println("\"");
+        writer.println();
+        writer.println(value);
+    }
+
+    private void writeFormFileHeader(PrintWriter writer, String name, String fileName) {
+        writer.append("--").println(FORM_DATA_BOUNDARY);
+        writer.append("Content-Disposition: form-data; name=\"").append(name);
+        writer.append("\"; filename=\"").append(fileName).println("\"");
+        writer.println("Content-Type: application/x-gzip");
+        writer.println("Content-Transfer-Encoding: binary");
+        writer.println();
+    }
+}
diff --git a/tools/tradefed-host/src/com/android/cts/tradefed/result/ResultReporter.java b/tools/tradefed-host/src/com/android/cts/tradefed/result/ResultReporter.java
new file mode 100644
index 0000000..05192c9
--- /dev/null
+++ b/tools/tradefed-host/src/com/android/cts/tradefed/result/ResultReporter.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.cts.tradefed.result;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Class that sends a HTTP POST multipart/form-data request containing
+ * the test result XML.
+ */
+class ResultReporter {
+
+    private static final int RESULT_XML_BYTES = 500 * 1024;
+
+    private final String mServerUrl;
+
+    private final File mReportFile;
+
+    ResultReporter(String serverUrl, File reportFile) {
+        mServerUrl = serverUrl;
+        mReportFile = reportFile;
+    }
+
+    public void reportResult() throws IOException {
+        if (isEmpty(mServerUrl)) {
+            return;
+        }
+
+        InputStream input = new FileInputStream(mReportFile);
+        try {
+            byte[] data = IssueReporter.getBytes(input, RESULT_XML_BYTES);
+            new MultipartForm(mServerUrl)
+                    .addFormFile("resultXml", "testResult.xml.gz", data)
+                    .submit();
+        } finally {
+            input.close();
+        }
+    }
+
+    private boolean isEmpty(String value) {
+        return value == null || value.trim().isEmpty();
+    }
+}