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