Bug 1533840 - Add convenience methods for dealing with WebRequest/WebResponse bodies r=geckoview-reviewers,agi,rbarker
☠☠ backed out by 0ec36abbdb31 ☠ ☠
authorJames Willcox <snorp@snorp.net>
Thu, 14 Mar 2019 20:18:33 +0000
changeset 464066 4fd7ce83efe1cad3f2ab3686cc9b45671b4cd0d5
parent 464065 2f2cefc2a5f7f8490640ed5ce875204bec8e9911
child 464067 e6e7b9953ede5e1435019e9ee80441ba144fab11
push id35707
push userrmaries@mozilla.com
push dateFri, 15 Mar 2019 03:42:43 +0000
treeherdermozilla-central@5ce27c44f79e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgeckoview-reviewers, agi, rbarker
bugs1533840
milestone67.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1533840 - Add convenience methods for dealing with WebRequest/WebResponse bodies r=geckoview-reviewers,agi,rbarker Differential Revision: https://phabricator.services.mozilla.com/D22748
mobile/android/geckoview/api.txt
mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExecutorTest.kt
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebRequest.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebResponse.java
mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
--- a/mobile/android/geckoview/api.txt
+++ b/mobile/android/geckoview/api.txt
@@ -952,16 +952,19 @@ package org.mozilla.geckoview {
     field public final int cacheMode;
     field @android.support.annotation.NonNull public final java.lang.String method;
     field @android.support.annotation.Nullable public final java.lang.String referrer;
   }
 
   @android.support.annotation.AnyThread public static class WebRequest.Builder extends org.mozilla.geckoview.WebMessage.Builder {
     ctor public Builder(@android.support.annotation.NonNull java.lang.String);
     method @android.support.annotation.NonNull public org.mozilla.geckoview.WebRequest.Builder body(@android.support.annotation.Nullable java.nio.ByteBuffer);
+    method @android.support.annotation.NonNull public org.mozilla.geckoview.WebRequest.Builder body(@android.support.annotation.NonNull byte[]);
+    method @android.support.annotation.NonNull public org.mozilla.geckoview.WebRequest.Builder body(@android.support.annotation.NonNull java.lang.String);
+    method @android.support.annotation.NonNull public org.mozilla.geckoview.WebRequest.Builder body(@android.support.annotation.NonNull org.json.JSONObject);
     method @android.support.annotation.NonNull public org.mozilla.geckoview.WebRequest build();
     method @android.support.annotation.NonNull public org.mozilla.geckoview.WebRequest.Builder cacheMode(int);
     method @android.support.annotation.NonNull public org.mozilla.geckoview.WebRequest.Builder method(@android.support.annotation.NonNull java.lang.String);
     method @android.support.annotation.NonNull public org.mozilla.geckoview.WebRequest.Builder referrer(@android.support.annotation.Nullable java.lang.String);
   }
 
   public static interface WebRequest.CacheMode implements java.lang.annotation.Annotation {
   }
