• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2022 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.net.apihelpers;
6 
7 import androidx.annotation.Nullable;
8 
9 import org.chromium.net.CronetException;
10 import org.chromium.net.UrlResponseInfo;
11 
12 import java.io.ByteArrayOutputStream;
13 import java.nio.ByteBuffer;
14 import java.nio.channels.Channels;
15 import java.nio.channels.WritableByteChannel;
16 import java.util.LinkedHashSet;
17 import java.util.List;
18 import java.util.Set;
19 
20 /**
21  * An abstract Cronet callback that reads the entire body to memory and optionally deserializes the
22  * body before passing it back to the issuer of the HTTP request.
23  *
24  * <p>The requester can subscribe for updates about the request by adding completion mListeners on
25  * the callback. When the request reaches a terminal state, the mListeners are informed in order of
26  * addition.
27  *
28  * @param <T> the response body type
29  */
30 public abstract class InMemoryTransformCronetCallback<T> extends ImplicitFlowControlCallback {
31     private static final String CONTENT_LENGTH_HEADER_NAME = "Content-Length";
32     // See ArrayList.MAX_ARRAY_SIZE for reasoning.
33     private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
34 
35     private ByteArrayOutputStream mResponseBodyStream;
36     private WritableByteChannel mResponseBodyChannel;
37 
38     /** The set of listeners observing the associated request. */
39     private final Set<CronetRequestCompletionListener<? super T>> mListeners =
40             new LinkedHashSet<>();
41 
42     /**
43      * Transforms (deserializes) the plain full body into a user-defined object.
44      *
45      * <p>It is assumed that the implementing classes handle edge cases (such as empty and malformed
46      * bodies) appropriately. Cronet doesn't inspects the objects and passes them (or any
47      * exceptions) along to the issuer of the request.
48      */
transformBodyBytes(UrlResponseInfo info, byte[] bodyBytes)49     protected abstract T transformBodyBytes(UrlResponseInfo info, byte[] bodyBytes);
50 
51     /**
52      * Adds a completion listener. All listeners are informed when the request reaches a terminal
53      * state, in order of addition. If a listener is added multiple times, it will only be called
54      * once according to the first time it was added.
55      *
56      * @see CronetRequestCompletionListener
57      */
addCompletionListener( CronetRequestCompletionListener<? super T> listener)58     public ImplicitFlowControlCallback addCompletionListener(
59             CronetRequestCompletionListener<? super T> listener) {
60         mListeners.add(listener);
61         return this;
62     }
63 
64     @Override
onResponseStarted(UrlResponseInfo info)65     protected final void onResponseStarted(UrlResponseInfo info) {
66         long bodyLength = getBodyLength(info);
67         if (bodyLength > MAX_ARRAY_SIZE) {
68             throw new IllegalArgumentException(
69                     "The body is too large and wouldn't fit in a byte array!");
70         }
71         // bodyLength returns -1 if the header can't be parsed, also ignore obviously bogus values
72         if (bodyLength >= 0) {
73             mResponseBodyStream = new ByteArrayOutputStream((int) bodyLength);
74         } else {
75             mResponseBodyStream = new ByteArrayOutputStream();
76         }
77         mResponseBodyChannel = Channels.newChannel(mResponseBodyStream);
78     }
79 
80     @Override
onBodyChunkRead(UrlResponseInfo info, ByteBuffer bodyChunk)81     protected final void onBodyChunkRead(UrlResponseInfo info, ByteBuffer bodyChunk)
82             throws Exception {
83         mResponseBodyChannel.write(bodyChunk);
84     }
85 
86     @Override
onSucceeded(UrlResponseInfo info)87     protected final void onSucceeded(UrlResponseInfo info) {
88         T body = transformBodyBytes(info, mResponseBodyStream.toByteArray());
89         for (CronetRequestCompletionListener<? super T> callback : mListeners) {
90             callback.onSucceeded(info, body);
91         }
92     }
93 
94     @Override
onFailed(@ullable UrlResponseInfo info, CronetException exception)95     protected final void onFailed(@Nullable UrlResponseInfo info, CronetException exception) {
96         for (CronetRequestCompletionListener<? super T> callback : mListeners) {
97             callback.onFailed(info, exception);
98         }
99     }
100 
101     @Override
onCanceled(@ullable UrlResponseInfo info)102     protected final void onCanceled(@Nullable UrlResponseInfo info) {
103         for (CronetRequestCompletionListener<? super T> callback : mListeners) {
104             callback.onCanceled(info);
105         }
106     }
107 
108     /** Returns the numerical value of the Content-Length header, or -1 if not set or invalid. */
getBodyLength(UrlResponseInfo info)109     private static long getBodyLength(UrlResponseInfo info) {
110         List<String> contentLengthHeader = info.getAllHeaders().get(CONTENT_LENGTH_HEADER_NAME);
111         if (contentLengthHeader == null || contentLengthHeader.size() != 1) {
112             return -1;
113         }
114         try {
115             return Long.parseLong(contentLengthHeader.get(0));
116         } catch (NumberFormatException e) {
117             return -1;
118         }
119     }
120 }
121