/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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.google.j2objc.net;
import com.google.j2objc.io.AsyncPipedNSInputStreamAdapter;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.net.CookieHandler;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/*-[
#include "NSDataInputStream.h"
#include "NSDataOutputStream.h"
#include "NSDictionaryMap.h"
#include "com/google/j2objc/net/NSErrorException.h"
#include "java/lang/Double.h"
#include "java/net/ConnectException.h"
#include "java/net/MalformedURLException.h"
#include "java/net/UnknownHostException.h"
#include "java/net/SocketTimeoutException.h"
#include "java/util/logging/Level.h"
#include "java/util/logging/Logger.h"
]-*/
/**
* HttpURLConnection implementation for iOS, using NSURLSession.
*
* @author Tom Ball
*/
public class IosHttpURLConnection extends HttpURLConnection {
/** Represents the current NSURLSessionDataTask. Guarded by nativeDataTaskLock. */
private Object nativeDataTask; // NSURLSessionDataTask.
/** Used to guard the release of the data task. */
private final Object nativeDataTaskLock = new Object();
/**
* If chunked transfer is not enabled, the OutputStream we offer to the writer is just an,
* NSDataOutputStream that accumulates bytes until the stream is closed.
*/
private OutputStream nativeDataOutputStream; // NSDataOutputStream.
/**
* If chunked transfer is enabled, we need to give the writer an OutputStream backed by a queue,
* and then pipe that stream to nativePipedRequestStream.
*/
private DataEnqueuedOutputStream requestStream;
/** The NSInputStream from AsyncPipedNSInputStreamAdapter. */
private Object nativePipedRequestStream; // NSInputStream.
/** An InputStream for reading the response body. Guarded by responseBodyStreamLock. */
private DataEnqueuedInputStream responseBodyStream;
/** Used to guard responseBodyStream. */
private final Object responseBodyStreamLock = new Object();
/** An InputStream for reading the error response. */
private InputStream errorDataStream;
private List<HeaderEntry> headers = new ArrayList<HeaderEntry>();
// Cache response failure, so multiple requests to a bad response throw the same exception.
private IOException responseException;
/** Guards both responseCode and responseException and is used to block getResponse(). */
private final Object getResponseLock = new Object();
// Delegate to handle native security data, to avoid a direct dependency on jre_security.
private final SecurityDataHandler securityDataHandler;
private static final int NATIVE_PIPED_STREAM_BUFFER_SIZE = 8192;
private static final String HTTP_DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss zzz";
private static final Map<Integer,String> RESPONSE_CODES = new HashMap<Integer,String>();
// A case-insensitive comparator that supports a null key, so headers with
// keys that only differ by case can be coalesced using a TreeMap.
private static final Comparator<String> HEADER_KEY_COMPARATOR =
new Comparator<String>() {
@Override
public int compare(String lhs, String rhs) {
if (lhs == null || rhs == null) {
if (rhs != null) {
return -1;
}
if (lhs != null) {
return 1;
}
return 0;
}
return String.CASE_INSENSITIVE_ORDER.compare(lhs, rhs);
}
};
public IosHttpURLConnection(URL url) {
this(url, null);
}
IosHttpURLConnection(URL url, SecurityDataHandler handler) {
super(url);
securityDataHandler = handler;
}
@Override
public void disconnect() {
connected = false;
// Cancel the data task if it's still running.
cancelDataTask();
synchronized (responseBodyStreamLock) {
if (responseBodyStream != null) {
// Close the responseBodyStream from the offering side if it still exists.
responseBodyStream.endOffering();
}
responseBodyStream = null;
}
nativeDataOutputStream = null;
requestStream = null;
nativePipedRequestStream = null;
errorDataStream = null;
// Unblock any remaining pending getResponse(), if for any reason cancelDataTask() fails.
synchronized (getResponseLock) {
responseException = null;
getResponseLock.notifyAll();
}
}
/**
* Enable chunked streaming mode. The chunk length doesn't matter since NSURLSession does not give
* us control over the chunk size.
*
* @param chunklen ignored.
*/
@Override
public void setChunkedStreamingMode(int chunklen) {
super.setChunkedStreamingMode(chunklen);
}
/**
* This method has no effect on iOS. NSURLSession only sends the Content-Length header field is
* the HTTP request body is a data or an NSInputStream from a file (which we don't support), and
* it always removes the Content-Length and sets Transfer-Encoding to chunked if a generic stream
* is supplied as the request body.
*
* <p>The implication here is that you should never use this method. If you use this method to
* attempt to send 5 MB of data to the OutputStream you obtain from {@link #getOutputStream()}, at
* least 5 MB of data (if not more) will have to be allocated just to be able to send it with the
* Content-Length field filled. Instead, call {@link #setChunkedStreamingMode(int)} and use the
* chunked streaming mode, although this also means your server has to have chunked transfer
* encoding support.
*
* @param contentLength ignored.
*/
@Override
public void setFixedLengthStreamingMode(long contentLength) {
super.setFixedLengthStreamingMode(contentLength);
}
@Override
public boolean usingProxy() {
return false;
}
@Override
public InputStream getInputStream() throws IOException {
if (!doInput) {
throw new ProtocolException("This protocol does not support input");
}
getResponse();
if (responseCode >= HTTP_BAD_REQUEST) {
throw new FileNotFoundException(url.toString());
}
return responseBodyStream;
}
@Override
public void connect() throws IOException {
if (responseCode != -1) {
// Request already made.
return;
}
if (responseException != null) {
throw responseException;
}
if (connected) {
return;
}
connected = true;
int timeout = getReadTimeout();
responseBodyStream = new DataEnqueuedInputStream(timeout > 0 ? timeout : -1);
loadRequestCookies();
makeRequest();
}
@Override
public Map<String, List<String>> getHeaderFields() {
try {
getResponse();
return getHeaderFieldsDoNotForceResponse();
} catch (IOException e) {
return Collections.emptyMap();
}
}
private Map<String, List<String>> getHeaderFieldsDoNotForceResponse() {
Map<String, List<String>> map = new TreeMap<String, List<String>>(HEADER_KEY_COMPARATOR);
for (HeaderEntry entry : headers) {
String k = entry.getKey();
String v = entry.getValue();
List<String> values = map.get(k);
if (values == null) {
values = new ArrayList<String>();
map.put(k, values);
}
values.add(v);
}
return Collections.unmodifiableMap(map);
}
private List<HeaderEntry> getResponseHeaders() throws IOException {
getResponse();
return headers;
}
@Override
public String getHeaderField(int pos) {
try {
List<HeaderEntry> headers = getResponseHeaders();
return pos < headers.size() ? headers.get(pos).getValue() : null;
} catch (IOException e) {
return null;
}
}
/**
* Returns the value of the named header field.
*
* If called on a connection that sets the same header multiple times with
* possibly different values, only the last value is returned.
*/
@Override
public String getHeaderField(String key) {
try {
getResponse();
return getHeaderFieldDoNotForceResponse(key);
} catch(IOException e) {
return null;
}
}
private String getHeaderFieldDoNotForceResponse(String key) {
for (int i = headers.size() - 1; i >= 0; i--) {
HeaderEntry entry = headers.get(i);
if (key == null) {
if (entry.getKey() == null) {
return entry.getValue();
}
continue;
}
if (entry.getKey() != null && key.equalsIgnoreCase(entry.getKey())) {
return entry.getValue();
}
}
return null;
}
@Override
public long getHeaderFieldDate(String field, long defaultValue) {
String dateString = getHeaderField(field);
try {
SimpleDateFormat format = new SimpleDateFormat(HTTP_DATE_FORMAT);
Date d = format.parse(dateString);
return d.getTime();
} catch (ParseException e) {
return defaultValue;
}
}
@Override
public String getHeaderFieldKey(int posn) {
try {
List<HeaderEntry> headers = getResponseHeaders();
return posn < headers.size() ? headers.get(posn).getKey() : null;
} catch (IOException e) {
return null;
}
}
@Override
public int getHeaderFieldInt(String field, int defaultValue) {
String intString = getHeaderField(field);
try {
return Integer.parseInt(intString);
} catch (NumberFormatException e) {
return defaultValue;
}
}
@Override
public long getHeaderFieldLong(String field, long defaultValue) {
String longString = getHeaderField(field);
return headerValueToLong(longString, defaultValue);
}
private long getHeaderFieldLongDoNotForceResponse(String field, long defaultValue) {
String longString = getHeaderFieldDoNotForceResponse(field);
return headerValueToLong(longString, defaultValue);
}
private long headerValueToLong(String value, long defaultValue) {
try {
return Long.parseLong(value);
} catch (NumberFormatException e) {
return defaultValue;
}
}
@Override
public final Map<String, List<String>> getRequestProperties() {
if (connected) {
throw new IllegalStateException(
"Cannot access request header fields after connection is set");
}
return getHeaderFieldsDoNotForceResponse();
}
@Override
public void setRequestProperty(String field, String newValue) {
if (connected) {
throw new IllegalStateException("Cannot set request property after connection is made");
}
if (field == null) {
throw new NullPointerException("field == null");
}
setHeader(field, newValue);
}
@Override
public void addRequestProperty(String field, String newValue) {
if (connected) {
throw new IllegalStateException("Cannot add request property after connection is made");
}
if (field == null) {
throw new NullPointerException("field == null");
}
addHeader(field, newValue);
}
@Override
public String getRequestProperty(String field) {
if (field == null) {
return null;
}
return getHeaderFieldDoNotForceResponse(field);
}
@Override
public InputStream getErrorStream() {
return errorDataStream;
}
@Override
public long getIfModifiedSince() {
return getHeaderFieldLongDoNotForceResponse("If-Modified-Since", 0L);
}
@Override
public void setIfModifiedSince(long newValue) {
super.setIfModifiedSince(newValue);
if (ifModifiedSince != 0) {
SimpleDateFormat format = new SimpleDateFormat(HTTP_DATE_FORMAT);
String dateString = format.format(new Date(ifModifiedSince));
setHeader("If-Modified-Since", dateString);
} else {
removeHeader("If-Modified-Since");
}
}
@Override
public OutputStream getOutputStream() throws IOException {
if (connected) {
throw new IllegalStateException("Cannot get output stream after connection is made");
}
// If {@link java.net.HttpURLConnection#setChunkedStreamingMode(int)} is ever called to enable
// chunked streaming mode, create a queue-backed OutputStream and pipe that to a native
// NSInputStream, which we can then use as the HTTP request body stream.
if (chunkLength > 0) {
if (requestStream == null) {
int timeout = getReadTimeout();
requestStream = new DataEnqueuedOutputStream(timeout > 0 ? timeout : -1);
}
if (nativePipedRequestStream == null) {
nativePipedRequestStream =
AsyncPipedNSInputStreamAdapter.create(requestStream, NATIVE_PIPED_STREAM_BUFFER_SIZE);
}
connect();
return requestStream;
} else {
if (nativeDataOutputStream == null) {
nativeDataOutputStream = createNativeDataOutputStream();
}
return nativeDataOutputStream;
}
}
private native OutputStream createNativeDataOutputStream() /*-[
return (JavaIoOutputStream *) [NSDataOutputStream stream];
]-*/;
private void getResponse() throws IOException {
if (responseCode != -1) {
// Request already made.
return;
}
connect();
synchronized (getResponseLock) {
if (responseCode == -1 && responseException == null) {
try {
// There are three places where getResponseLock.notifyAll() is called: in disconnect(), in
// the native -URLSession:dataTask:didReceiveResponse:, and in the native
// -URLSession:task:didCompleteWithError:. The call in disconnect() is just a clean-up
// step, where as -URLSession:dataTask:didReceiveResponse: is called only if the
// connection is made and an HTTP response code is received. If a non-server error occurs
// (such as lost connection), -URLSession:task:didCompleteWithError: will be called and an
// exception will be set there. So the only condition we are waiting for here is if both
// responseCode and responseExecption are not set.
getResponseLock.wait();
} catch (InterruptedException e) {
// Ignored.
}
}
}
if (responseException != null) {
throw responseException;
}
saveResponseCookies();
}
/**
* Add any cookies for this URI to the request headers.
*/
private void loadRequestCookies() throws IOException {
CookieHandler cookieHandler = CookieHandler.getDefault();
if (cookieHandler != null) {
try {
URI uri = getURL().toURI();
Map<String, List<String>> cookieHeaders =
cookieHandler.get(uri, getHeaderFieldsDoNotForceResponse());
for (Map.Entry<String, List<String>> entry : cookieHeaders.entrySet()) {
String key = entry.getKey();
if (("Cookie".equalsIgnoreCase(key)
|| "Cookie2".equalsIgnoreCase(key))
&& !entry.getValue().isEmpty()) {
List<String> cookies = entry.getValue();
StringBuilder sb = new StringBuilder();
for (int i = 0, size = cookies.size(); i < size; i++) {
if (i > 0) {
sb.append("; ");
}
sb.append(cookies.get(i));
}
setHeader(key, sb.toString());
}
}
} catch (URISyntaxException e) {
throw new IOException(e);
}
}
}
/**
* Store any returned cookies.
*/
private void saveResponseCookies() throws IOException {
CookieHandler cookieHandler = CookieHandler.getDefault();
if (cookieHandler != null) {
try {
URI uri = getURL().toURI();
cookieHandler.put(uri, getHeaderFieldsDoNotForceResponse());
} catch (URISyntaxException e) {
throw new IOException(e);
}
}
}
@Override
public int getResponseCode() throws IOException {
getResponse();
return responseCode;
}
private native void makeRequest() throws IOException /*-[
@autoreleasepool {
NSMutableURLRequest *request =
[NSMutableURLRequest requestWithURL:[NSURL URLWithString:[self->url_ toExternalForm]]];
request.HTTPShouldHandleCookies = false;
request.cachePolicy = self->useCaches_ ?
NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
int readTimeout = [self getReadTimeout];
request.timeoutInterval = readTimeout > 0 ? (readTimeout / 1000.0) : JavaLangDouble_MAX_VALUE;
int n = [self->headers_ size];
for (int i = 0; i < n; i++) {
ComGoogleJ2objcNetIosHttpURLConnection_HeaderEntry *entry = [self->headers_ getWithInt:i];
if (entry->key_) {
[request setValue:[entry getValue] forHTTPHeaderField:entry->key_];
}
}
if (self->doOutput_) {
if ([self->method_ isEqualToString:@"GET"]) {
self->method_ = @"POST"; // GET doesn't support output, so assume POST.
} else if (![self->method_ isEqualToString:@"POST"] &&
![self->method_ isEqualToString:@"PUT"] &&
![self->method_ isEqualToString:@"PATCH"]) {
NSString *errMsg =
[NSString stringWithFormat:@"%@ does not support writing", self->method_];
self->responseException_ = [[JavaNetProtocolException alloc] initWithNSString:errMsg];
@throw self->responseException_;
}
if (self->nativeDataOutputStream_) {
// Use the accumululated data as the request body.
request.HTTPBody = [(NSDataOutputStream *) self->nativeDataOutputStream_ data];
} else if (self->nativePipedRequestStream_) {
// Use the piped NSInputStream as the request stream.
request.HTTPBodyStream = self->nativePipedRequestStream_;
}
}
request.HTTPMethod = self->method_;
NSURLSessionConfiguration *sessionConfiguration =
[NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session =
[NSURLSession sessionWithConfiguration:sessionConfiguration
delegate:(id<NSURLSessionDataDelegate>)self
delegateQueue:nil];
NSURLSessionTask *task = [session dataTaskWithRequest:request];
[task resume];
JreStrongAssign(&self->nativeDataTask_, task);
[session finishTasksAndInvalidate];
}
]-*/;
/*
* NSURLSessionDataDelegate method: initial reply received.
*/
/*-[
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)urlResponse
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
if (urlResponse && ![urlResponse isKindOfClass:[NSHTTPURLResponse class]]) {
@throw AUTORELEASE(([[JavaLangAssertionError alloc]
initWithId:[NSString stringWithFormat:@"Unknown class %@",
NSStringFromClass([urlResponse class])]]));
}
NSHTTPURLResponse *response = (NSHTTPURLResponse *) urlResponse;
int responseCode = (int) response.statusCode;
JavaNetHttpURLConnection_set_responseMessage_(self,
ComGoogleJ2objcNetIosHttpURLConnection_getResponseStatusTextWithInt_(responseCode));
// Clear request headers to make room for the response headers.
[self->headers_ clear];
// The HttpURLConnection headerFields map uses a null key for Status-Line.
NSString *statusLine = [NSString stringWithFormat:@"HTTP/1.1 %d %@", responseCode,
self->responseMessage_];
[self addHeaderWithNSString:nil withNSString:statusLine];
// Copy remaining response headers.
[response.allHeaderFields enumerateKeysAndObjectsUsingBlock:
^(id key, id value, BOOL *stop) {
[self addHeaderWithNSString:key withNSString:value];
}];
if (response.statusCode >= JavaNetHttpURLConnection_HTTP_BAD_REQUEST) {
// Make errorDataStream an alias to responseBodyStream. Since getInputStream() throws an
// exception when status code >= HTTP_BAD_REQUEST, it is guaranteed that responseBodyStream
// can only mean error stream going forward.
JreStrongAssign(&self->errorDataStream_, self->responseBodyStream_);
}
completionHandler(NSURLSessionResponseAllow);
// Since the original request might have been redirected, we might need to
// update the URL to the redirected URL.
JavaNetURLConnection_set_url_(
self, create_JavaNetURL_initWithNSString_(response.URL.absoluteString));
// Unblock getResponse().
@synchronized(getResponseLock_) {
self->responseCode_ = responseCode;
[self->getResponseLock_ java_notifyAll];
}
}
]-*/
/*
* NSURLSessionDataDelegate method: data received.
*/
/*-[
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data {
@synchronized(responseBodyStreamLock_) {
[self->responseBodyStream_ offerDataWithByteArray:[IOSByteArray arrayWithNSData:data]];
}
}
]-*/
/*
* NSURLSessionDataDelegate method: should we store the response in the cache.
*/
/*-[
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
willCacheResponse:(NSCachedURLResponse *)proposedResponse
completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
completionHandler( self->useCaches_ ? proposedResponse : nil );
}
]-*/
/*
* NSURLSessionDelegate method: task completed.
*/
/*-[
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error {
JavaIoIOException *responseException = nil;
if (error) {
NSString *url = [self->url_ description]; // Use original URL in any error text.
if ([[error domain] isEqualToString:@"NSURLErrorDomain"]) {
switch ([error code]) {
case NSURLErrorBadURL:
responseException = create_JavaNetMalformedURLException_initWithNSString_(url);
break;
case NSURLErrorCannotConnectToHost:
responseException =
create_JavaNetConnectException_initWithNSString_([error description]);
break;
case NSURLErrorSecureConnectionFailed:
responseException = RETAIN_(
ComGoogleJ2objcNetIosHttpURLConnection_secureConnectionExceptionWithNSString_
([error description]));
break;
case NSURLErrorCannotFindHost:
responseException = create_JavaNetUnknownHostException_initWithNSString_(url);
break;
case NSURLErrorTimedOut:
responseException = create_JavaNetSocketTimeoutException_initWithNSString_(url);
break;
}
}
if (!responseException) {
responseException = create_JavaIoIOException_initWithNSString_([error description]);
}
ComGoogleJ2objcNetNSErrorException *cause =
create_ComGoogleJ2objcNetNSErrorException_initWithId_(error);
[responseException initCauseWithNSException:cause];
}
@synchronized(responseBodyStreamLock_) {
if (!responseException) {
// No error, close the responseBodyStream.
[self->responseBodyStream_ endOffering];
} else {
// Close responseBodyStream with the exception so that subsequent calls to read() cause the
// same exception to be thrown.
[self->responseBodyStream_ endOfferingWithJavaIoIOException:responseException];
}
}
// Set nativeDataTask to null.
@synchronized(nativeDataTaskLock_) {
JreStrongAssign(&self->nativeDataTask_, nil);
}
// Unblock getResponse() and set responseException. This call to notifyAll() is needed because
// -URLSession:dataTask:didReceiveResponse: may not be called if a non-server error (such as
// lost connection) occurs.
@synchronized(getResponseLock_) {
JreStrongAssign(&self->responseException_, responseException);
[self->getResponseLock_ java_notifyAll];
}
}
]-*/
/*
* NSURLSessionDelegate method: session completed.
*/
/*-[
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
if (error) {
// Cannot return error since this happened on another thread and the task
// finished, so just log it.
ComGoogleJ2objcNetNSErrorException *exception =
create_ComGoogleJ2objcNetNSErrorException_initWithId_(error);
JavaUtilLoggingLogger *logger = JavaUtilLoggingLogger_getLoggerWithNSString_(
[[self java_getClass] getName]);
[logger logWithJavaUtilLoggingLevel:JreLoadStatic(JavaUtilLoggingLevel, SEVERE)
withNSString:@"session invalidated with error"
withNSException:exception];
}
}
]-*/
/**
* Returns an SSLException if that class is linked into the application,
* otherwise IOException.
*/
private static IOException secureConnectionException(String description) {
try {
Class<?> sslExceptionClass = Class.forName("javax.net.ssl.SSLException");
Constructor<?> constructor = sslExceptionClass.getConstructor(String.class);
return (IOException) constructor.newInstance(description);
} catch (ClassNotFoundException e) {
return new IOException(description);
} catch (Exception e) {
throw new AssertionError("unexpected exception", e);
}
}
/*-[
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
willPerformHTTPRedirection:(NSHTTPURLResponse *)response
newRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSURLRequest *))completionHandler {
if (self->instanceFollowRedirects_
&& [response.URL.scheme isEqualToString:request.URL.scheme]) {
completionHandler(request);
} else {
completionHandler(nil);
}
}
]-*/
private void addHeader(String k, String v) {
if (k != null && (k.equalsIgnoreCase("Set-Cookie") || k.equalsIgnoreCase("Set-Cookie2"))) {
CookieSplitter cs = new CookieSplitter(v);
while (cs.hasNext()) {
headers.add(new HeaderEntry(k, cs.next()));
}
} else {
headers.add(new HeaderEntry(k, v));
}
}
private void removeHeader(String k) {
Iterator<HeaderEntry> iter = headers.iterator();
while (iter.hasNext()) {
HeaderEntry entry = iter.next();
if (entry.key == k) {
iter.remove();
return;
}
}
}
private void setHeader(String k, String v) {
for (HeaderEntry entry : headers) {
if (entry.key == k || (entry.key != null && k != null && k.equalsIgnoreCase(entry.key))) {
headers.remove(entry);
break;
}
}
headers.add(new HeaderEntry(k, v));
}
private static class HeaderEntry implements Map.Entry<String, String> {
private final String key;
private final String value;
HeaderEntry(String k, String v) {
this.key = k;
this.value = v;
}
@Override
public String getKey() {
return key;
}
@Override
public String getValue() {
return value;
}
@Override
public String setValue(String object) {
throw new AssertionError("mutable method called on immutable class");
}
}
private static String getResponseStatusText(int responseCode) {
return RESPONSE_CODES.get(responseCode);
}
/** Cancels the native data task. */
private native void cancelDataTask() /*-[
@synchronized (self->nativeDataTaskLock_) {
// Safe to do even if self->nativeDataTask_ is already nil.
[(NSURLSessionDataTask *)self->nativeDataTask_ cancel];
JreStrongAssign(&self->nativeDataTask_, nil);
}
]-*/;
/**
* Splits a string with one or more cookies separated with a comma. Since
* some attribute values can also have commas, like the expires date field,
* this does a look ahead to detect where it's a separator or not.
*/
private static class CookieSplitter {
String s;
char[] buf;
int pos;
CookieSplitter(String s) {
this.s = s;
buf = s.toCharArray();
pos = 0;
}
boolean hasNext() {
return pos < buf.length;
}
String next() {
int start = pos;
while (skipSpace()) {
if (buf[pos] == ',') {
int lastComma = pos++;
skipSpace();
int nextStart = pos;
while (pos < buf.length && buf[pos] != '=' && buf[pos] != ';' && buf[pos] != ',') {
pos++;
}
if (pos < buf.length && buf[pos] == '=') {
// pos is inside the next cookie, so back up and return it.
pos = nextStart;
return s.substring(start, lastComma);
}
pos = lastComma;
}
pos++;
}
return s.substring(start);
}
// Skip whitespace, returning true if there are more chars to read.
private boolean skipSpace() {
while (pos < buf.length && Character.isWhitespace(buf[pos])) {
pos++;
}
return pos < buf.length;
}
}
static {
RESPONSE_CODES.put(100, "Continue");
RESPONSE_CODES.put(101, "Switching Protocols");
RESPONSE_CODES.put(102, "Processing");
RESPONSE_CODES.put(HTTP_OK, "OK");
RESPONSE_CODES.put(HTTP_CREATED, "Created");
RESPONSE_CODES.put(HTTP_ACCEPTED, "Accepted");
RESPONSE_CODES.put(HTTP_NOT_AUTHORITATIVE, "Non Authoritative Information");
RESPONSE_CODES.put(HTTP_NO_CONTENT, "No Content");
RESPONSE_CODES.put(HTTP_RESET, "Reset Content");
RESPONSE_CODES.put(HTTP_PARTIAL, "Partial Content");
RESPONSE_CODES.put(207, "Multi-Status");
RESPONSE_CODES.put(HTTP_MULT_CHOICE, "Multiple Choices");
RESPONSE_CODES.put(HTTP_MOVED_PERM, "Moved Permanently");
RESPONSE_CODES.put(HTTP_MOVED_TEMP, "Moved Temporarily");
RESPONSE_CODES.put(HTTP_SEE_OTHER, "See Other");
RESPONSE_CODES.put(HTTP_NOT_MODIFIED, "Not Modified");
RESPONSE_CODES.put(HTTP_USE_PROXY, "Use Proxy");
RESPONSE_CODES.put(307, "Temporary Redirect");
RESPONSE_CODES.put(HTTP_BAD_REQUEST, "Bad Request");
RESPONSE_CODES.put(HTTP_UNAUTHORIZED, "Unauthorized");
RESPONSE_CODES.put(HTTP_PAYMENT_REQUIRED, "Payment Required");
RESPONSE_CODES.put(HTTP_FORBIDDEN, "Forbidden");
RESPONSE_CODES.put(HTTP_NOT_FOUND, "Not Found");
RESPONSE_CODES.put(HTTP_BAD_METHOD, "Method Not Allowed");
RESPONSE_CODES.put(HTTP_NOT_ACCEPTABLE, "Not Acceptable");
RESPONSE_CODES.put(HTTP_PROXY_AUTH, "Proxy Authentication Required");
RESPONSE_CODES.put(HTTP_CLIENT_TIMEOUT, "Request Timeout");
RESPONSE_CODES.put(HTTP_CONFLICT, "Conflict");
RESPONSE_CODES.put(HTTP_GONE, "Gone");
RESPONSE_CODES.put(HTTP_LENGTH_REQUIRED, "Length Required");
RESPONSE_CODES.put(HTTP_PRECON_FAILED, "Precondition Failed");
RESPONSE_CODES.put(HTTP_ENTITY_TOO_LARGE, "Request Too Long");
RESPONSE_CODES.put(HTTP_REQ_TOO_LONG, "Request-URI Too Long");
RESPONSE_CODES.put(HTTP_UNSUPPORTED_TYPE, "Unsupported Media Type");
RESPONSE_CODES.put(416, "Requested Range Not Satisfiable");
RESPONSE_CODES.put(417, "Expectation Failed");
RESPONSE_CODES.put(418, "Unprocessable Entity");
RESPONSE_CODES.put(419, "Insufficient Space On Resource");
RESPONSE_CODES.put(420, "Method Failure");
RESPONSE_CODES.put(423, "Locked");
RESPONSE_CODES.put(424, "Failed Dependency");
RESPONSE_CODES.put(HTTP_INTERNAL_ERROR, "Internal Server Error");
RESPONSE_CODES.put(HTTP_NOT_IMPLEMENTED, "Not Implemented");
RESPONSE_CODES.put(HTTP_BAD_GATEWAY, "Bad Gateway");
RESPONSE_CODES.put(HTTP_UNAVAILABLE, "Service Unavailable");
RESPONSE_CODES.put(HTTP_GATEWAY_TIMEOUT, "Gateway Timeout");
RESPONSE_CODES.put(HTTP_VERSION, "Http Version Not Supported");
RESPONSE_CODES.put(507, "Insufficient Storage");
}
}