@@ -1008,16 +1011,19 @@ package org.mozilla.geckoview {
   public static interface WebRequestError.Error implements java.lang.annotation.Annotation {
   }
 
   public static interface WebRequestError.ErrorCategory implements java.lang.annotation.Annotation {
   }
 
   @android.support.annotation.AnyThread public class WebResponse extends org.mozilla.geckoview.WebMessage {
     ctor protected WebResponse(@android.support.annotation.NonNull org.mozilla.geckoview.WebResponse.Builder);
+    method @android.support.annotation.NonNull public org.mozilla.geckoview.GeckoResult<byte[]> byteArray();
+    method @android.support.annotation.NonNull public org.mozilla.geckoview.GeckoResult<org.json.JSONObject> json();
+    method @android.support.annotation.NonNull public org.mozilla.geckoview.GeckoResult<java.lang.String> text();
     field @android.support.annotation.Nullable public final java.io.InputStream body;
     field public final boolean redirected;
     field public final int statusCode;
   }
 
   @android.support.annotation.AnyThread public static class WebResponse.Builder extends org.mozilla.geckoview.WebMessage.Builder {
     ctor public Builder(@android.support.annotation.NonNull java.lang.String);
     method @android.support.annotation.NonNull public org.mozilla.geckoview.WebResponse.Builder body(@android.support.annotation.NonNull java.io.InputStream);
--- a/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExecutorTest.kt
+++ b/mobile/android/geckoview/src/androidTest/java/org/mozilla/geckoview/test/WebExecutorTest.kt
@@ -13,20 +13,16 @@ import android.support.test.Instrumentat
 import android.support.test.filters.MediumTest
 import android.support.test.filters.SdkSuppress
 import android.support.test.runner.AndroidJUnit4
 
 import java.math.BigInteger
 
 import java.net.URI
 
-import java.nio.ByteBuffer
-import java.nio.CharBuffer
-import java.nio.charset.Charset
-
 import java.security.MessageDigest
 
 import java.util.concurrent.CountDownLatch
 
 import org.hamcrest.MatcherAssert.assertThat
 import org.hamcrest.Matchers.*
 
 import org.json.JSONObject
@@ -39,16 +35,17 @@ import org.mozilla.geckoview.GeckoWebExe
 import org.mozilla.geckoview.WebRequest
 import org.mozilla.geckoview.WebRequestError
 import org.mozilla.geckoview.WebResponse
 
 import org.mozilla.geckoview.test.util.Environment
 import org.mozilla.geckoview.test.util.HttpBin
 import org.mozilla.geckoview.test.util.RuntimeCreator
 import java.net.UnknownHostException
+import java.nio.ByteBuffer
 
 @MediumTest
 @RunWith(AndroidJUnit4::class)
 class WebExecutorTest {
     companion object {
         val TEST_ENDPOINT: String = "http://localhost:4242"
     }
 
@@ -83,65 +80,55 @@ class WebExecutorTest {
     private fun fetch(request: WebRequest): WebResponse {
         return fetch(request, GeckoWebExecutor.FETCH_FLAGS_NONE)
     }
 
     private fun fetch(request: WebRequest, @GeckoWebExecutor.FetchFlags flags: Int): WebResponse {
         return executor.fetch(request, flags).poll(env.defaultTimeoutMillis)!!
     }
 
-    fun String.toDirectByteBuffer(): ByteBuffer {
-        val chars = CharBuffer.wrap(this)
-        val buffer = ByteBuffer.allocateDirect(this.length)
-        Charset.forName("UTF-8").newEncoder().encode(chars, buffer, true)
-
-        return buffer
-    }
-
     fun WebResponse.getBodyBytes(): ByteBuffer {
-        return ByteBuffer.wrap(body!!.readBytes())
+        return ByteBuffer.wrap(byteArray().poll()!!);
     }
 
     fun WebResponse.getJSONBody(): JSONObject {
-        val bytes = this.getBodyBytes()
-        val bodyString = Charset.forName("UTF-8").decode(bytes).toString()
-        return JSONObject(bodyString)
+        return json().poll()!!
     }
 
     @Test
     fun smoke() {
         val uri = "$TEST_ENDPOINT/anything"
-        val bodyString = "This is the POST data"
+        val requestBody = JSONObject("{ \"foo\": 42 }")
         val referrer = "http://foo/bar"
 
         val request = WebRequest.Builder(uri)
                 .method("POST")
                 .header("Header1", "Clobbered")
                 .header("Header1", "Value")
                 .addHeader("Header2", "Value1")
                 .addHeader("Header2", "Value2")
                 .referrer(referrer)
                 .header("Content-Type", "text/plain")
-                .body(bodyString.toDirectByteBuffer())
+                .body(requestBody)
                 .build()
 
         val response = fetch(request)
 
         assertThat("URI should match", response.uri, equalTo(uri))
         assertThat("Status could should match", response.statusCode, equalTo(200))
         assertThat("Content type should match", response.headers["Content-Type"], equalTo("application/json"))
         assertThat("Redirected should match", response.redirected, equalTo(false))
 
         val body = response.getJSONBody()
         assertThat("Method should match", body.getString("method"), equalTo("POST"))
         assertThat("Headers should match", body.getJSONObject("headers").getString("Header1"), equalTo("Value"))
         assertThat("Headers should match", body.getJSONObject("headers").getString("Header2"), equalTo("Value1, Value2"))
         assertThat("Headers should match", body.getJSONObject("headers").getString("Content-Type"), equalTo("text/plain"))
         assertThat("Referrer should match", body.getJSONObject("headers").getString("Referer"), equalTo(referrer))
-        assertThat("Data should match", body.getString("data"), equalTo(bodyString));
+        assertThat("Data should match", body.getString("data"), equalTo(requestBody.toString()));
     }
 
     @Test
     fun testFetchAsset() {
         val response = fetch(WebRequest("$TEST_ENDPOINT/assets/www/hello.html"))
         assertThat("Status should match", response.statusCode, equalTo(200))
         assertThat("Body should have bytes", response.getBodyBytes().remaining(), greaterThan(0))
     }
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebRequest.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebRequest.java
@@ -1,27 +1,30 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * vim: ts=4 sw=4 expandtab:
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.geckoview;
 
+import org.json.JSONObject;
+
 import org.mozilla.gecko.annotation.WrapForJNI;
 
 import android.support.annotation.AnyThread;
 import android.support.annotation.IntDef;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
 import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
 
 /**
  * WebRequest represents an HTTP[S] request. The typical pattern is to create instances of this
  * class via {@link WebRequest.Builder}, and fetch responses via {@link GeckoWebExecutor#fetch(WebRequest)}.
  */
 @WrapForJNI
 @AnyThread
 public class WebRequest extends WebMessage {
@@ -165,16 +168,52 @@ public class WebRequest extends WebMessa
             if (buffer != null && !buffer.isDirect()) {
                 throw new IllegalArgumentException("body must be directly allocated");
             }
             mBody = buffer;
             return this;
         }
 
         /**
+         * Set the body.
+         *
+         * @param bytes A non-null byte array.
+         * @return This Builder instance.
+         */
+        public @NonNull Builder body(final @NonNull byte[] bytes) {
+            final ByteBuffer buffer = ByteBuffer.allocateDirect(bytes.length);
+            buffer.put(bytes);
+
+            body(buffer);
+            return this;
+        }
+
+        /**
+         * Set the body.
+         *
+         * @param bodyString A non-null {@link String}.
+         * @return This Builder instance.
+         */
+        public @NonNull Builder body(final @NonNull String bodyString) {
+            body(bodyString.getBytes(Charset.forName("UTF-8")));
+            return this;
+        }
+
+        /**
+         * Set the body.
+         *
+         * @param object A non-null {@link JSONObject}.
+         * @return This Builder instance.
+         */
+        public @NonNull Builder body(final @NonNull JSONObject object) {
+            body(object.toString());
+            return this;
+        }
+
+        /**
          * Set the HTTP method.
          *
          * @param method The HTTP method String.
          * @return This Builder instance.
          */
         public @NonNull Builder method(final @NonNull String method) {
             mMethod = method;
             return this;
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebResponse.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/WebResponse.java
@@ -1,32 +1,40 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * vim: ts=4 sw=4 expandtab:
  * This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 package org.mozilla.geckoview;
 
+import org.json.JSONObject;
 import org.mozilla.gecko.annotation.WrapForJNI;
 
 import android.support.annotation.AnyThread;
 import android.support.annotation.NonNull;
 import android.support.annotation.Nullable;
 
+import java.io.ByteArrayOutputStream;
 import java.io.InputStream;
 import java.nio.ByteBuffer;
+import java.nio.charset.Charset;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
 
 /**
  * WebResponse represents an HTTP[S] response. It is normally created
  * by {@link GeckoWebExecutor#fetch(WebRequest)}.
  */
 @WrapForJNI
 @AnyThread
 public class WebResponse extends WebMessage {
+    private static final ExecutorService sExecutorService = Executors.newCachedThreadPool();
+    private static final int BUFSIZE = 8192;
+
     /**
      * The HTTP status code for the response, e.g. 200.
      */
     public final int statusCode;
 
     /**
      * A boolean indicating whether or not this response is
      * the result of a redirection.
@@ -41,16 +49,87 @@ public class WebResponse extends WebMess
     protected WebResponse(final @NonNull Builder builder) {
         super(builder);
         this.statusCode = builder.mStatusCode;
         this.redirected = builder.mRedirected;
         this.body = builder.mBody;
     }
 
     /**
+     * Reads the {@link #body} stream into a byte array.
+     *
+     * @return A {@link GeckoResult} which resolves to a byte array containing the response body.
+     */
+    public @NonNull GeckoResult<byte[]> byteArray() {
+        if (body == null) {
+            return GeckoResult.fromValue(new byte[0]);
+        }
+
+        final GeckoResult<byte[]> result = new GeckoResult<>();
+        sExecutorService.submit(() -> {
+            final ByteArrayOutputStream os = new ByteArrayOutputStream();
+            final byte buf[] = new byte[BUFSIZE];
+            int count;
+
+            try {
+                while ((count = body.read(buf)) > 0) {
+                    os.write(buf, 0, count);
+                }
+
+                os.flush();
+
+                result.complete(os.toByteArray());
+            } catch (Exception e) {
+                result.completeExceptionally(e);
+            }
+        });
+
+        return result;
+    }
+
+    /**
+     * Reads the {@link #body} stream into a {@link String}.
+     *
+     * @return A {@link GeckoResult} which resolves to a {@link String} containing the response body.
+     */
+    public @NonNull GeckoResult<String> text() {
+        final GeckoResult<String> result = new GeckoResult<>();
+
+        sExecutorService.submit(() -> {
+            try {
+                final ByteBuffer bytes = ByteBuffer.wrap(byteArray().poll());
+                result.complete(Charset.forName("UTF-8").decode(bytes).toString());
+            } catch (Throwable t) {
+                result.completeExceptionally(t);
+            }
+        });
+
+        return result;
+    }
+
+    /**
+     * Reads the {@link #body} stream into a {@link JSONObject}.
+     *
+     * @return A {@link GeckoResult} which resolves to a {@link JSONObject} containing the response body.
+     */
+    public @NonNull GeckoResult<JSONObject> json() {
+        final GeckoResult<JSONObject> result = new GeckoResult<>();
+
+        sExecutorService.submit(() -> {
+            try {
+                result.complete(new JSONObject(text().poll()));
+            } catch (Throwable t) {
+                result.completeExceptionally(t);
+            }
+        });
+
+        return result;
+    }
+
+    /**
      * Builder offers a convenient way to create WebResponse instances.
      */
     @WrapForJNI
     @AnyThread
     public static class Builder extends WebMessage.Builder {
         /* package */ int mStatusCode;
         /* package */ boolean mRedirected;
         /* package */ InputStream mBody;
--- a/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/geckoview/doc-files/CHANGELOG.md
@@ -90,16 +90,30 @@ exclude: true
 
 - Added `default` implementations for all non-functional `interface`s.
 
 - Added [`ContentDelegate.onWebAppManifest`][67.22], which will deliver the contents of a parsed
   and validated Web App Manifest on pages that contain one.
 
 [67.22]: ../GeckoSession.ContentDelegate.html#onWebAppManifest-org.mozilla.geckoview.GeckoSession-org.json.JSONObject
 
+- Added [`WebResponse.byteArray()`][67.22], [`WebResponse.text()`][67.23], and
+  [`WebResponse.json()`][67.24] as convenience methods for accessing the response body.
+
+[67.23]: ../WebResponse.html#byteArray--
+[67.24]: ../WebResponse.html#text--
+[67.25]: ../WebResponse.html#json--
+
+- Added [`byte[]`][67.25], [`String`][67.26], and [`JSONObject`][67.27] overloads of
+  `WebRequest.Body.Builder.body()` as convenience methods for setting the request body.
+
+[67.26]: ../WebRequest.Builder.html#body-byte:A-
+[67.27]: ../WebRequest.Builder.html#body-java.lang.String-
+[67.28]: ../WebRequest.Builder.html#body-org.json.JSONObject-
+
 ## v66
 - Removed redundant field `trackingMode` from [`SecurityInformation`][66.6].
   Use `TrackingProtectionDelegate.onTrackerBlocked` for notification of blocked
   elements during page load.
 
 [66.6]: ../GeckoSession.ProgressDelegate.SecurityInformation.html
 
 - Added [`@NonNull`][66.1] or [`@Nullable`][66.2] to all APIs.
@@ -209,9 +223,9 @@ exclude: true
 [65.23]: ../GeckoSession.FinderResult.html
 
 - Update [`CrashReporter#sendCrashReport`][65.24] to return the crash ID as a
   [`GeckoResult<String>`][65.25].
 
 [65.24]: ../CrashReporter.html#sendCrashReport-android.content.Context-android.os.Bundle-java.lang.String-
 [65.25]: ../GeckoResult.html
 
-[api-version]: 910e94d6c3ec93faf7ba4ef73c4746b86e77b1c4
+[api-version]: bf7527ba15aeda260dbfb8dd134e963ab062fba8