Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / components / cronet / android / java / src / org / chromium / net / CronetUrlRequest.java
1 // Copyright 2014 The Chromium Authors. All rights reserved.
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;
6
7 import android.util.Log;
8
9 import org.chromium.base.CalledByNative;
10 import org.chromium.base.JNINamespace;
11
12 import java.nio.ByteBuffer;
13 import java.util.AbstractMap;
14 import java.util.ArrayList;
15 import java.util.HashMap;
16 import java.util.List;
17 import java.util.Map;
18 import java.util.concurrent.Executor;
19
20 /**
21  * UrlRequest using Chromium HTTP stack implementation. Could be accessed from
22  * any thread on Executor. Cancel can be done from any thread.
23  * All @CallByNative methods are called on native network thread
24  * and post tasks with listener calls onto Executor. Upon return from listener
25  * callback native request adapter is called on executive thread and posts
26  * native tasks to native network thread. Because Cancel could be called from
27  * any thread it is protected by mUrlRequestAdapterLock.
28  */
29 @JNINamespace("cronet")
30 final class CronetUrlRequest implements UrlRequest {
31     /* Native adapter object, owned by UrlRequest. */
32     private long mUrlRequestAdapter;
33     private boolean mStarted = false;
34     private boolean mCanceled = false;
35     private boolean mInOnDataReceived = false;
36
37     /*
38      * Synchronize access to mUrlRequestAdapter, mStarted, mCanceled and
39      * mDestroyAfterReading.
40      */
41     private final Object mUrlRequestAdapterLock = new Object();
42     private final CronetUrlRequestContext mRequestContext;
43     private final Executor mExecutor;
44
45     /*
46      * Url chain contans the URL currently being requested, and
47      * all URLs previously requested.  New URLs are only added after it is
48      * decided a redirect will be followed.
49      */
50     private final List<String> mUrlChain = new ArrayList<String>();
51
52     private final UrlRequestListener mListener;
53     private final String mInitialUrl;
54     private final int mPriority;
55     private String mInitialMethod;
56     private final HeadersList mRequestHeaders = new HeadersList();
57
58     private NativeResponseInfo mResponseInfo;
59
60     /*
61      * Listener callback is repeatedly called when data is received, so it is
62      * cached as member variable.
63      */
64     private OnDataReceivedRunnable mOnDataReceivedTask;
65
66     static final class HeaderEntry extends
67             AbstractMap.SimpleEntry<String, String> {
68         public HeaderEntry(String name, String value) {
69             super(name, value);
70         }
71     }
72
73     static final class HeadersList extends ArrayList<HeaderEntry> {
74     }
75
76     final class OnDataReceivedRunnable implements Runnable {
77         ByteBuffer mByteBuffer;
78
79         public void run() {
80             if (isCanceled()) {
81                 return;
82             }
83             try {
84                 synchronized (mUrlRequestAdapterLock) {
85                     if (mUrlRequestAdapter == 0) {
86                         return;
87                     }
88                     // mByteBuffer is direct buffer backed by native memory,
89                     // and passed to listener, so the request adapter cannot
90                     // be destroyed while listener has access to it.
91                     // Set |mInOnDataReceived| flag during the call to listener
92                     // and destroy adapter immediately after if request was
93                     // cancelled during the call from listener or other thread.
94                     mInOnDataReceived = true;
95                 }
96                 mListener.onDataReceived(CronetUrlRequest.this,
97                         mResponseInfo, mByteBuffer);
98                 mByteBuffer = null;
99                 synchronized (mUrlRequestAdapterLock) {
100                     mInOnDataReceived = false;
101                     if (isCanceled()) {
102                         destroyRequestAdapter();
103                         return;
104                     }
105                     nativeReceiveData(mUrlRequestAdapter);
106                 }
107             } catch (Exception e) {
108                 synchronized (mUrlRequestAdapterLock) {
109                     mInOnDataReceived = false;
110                     if (isCanceled()) {
111                         destroyRequestAdapter();
112                     }
113                 }
114                 onListenerException(e);
115             }
116         }
117     }
118
119     static final class NativeResponseInfo implements ResponseInfo {
120         private final String[] mResponseInfoUrlChain;
121         private final int mHttpStatusCode;
122         private final HeadersMap mAllHeaders = new HeadersMap();
123         private final boolean mWasCached;
124         private final String mNegotiatedProtocol;
125
126         NativeResponseInfo(String[] urlChain, int httpStatusCode,
127                 boolean wasCached, String negotiatedProtocol) {
128             mResponseInfoUrlChain = urlChain;
129             mHttpStatusCode = httpStatusCode;
130             mWasCached = wasCached;
131             mNegotiatedProtocol = negotiatedProtocol;
132         }
133
134         @Override
135         public String getUrl() {
136             return mResponseInfoUrlChain[mResponseInfoUrlChain.length - 1];
137         }
138
139         @Override
140         public String[] getUrlChain() {
141             return mResponseInfoUrlChain;
142         }
143
144         @Override
145         public int getHttpStatusCode() {
146             return mHttpStatusCode;
147         }
148
149         @Override
150         public Map<String, List<String>> getAllHeaders() {
151             return mAllHeaders;
152         }
153
154         @Override
155         public boolean wasCached() {
156             return mWasCached;
157         }
158
159         @Override
160         public String getNegotiatedProtocol() {
161             return mNegotiatedProtocol;
162         }
163     };
164
165     static final class NativeExtendedResponseInfo implements
166             ExtendedResponseInfo {
167         private final ResponseInfo mResponseInfo;
168         private final long mTotalReceivedBytes;
169
170         NativeExtendedResponseInfo(ResponseInfo responseInfo,
171                                    long totalReceivedBytes) {
172             mResponseInfo = responseInfo;
173             mTotalReceivedBytes = totalReceivedBytes;
174         }
175
176         @Override
177         public ResponseInfo getResponseInfo() {
178             return mResponseInfo;
179         }
180
181         @Override
182         public long getTotalReceivedBytes() {
183             return mTotalReceivedBytes;
184         }
185     };
186
187     CronetUrlRequest(CronetUrlRequestContext requestContext,
188             long urlRequestContextAdapter,
189             String url,
190             int priority,
191             UrlRequestListener listener,
192             Executor executor) {
193         if (url == null) {
194             throw new NullPointerException("URL is required");
195         }
196         if (listener == null) {
197             throw new NullPointerException("Listener is required");
198         }
199         if (executor == null) {
200             throw new NullPointerException("Executor is required");
201         }
202
203         mRequestContext = requestContext;
204         mInitialUrl = url;
205         mUrlChain.add(url);
206         mPriority = convertRequestPriority(priority);
207         mListener = listener;
208         mExecutor = executor;
209     }
210
211     @Override
212     public void setHttpMethod(String method) {
213         checkNotStarted();
214         if (method == null) {
215             throw new NullPointerException("Method is required.");
216         }
217         mInitialMethod = method;
218     }
219
220     @Override
221     public void addHeader(String header, String value) {
222         checkNotStarted();
223         if (header == null) {
224             throw new NullPointerException("Invalid header name.");
225         }
226         if (value == null) {
227             throw new NullPointerException("Invalid header value.");
228         }
229         mRequestHeaders.add(new HeaderEntry(header, value));
230     }
231
232     @Override
233     public void start() {
234         synchronized (mUrlRequestAdapterLock) {
235             checkNotStarted();
236             mUrlRequestAdapter = nativeCreateRequestAdapter(
237                     mRequestContext.getUrlRequestContextAdapter(),
238                     mInitialUrl,
239                     mPriority);
240             mRequestContext.onRequestStarted(this);
241             if (mInitialMethod != null) {
242                 if (!nativeSetHttpMethod(mUrlRequestAdapter, mInitialMethod)) {
243                     destroyRequestAdapter();
244                     throw new IllegalArgumentException("Invalid http method "
245                             + mInitialMethod);
246                 }
247             }
248             for (HeaderEntry header : mRequestHeaders) {
249                 if (!nativeAddHeader(mUrlRequestAdapter, header.getKey(),
250                         header.getValue())) {
251                     destroyRequestAdapter();
252                     throw new IllegalArgumentException("Invalid header "
253                             + header.getKey() + "=" + header.getValue());
254                 }
255             }
256             mStarted = true;
257             nativeStart(mUrlRequestAdapter);
258         }
259     }
260
261     @Override
262     public void cancel() {
263         synchronized (mUrlRequestAdapterLock) {
264             if (mCanceled || !mStarted) {
265                 return;
266             }
267             mCanceled = true;
268             // During call into listener OnDataReceived adapter cannot be
269             // destroyed as it owns the byte buffer.
270             if (!mInOnDataReceived) {
271                 destroyRequestAdapter();
272             }
273         }
274     }
275
276     @Override
277     public boolean isCanceled() {
278         synchronized (mUrlRequestAdapterLock) {
279             return mCanceled;
280         }
281     }
282
283     @Override
284     public void pause() {
285         throw new UnsupportedOperationException("Not implemented yet");
286     }
287
288     @Override
289     public boolean isPaused() {
290         return false;
291     }
292
293     @Override
294     public void resume() {
295         throw new UnsupportedOperationException("Not implemented yet");
296     }
297
298     /**
299      * Post task to application Executor. Used for Listener callbacks
300      * and other tasks that should not be executed on network thread.
301      */
302     private void postTaskToExecutor(Runnable task) {
303         mExecutor.execute(task);
304     }
305
306     private static int convertRequestPriority(int priority) {
307         switch (priority) {
308             case REQUEST_PRIORITY_IDLE:
309                 return RequestPriority.IDLE;
310             case REQUEST_PRIORITY_LOWEST:
311                 return RequestPriority.LOWEST;
312             case REQUEST_PRIORITY_LOW:
313                 return RequestPriority.LOW;
314             case REQUEST_PRIORITY_MEDIUM:
315                 return RequestPriority.MEDIUM;
316             case REQUEST_PRIORITY_HIGHEST:
317                 return RequestPriority.HIGHEST;
318             default:
319                 return RequestPriority.MEDIUM;
320         }
321     }
322
323     private NativeResponseInfo prepareResponseInfoOnNetworkThread(
324             int httpStatusCode) {
325         long urlRequestAdapter;
326         synchronized (mUrlRequestAdapterLock) {
327             if (mUrlRequestAdapter == 0) {
328                 return null;
329             }
330             // This method is running on network thread, so even if
331             // mUrlRequestAdapter is set to 0 from another thread the actual
332             // deletion of the adapter is posted to network thread, so it is
333             // safe to preserve and use urlRequestAdapter outside the lock.
334             urlRequestAdapter = mUrlRequestAdapter;
335         }
336         NativeResponseInfo responseInfo = new NativeResponseInfo(
337                 mUrlChain.toArray(new String[mUrlChain.size()]),
338                 httpStatusCode,
339                 nativeGetWasCached(urlRequestAdapter),
340                 nativeGetNegotiatedProtocol(urlRequestAdapter));
341         nativePopulateResponseHeaders(urlRequestAdapter,
342                                       responseInfo.mAllHeaders);
343         return responseInfo;
344     }
345
346     private void checkNotStarted() {
347         synchronized (mUrlRequestAdapterLock) {
348             if (mStarted || isCanceled()) {
349                 throw new IllegalStateException("Request is already started.");
350             }
351         }
352     }
353
354     private void destroyRequestAdapter() {
355         synchronized (mUrlRequestAdapterLock) {
356             if (mUrlRequestAdapter == 0) {
357                 return;
358             }
359             nativeDestroyRequestAdapter(mUrlRequestAdapter);
360             mRequestContext.onRequestDestroyed(this);
361             mUrlRequestAdapter = 0;
362         }
363     }
364
365     /**
366      * If listener method throws an exception, request gets canceled
367      * and exception is reported via onFailed listener callback.
368      * Only called on the Executor.
369      */
370     private void onListenerException(Exception e) {
371         UrlRequestException requestError = new UrlRequestException(
372                 "CalledByNative method has thrown an exception", e);
373         Log.e(CronetUrlRequestContext.LOG_TAG,
374                 "Exception in CalledByNative method", e);
375         // Do not call into listener if request is canceled.
376         if (isCanceled()) {
377             return;
378         }
379         try {
380             cancel();
381             mListener.onFailed(this, mResponseInfo, requestError);
382         } catch (Exception cancelException) {
383             Log.e(CronetUrlRequestContext.LOG_TAG,
384                     "Exception trying to cancel request", cancelException);
385         }
386     }
387
388     ////////////////////////////////////////////////
389     // Private methods called by the native code.
390     // Always called on network thread.
391     ////////////////////////////////////////////////
392
393     /**
394      * Called before following redirects. The redirect will automatically be
395      * followed, unless the request is paused or canceled during this
396      * callback. If the redirect response has a body, it will be ignored.
397      * This will only be called between start and onResponseStarted.
398      *
399      * @param newLocation Location where request is redirected.
400      * @param httpStatusCode from redirect response
401      */
402     @SuppressWarnings("unused")
403     @CalledByNative
404     private void onRedirect(final String newLocation, int httpStatusCode) {
405         final NativeResponseInfo responseInfo =
406                 prepareResponseInfoOnNetworkThread(httpStatusCode);
407         Runnable task = new Runnable() {
408             public void run() {
409                 if (isCanceled()) {
410                     return;
411                 }
412                 try {
413                     mListener.onRedirect(CronetUrlRequest.this, responseInfo,
414                             newLocation);
415                     synchronized (mUrlRequestAdapterLock) {
416                         if (isCanceled()) {
417                             return;
418                         }
419                         // It is Ok to access mUrlChain not on the network
420                         // thread as the request is waiting to follow redirect.
421                         mUrlChain.add(newLocation);
422                         nativeFollowDeferredRedirect(mUrlRequestAdapter);
423                     }
424                 } catch (Exception e) {
425                     onListenerException(e);
426                 }
427             }
428         };
429         postTaskToExecutor(task);
430     }
431
432     /**
433      * Called when the final set of headers, after all redirects,
434      * is received. Can only be called once for each request.
435      */
436     @SuppressWarnings("unused")
437     @CalledByNative
438     private void onResponseStarted(int httpStatusCode) {
439         mResponseInfo = prepareResponseInfoOnNetworkThread(httpStatusCode);
440         Runnable task = new Runnable() {
441             public void run() {
442                 if (isCanceled()) {
443                     return;
444                 }
445                 try {
446                     mListener.onResponseStarted(CronetUrlRequest.this,
447                                                 mResponseInfo);
448                     synchronized (mUrlRequestAdapterLock) {
449                         if (isCanceled()) {
450                             return;
451                         }
452                         nativeReceiveData(mUrlRequestAdapter);
453                     }
454                 } catch (Exception e) {
455                     onListenerException(e);
456                 }
457             }
458         };
459         postTaskToExecutor(task);
460     }
461
462     /**
463      * Called whenever data is received. The ByteBuffer remains
464      * valid only until listener callback. Or if the callback
465      * pauses the request, it remains valid until the request is resumed.
466      * Cancelling the request also invalidates the buffer.
467      *
468      * @param byteBuffer Received data.
469      */
470     @SuppressWarnings("unused")
471     @CalledByNative
472     private void onDataReceived(final ByteBuffer byteBuffer) {
473         if (mOnDataReceivedTask == null) {
474             mOnDataReceivedTask = new OnDataReceivedRunnable();
475         }
476         mOnDataReceivedTask.mByteBuffer = byteBuffer;
477         postTaskToExecutor(mOnDataReceivedTask);
478     }
479
480     /**
481      * Called when request is completed successfully, no callbacks will be
482      * called afterwards.
483      */
484     @SuppressWarnings("unused")
485     @CalledByNative
486     private void onSucceeded() {
487         long totalReceivedBytes;
488         synchronized (mUrlRequestAdapterLock) {
489             if (mUrlRequestAdapter == 0) {
490                 return;
491             }
492             totalReceivedBytes =
493                     nativeGetTotalReceivedBytes(mUrlRequestAdapter);
494         }
495
496         final NativeExtendedResponseInfo extendedResponseInfo =
497                 new NativeExtendedResponseInfo(mResponseInfo,
498                         totalReceivedBytes);
499         Runnable task = new Runnable() {
500             public void run() {
501                 synchronized (mUrlRequestAdapterLock) {
502                     if (isCanceled()) {
503                         return;
504                     }
505                     // Destroy adapter first, so request context could be shut
506                     // down from the listener.
507                     destroyRequestAdapter();
508                 }
509                 try {
510                     mListener.onSucceeded(CronetUrlRequest.this,
511                                           extendedResponseInfo);
512                 } catch (Exception e) {
513                     Log.e(CronetUrlRequestContext.LOG_TAG,
514                             "Exception in onComplete method", e);
515                 }
516             }
517         };
518         postTaskToExecutor(task);
519     }
520
521     /**
522      * Called when error has occured, no callbacks will be called afterwards.
523      *
524      * @param nativeError native net error code.
525      * @param errorString textual representation of the error code.
526      */
527     @SuppressWarnings("unused")
528     @CalledByNative
529     private void onError(final int nativeError, final String errorString) {
530         Runnable task = new Runnable() {
531             public void run() {
532                 if (isCanceled()) {
533                     return;
534                 }
535                 // Destroy adapter first, so request context could be shut down
536                 // from the listener.
537                 destroyRequestAdapter();
538                 try {
539                     UrlRequestException requestError = new UrlRequestException(
540                             "Exception in CronetUrlRequest: " + errorString,
541                             nativeError);
542                     mListener.onFailed(CronetUrlRequest.this,
543                                        mResponseInfo,
544                                        requestError);
545                 } catch (Exception e) {
546                     Log.e(CronetUrlRequestContext.LOG_TAG,
547                             "Exception in onError method", e);
548                 }
549             }
550         };
551         postTaskToExecutor(task);
552     }
553
554     /**
555      * Appends header |name| with value |value| to |headersMap|.
556      */
557     @SuppressWarnings("unused")
558     @CalledByNative
559     private void onAppendResponseHeader(HeadersMap headersMap,
560             String name, String value) {
561         try {
562             if (!headersMap.containsKey(name)) {
563                 headersMap.put(name, new ArrayList<String>());
564             }
565             headersMap.get(name).add(value);
566         } catch (final Exception e) {
567             Log.e(CronetUrlRequestContext.LOG_TAG,
568                     "Exception in onAppendResponseHeader method", e);
569         }
570     }
571
572     // Native methods are implemented in cronet_url_request.cc.
573
574     private native long nativeCreateRequestAdapter(
575             long urlRequestContextAdapter, String url, int priority);
576
577     private native boolean nativeAddHeader(long urlRequestAdapter, String name,
578             String value);
579
580     private native boolean nativeSetHttpMethod(long urlRequestAdapter,
581             String method);
582
583     private native void nativeStart(long urlRequestAdapter);
584
585     private native void nativeDestroyRequestAdapter(long urlRequestAdapter);
586
587     private native void nativeFollowDeferredRedirect(long urlRequestAdapter);
588
589     private native void nativeReceiveData(long urlRequestAdapter);
590
591     private native void nativePopulateResponseHeaders(long urlRequestAdapter,
592             HeadersMap headers);
593
594     private native String nativeGetNegotiatedProtocol(long urlRequestAdapter);
595
596     private native boolean nativeGetWasCached(long urlRequestAdapter);
597
598     private native long nativeGetTotalReceivedBytes(long urlRequestAdapter);
599
600     // Explicit class to work around JNI-generator generics confusion.
601     private static class HeadersMap extends HashMap<String, List<String>> {
602     }
603 }