| @@ -0,0 +1,54 @@ | ||
| /* | ||
| 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. | ||
| */ | ||
|
|
||
|
|
||
|
|
||
| buildscript { | ||
| repositories { | ||
| mavenCentral() | ||
| } | ||
|
|
||
| dependencies { | ||
| classpath 'com.android.tools.build:gradle:0.10.+' | ||
| } | ||
| } | ||
|
|
||
| apply plugin: 'android-library' | ||
|
|
||
| android { | ||
| compileSdkVersion 19 | ||
| buildToolsVersion "19.0.0" | ||
|
|
||
| compileOptions { | ||
| sourceCompatibility JavaVersion.VERSION_1_7 | ||
| targetCompatibility JavaVersion.VERSION_1_7 | ||
| } | ||
|
|
||
| sourceSets { | ||
| main { | ||
| manifest.srcFile 'AndroidManifest.xml' | ||
| java.srcDirs = ['src'] | ||
| resources.srcDirs = ['src'] | ||
| aidl.srcDirs = ['src'] | ||
| renderscript.srcDirs = ['src'] | ||
| res.srcDirs = ['res'] | ||
| assets.srcDirs = ['assets'] | ||
| } | ||
| } | ||
| } |
| @@ -0,0 +1,16 @@ | ||
| # This file is automatically generated by Android Tools. | ||
| # Do not modify this file -- YOUR CHANGES WILL BE ERASED! | ||
| # | ||
| # This file must be checked in Version Control Systems. | ||
| # | ||
| # To customize properties used by the Ant build system use, | ||
| # "ant.properties", and override values to adapt the script to your | ||
| # project structure. | ||
|
|
||
| # Indicates whether an apk should be generated for each density. | ||
| split.density=false | ||
| # Project target. | ||
| target=android-19 | ||
| apk-configurations= | ||
| renderscript.opt.level=O0 | ||
| android.library=true |
| @@ -0,0 +1,140 @@ | ||
| /* | ||
| * Copyright (C) 2012 The Android Open Source Project | ||
| * | ||
| * Licensed 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.squareup.okhttp; | ||
|
|
||
| import com.squareup.okhttp.internal.Util; | ||
| import java.net.Proxy; | ||
| import java.net.UnknownHostException; | ||
| import java.util.List; | ||
| import javax.net.ssl.HostnameVerifier; | ||
| import javax.net.ssl.SSLSocketFactory; | ||
|
|
||
| import static com.squareup.okhttp.internal.Util.equal; | ||
|
|
||
| /** | ||
| * A specification for a connection to an origin server. For simple connections, | ||
| * this is the server's hostname and port. If an explicit proxy is requested (or | ||
| * {@link Proxy#NO_PROXY no proxy} is explicitly requested), this also includes | ||
| * that proxy information. For secure connections the address also includes the | ||
| * SSL socket factory and hostname verifier. | ||
| * | ||
| * <p>HTTP requests that share the same {@code Address} may also share the same | ||
| * {@link Connection}. | ||
| */ | ||
| public final class Address { | ||
| final Proxy proxy; | ||
| final String uriHost; | ||
| final int uriPort; | ||
| final SSLSocketFactory sslSocketFactory; | ||
| final HostnameVerifier hostnameVerifier; | ||
| final OkAuthenticator authenticator; | ||
| final List<String> transports; | ||
|
|
||
| public Address(String uriHost, int uriPort, SSLSocketFactory sslSocketFactory, | ||
| HostnameVerifier hostnameVerifier, OkAuthenticator authenticator, Proxy proxy, | ||
| List<String> transports) throws UnknownHostException { | ||
| if (uriHost == null) throw new NullPointerException("uriHost == null"); | ||
| if (uriPort <= 0) throw new IllegalArgumentException("uriPort <= 0: " + uriPort); | ||
| if (authenticator == null) throw new IllegalArgumentException("authenticator == null"); | ||
| if (transports == null) throw new IllegalArgumentException("transports == null"); | ||
| this.proxy = proxy; | ||
| this.uriHost = uriHost; | ||
| this.uriPort = uriPort; | ||
| this.sslSocketFactory = sslSocketFactory; | ||
| this.hostnameVerifier = hostnameVerifier; | ||
| this.authenticator = authenticator; | ||
| this.transports = Util.immutableList(transports); | ||
| } | ||
|
|
||
| /** Returns the hostname of the origin server. */ | ||
| public String getUriHost() { | ||
| return uriHost; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the port of the origin server; typically 80 or 443. Unlike | ||
| * may {@code getPort()} accessors, this method never returns -1. | ||
| */ | ||
| public int getUriPort() { | ||
| return uriPort; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the SSL socket factory, or null if this is not an HTTPS | ||
| * address. | ||
| */ | ||
| public SSLSocketFactory getSslSocketFactory() { | ||
| return sslSocketFactory; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the hostname verifier, or null if this is not an HTTPS | ||
| * address. | ||
| */ | ||
| public HostnameVerifier getHostnameVerifier() { | ||
| return hostnameVerifier; | ||
| } | ||
|
|
||
|
|
||
| /** | ||
| * Returns the client's authenticator. This method never returns null. | ||
| */ | ||
| public OkAuthenticator getAuthenticator() { | ||
| return authenticator; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the client's transports. This method always returns a non-null list | ||
| * that contains "http/1.1", possibly among other transports. | ||
| */ | ||
| public List<String> getTransports() { | ||
| return transports; | ||
| } | ||
|
|
||
| /** | ||
| * Returns this address's explicitly-specified HTTP proxy, or null to | ||
| * delegate to the HTTP client's proxy selector. | ||
| */ | ||
| public Proxy getProxy() { | ||
| return proxy; | ||
| } | ||
|
|
||
| @Override public boolean equals(Object other) { | ||
| if (other instanceof Address) { | ||
| Address that = (Address) other; | ||
| return equal(this.proxy, that.proxy) | ||
| && this.uriHost.equals(that.uriHost) | ||
| && this.uriPort == that.uriPort | ||
| && equal(this.sslSocketFactory, that.sslSocketFactory) | ||
| && equal(this.hostnameVerifier, that.hostnameVerifier) | ||
| && equal(this.authenticator, that.authenticator) | ||
| && equal(this.transports, that.transports); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| @Override public int hashCode() { | ||
| int result = 17; | ||
| result = 31 * result + uriHost.hashCode(); | ||
| result = 31 * result + uriPort; | ||
| result = 31 * result + (sslSocketFactory != null ? sslSocketFactory.hashCode() : 0); | ||
| result = 31 * result + (hostnameVerifier != null ? hostnameVerifier.hashCode() : 0); | ||
| result = 31 * result + (authenticator != null ? authenticator.hashCode() : 0); | ||
| result = 31 * result + (proxy != null ? proxy.hashCode() : 0); | ||
| result = 31 * result + transports.hashCode(); | ||
| return result; | ||
| } | ||
| } |
| @@ -0,0 +1,335 @@ | ||
| /* | ||
| * 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.squareup.okhttp; | ||
|
|
||
| import com.squareup.okhttp.internal.Platform; | ||
| import com.squareup.okhttp.internal.http.HttpAuthenticator; | ||
| import com.squareup.okhttp.internal.http.HttpEngine; | ||
| import com.squareup.okhttp.internal.http.HttpTransport; | ||
| import com.squareup.okhttp.internal.http.RawHeaders; | ||
| import com.squareup.okhttp.internal.http.SpdyTransport; | ||
| import com.squareup.okhttp.internal.spdy.SpdyConnection; | ||
| import java.io.BufferedInputStream; | ||
| import java.io.BufferedOutputStream; | ||
| import java.io.Closeable; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.io.OutputStream; | ||
| import java.net.Proxy; | ||
| import java.net.Socket; | ||
| import java.net.SocketTimeoutException; | ||
| import java.net.URL; | ||
| import java.util.Arrays; | ||
| import javax.net.ssl.SSLSocket; | ||
|
|
||
| import static java.net.HttpURLConnection.HTTP_OK; | ||
| import static java.net.HttpURLConnection.HTTP_PROXY_AUTH; | ||
|
|
||
| /** | ||
| * Holds the sockets and streams of an HTTP, HTTPS, or HTTPS+SPDY connection, | ||
| * which may be used for multiple HTTP request/response exchanges. Connections | ||
| * may be direct to the origin server or via a proxy. | ||
| * | ||
| * <p>Typically instances of this class are created, connected and exercised | ||
| * automatically by the HTTP client. Applications may use this class to monitor | ||
| * HTTP connections as members of a {@link ConnectionPool connection pool}. | ||
| * | ||
| * <p>Do not confuse this class with the misnamed {@code HttpURLConnection}, | ||
| * which isn't so much a connection as a single request/response exchange. | ||
| * | ||
| * <h3>Modern TLS</h3> | ||
| * There are tradeoffs when selecting which options to include when negotiating | ||
| * a secure connection to a remote host. Newer TLS options are quite useful: | ||
| * <ul> | ||
| * <li>Server Name Indication (SNI) enables one IP address to negotiate secure | ||
| * connections for multiple domain names. | ||
| * <li>Next Protocol Negotiation (NPN) enables the HTTPS port (443) to be used | ||
| * for both HTTP and SPDY transports. | ||
| * </ul> | ||
| * Unfortunately, older HTTPS servers refuse to connect when such options are | ||
| * presented. Rather than avoiding these options entirely, this class allows a | ||
| * connection to be attempted with modern options and then retried without them | ||
| * should the attempt fail. | ||
| */ | ||
| public final class Connection implements Closeable { | ||
| private static final byte[] NPN_PROTOCOLS = new byte[] { | ||
| 6, 's', 'p', 'd', 'y', '/', '3', | ||
| 8, 'h', 't', 't', 'p', '/', '1', '.', '1' | ||
| }; | ||
| private static final byte[] SPDY3 = new byte[] { | ||
| 's', 'p', 'd', 'y', '/', '3' | ||
| }; | ||
| private static final byte[] HTTP_11 = new byte[] { | ||
| 'h', 't', 't', 'p', '/', '1', '.', '1' | ||
| }; | ||
|
|
||
| private final Route route; | ||
|
|
||
| private Socket socket; | ||
| private InputStream in; | ||
| private OutputStream out; | ||
| private boolean connected = false; | ||
| private SpdyConnection spdyConnection; | ||
| private int httpMinorVersion = 1; // Assume HTTP/1.1 | ||
| private long idleStartTimeNs; | ||
|
|
||
| public Connection(Route route) { | ||
| this.route = route; | ||
| } | ||
|
|
||
| public void connect(int connectTimeout, int readTimeout, TunnelRequest tunnelRequest) | ||
| throws IOException { | ||
| if (connected) throw new IllegalStateException("already connected"); | ||
|
|
||
| socket = (route.proxy.type() != Proxy.Type.HTTP) ? new Socket(route.proxy) : new Socket(); | ||
| Platform.get().connectSocket(socket, route.inetSocketAddress, connectTimeout); | ||
| socket.setSoTimeout(readTimeout); | ||
| in = socket.getInputStream(); | ||
| out = socket.getOutputStream(); | ||
|
|
||
| if (route.address.sslSocketFactory != null) { | ||
| upgradeToTls(tunnelRequest); | ||
| } else { | ||
| streamWrapper(); | ||
| } | ||
| connected = true; | ||
| } | ||
|
|
||
| /** | ||
| * Create an {@code SSLSocket} and perform the TLS handshake and certificate | ||
| * validation. | ||
| */ | ||
| private void upgradeToTls(TunnelRequest tunnelRequest) throws IOException { | ||
| Platform platform = Platform.get(); | ||
|
|
||
| // Make an SSL Tunnel on the first message pair of each SSL + proxy connection. | ||
| if (requiresTunnel()) { | ||
| makeTunnel(tunnelRequest); | ||
| } | ||
|
|
||
| // Create the wrapper over connected socket. | ||
| socket = route.address.sslSocketFactory | ||
| .createSocket(socket, route.address.uriHost, route.address.uriPort, true /* autoClose */); | ||
| SSLSocket sslSocket = (SSLSocket) socket; | ||
| if (route.modernTls) { | ||
| platform.enableTlsExtensions(sslSocket, route.address.uriHost); | ||
| } else { | ||
| platform.supportTlsIntolerantServer(sslSocket); | ||
| } | ||
|
|
||
| boolean useNpn = route.modernTls && route.address.transports.contains("spdy/3"); | ||
| if (useNpn) { | ||
| platform.setNpnProtocols(sslSocket, NPN_PROTOCOLS); | ||
| } | ||
|
|
||
| // Force handshake. This can throw! | ||
| sslSocket.startHandshake(); | ||
|
|
||
| // Verify that the socket's certificates are acceptable for the target host. | ||
| if (!route.address.hostnameVerifier.verify(route.address.uriHost, sslSocket.getSession())) { | ||
| throw new IOException("Hostname '" + route.address.uriHost + "' was not verified"); | ||
| } | ||
|
|
||
| out = sslSocket.getOutputStream(); | ||
| in = sslSocket.getInputStream(); | ||
| streamWrapper(); | ||
|
|
||
| byte[] selectedProtocol; | ||
| if (useNpn && (selectedProtocol = platform.getNpnSelectedProtocol(sslSocket)) != null) { | ||
| if (Arrays.equals(selectedProtocol, SPDY3)) { | ||
| sslSocket.setSoTimeout(0); // SPDY timeouts are set per-stream. | ||
| spdyConnection = new SpdyConnection.Builder(route.address.getUriHost(), true, in, out) | ||
| .build(); | ||
| spdyConnection.sendConnectionHeader(); | ||
| } else if (!Arrays.equals(selectedProtocol, HTTP_11)) { | ||
| throw new IOException( | ||
| "Unexpected NPN transport " + new String(selectedProtocol, "ISO-8859-1")); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** Returns true if {@link #connect} has been attempted on this connection. */ | ||
| public boolean isConnected() { | ||
| return connected; | ||
| } | ||
|
|
||
| @Override public void close() throws IOException { | ||
| socket.close(); | ||
| } | ||
|
|
||
| /** Returns the route used by this connection. */ | ||
| public Route getRoute() { | ||
| return route; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the socket that this connection uses, or null if the connection | ||
| * is not currently connected. | ||
| */ | ||
| public Socket getSocket() { | ||
| return socket; | ||
| } | ||
|
|
||
| /** Returns true if this connection is alive. */ | ||
| public boolean isAlive() { | ||
| return !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown(); | ||
| } | ||
|
|
||
| /** | ||
| * Returns true if we are confident that we can read data from this | ||
| * connection. This is more expensive and more accurate than {@link | ||
| * #isAlive()}; callers should check {@link #isAlive()} first. | ||
| */ | ||
| public boolean isReadable() { | ||
| if (!(in instanceof BufferedInputStream)) { | ||
| return true; // Optimistic. | ||
| } | ||
| if (isSpdy()) { | ||
| return true; // Optimistic. We can't test SPDY because its streams are in use. | ||
| } | ||
| BufferedInputStream bufferedInputStream = (BufferedInputStream) in; | ||
| try { | ||
| int readTimeout = socket.getSoTimeout(); | ||
| try { | ||
| socket.setSoTimeout(1); | ||
| bufferedInputStream.mark(1); | ||
| if (bufferedInputStream.read() == -1) { | ||
| return false; // Stream is exhausted; socket is closed. | ||
| } | ||
| bufferedInputStream.reset(); | ||
| return true; | ||
| } finally { | ||
| socket.setSoTimeout(readTimeout); | ||
| } | ||
| } catch (SocketTimeoutException ignored) { | ||
| return true; // Read timed out; socket is good. | ||
| } catch (IOException e) { | ||
| return false; // Couldn't read; socket is closed. | ||
| } | ||
| } | ||
|
|
||
| public void resetIdleStartTime() { | ||
| if (spdyConnection != null) { | ||
| throw new IllegalStateException("spdyConnection != null"); | ||
| } | ||
| this.idleStartTimeNs = System.nanoTime(); | ||
| } | ||
|
|
||
| /** Returns true if this connection is idle. */ | ||
| public boolean isIdle() { | ||
| return spdyConnection == null || spdyConnection.isIdle(); | ||
| } | ||
|
|
||
| /** | ||
| * Returns true if this connection has been idle for longer than | ||
| * {@code keepAliveDurationNs}. | ||
| */ | ||
| public boolean isExpired(long keepAliveDurationNs) { | ||
| return getIdleStartTimeNs() < System.nanoTime() - keepAliveDurationNs; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the time in ns when this connection became idle. Undefined if | ||
| * this connection is not idle. | ||
| */ | ||
| public long getIdleStartTimeNs() { | ||
| return spdyConnection == null ? idleStartTimeNs : spdyConnection.getIdleStartTimeNs(); | ||
| } | ||
|
|
||
| /** Returns the transport appropriate for this connection. */ | ||
| public Object newTransport(HttpEngine httpEngine) throws IOException { | ||
| return (spdyConnection != null) | ||
| ? new SpdyTransport(httpEngine, spdyConnection) | ||
| : new HttpTransport(httpEngine, out, in); | ||
| } | ||
|
|
||
| /** | ||
| * Returns true if this is a SPDY connection. Such connections can be used | ||
| * in multiple HTTP requests simultaneously. | ||
| */ | ||
| public boolean isSpdy() { | ||
| return spdyConnection != null; | ||
| } | ||
|
|
||
| public SpdyConnection getSpdyConnection() { | ||
| return spdyConnection; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the minor HTTP version that should be used for future requests on | ||
| * this connection. Either 0 for HTTP/1.0, or 1 for HTTP/1.1. The default | ||
| * value is 1 for new connections. | ||
| */ | ||
| public int getHttpMinorVersion() { | ||
| return httpMinorVersion; | ||
| } | ||
|
|
||
| public void setHttpMinorVersion(int httpMinorVersion) { | ||
| this.httpMinorVersion = httpMinorVersion; | ||
| } | ||
|
|
||
| /** | ||
| * Returns true if the HTTP connection needs to tunnel one protocol over | ||
| * another, such as when using HTTPS through an HTTP proxy. When doing so, | ||
| * we must avoid buffering bytes intended for the higher-level protocol. | ||
| */ | ||
| public boolean requiresTunnel() { | ||
| return route.address.sslSocketFactory != null && route.proxy.type() == Proxy.Type.HTTP; | ||
| } | ||
|
|
||
| public void updateReadTimeout(int newTimeout) throws IOException { | ||
| if (!connected) throw new IllegalStateException("updateReadTimeout - not connected"); | ||
| socket.setSoTimeout(newTimeout); | ||
| } | ||
|
|
||
| /** | ||
| * To make an HTTPS connection over an HTTP proxy, send an unencrypted | ||
| * CONNECT request to create the proxy connection. This may need to be | ||
| * retried if the proxy requires authorization. | ||
| */ | ||
| private void makeTunnel(TunnelRequest tunnelRequest) throws IOException { | ||
| RawHeaders requestHeaders = tunnelRequest.getRequestHeaders(); | ||
| while (true) { | ||
| out.write(requestHeaders.toBytes()); | ||
| RawHeaders responseHeaders = RawHeaders.fromBytes(in); | ||
|
|
||
| switch (responseHeaders.getResponseCode()) { | ||
| case HTTP_OK: | ||
| return; | ||
| case HTTP_PROXY_AUTH: | ||
| requestHeaders = new RawHeaders(requestHeaders); | ||
| URL url = new URL("https", tunnelRequest.host, tunnelRequest.port, "/"); | ||
| boolean credentialsFound = HttpAuthenticator.processAuthHeader( | ||
| route.address.authenticator, HTTP_PROXY_AUTH, responseHeaders, requestHeaders, | ||
| route.proxy, url); | ||
| if (credentialsFound) { | ||
| continue; | ||
| } else { | ||
| throw new IOException("Failed to authenticate with proxy"); | ||
| } | ||
| default: | ||
| throw new IOException( | ||
| "Unexpected response code for CONNECT: " + responseHeaders.getResponseCode()); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private void streamWrapper() throws IOException { | ||
| in = new BufferedInputStream(in, 4096); | ||
| out = new BufferedOutputStream(out, 256); | ||
| } | ||
| } |
| @@ -0,0 +1,274 @@ | ||
| /* | ||
| * 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.squareup.okhttp; | ||
|
|
||
| import com.squareup.okhttp.internal.Platform; | ||
| import com.squareup.okhttp.internal.Util; | ||
| import java.net.SocketException; | ||
| import java.util.ArrayList; | ||
| import java.util.LinkedList; | ||
| import java.util.List; | ||
| import java.util.ListIterator; | ||
| import java.util.concurrent.Callable; | ||
| import java.util.concurrent.ExecutorService; | ||
| import java.util.concurrent.LinkedBlockingQueue; | ||
| import java.util.concurrent.ThreadPoolExecutor; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| /** | ||
| * Manages reuse of HTTP and SPDY connections for reduced network latency. HTTP | ||
| * requests that share the same {@link com.squareup.okhttp.Address} may share a | ||
| * {@link com.squareup.okhttp.Connection}. This class implements the policy of | ||
| * which connections to keep open for future use. | ||
| * | ||
| * <p>The {@link #getDefault() system-wide default} uses system properties for | ||
| * tuning parameters: | ||
| * <ul> | ||
| * <li>{@code http.keepAlive} true if HTTP and SPDY connections should be | ||
| * pooled at all. Default is true. | ||
| * <li>{@code http.maxConnections} maximum number of idle connections to | ||
| * each to keep in the pool. Default is 5. | ||
| * <li>{@code http.keepAliveDuration} Time in milliseconds to keep the | ||
| * connection alive in the pool before closing it. Default is 5 minutes. | ||
| * This property isn't used by {@code HttpURLConnection}. | ||
| * </ul> | ||
| * | ||
| * <p>The default instance <i>doesn't</i> adjust its configuration as system | ||
| * properties are changed. This assumes that the applications that set these | ||
| * parameters do so before making HTTP connections, and that this class is | ||
| * initialized lazily. | ||
| */ | ||
| public class ConnectionPool { | ||
| private static final int MAX_CONNECTIONS_TO_CLEANUP = 2; | ||
| private static final long DEFAULT_KEEP_ALIVE_DURATION_MS = 5 * 60 * 1000; // 5 min | ||
|
|
||
| private static final ConnectionPool systemDefault; | ||
|
|
||
| static { | ||
| String keepAlive = System.getProperty("http.keepAlive"); | ||
| String keepAliveDuration = System.getProperty("http.keepAliveDuration"); | ||
| String maxIdleConnections = System.getProperty("http.maxConnections"); | ||
| long keepAliveDurationMs = keepAliveDuration != null ? Long.parseLong(keepAliveDuration) | ||
| : DEFAULT_KEEP_ALIVE_DURATION_MS; | ||
| if (keepAlive != null && !Boolean.parseBoolean(keepAlive)) { | ||
| systemDefault = new ConnectionPool(0, keepAliveDurationMs); | ||
| } else if (maxIdleConnections != null) { | ||
| systemDefault = new ConnectionPool(Integer.parseInt(maxIdleConnections), keepAliveDurationMs); | ||
| } else { | ||
| systemDefault = new ConnectionPool(5, keepAliveDurationMs); | ||
| } | ||
| } | ||
|
|
||
| /** The maximum number of idle connections for each address. */ | ||
| private final int maxIdleConnections; | ||
| private final long keepAliveDurationNs; | ||
|
|
||
| private final LinkedList<Connection> connections = new LinkedList<Connection>(); | ||
|
|
||
| /** We use a single background thread to cleanup expired connections. */ | ||
| private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, | ||
| 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), | ||
| Util.daemonThreadFactory("OkHttp ConnectionPool")); | ||
| private final Callable<Void> connectionsCleanupCallable = new Callable<Void>() { | ||
| @Override public Void call() throws Exception { | ||
| List<Connection> expiredConnections = new ArrayList<Connection>(MAX_CONNECTIONS_TO_CLEANUP); | ||
| int idleConnectionCount = 0; | ||
| synchronized (ConnectionPool.this) { | ||
| for (ListIterator<Connection> i = connections.listIterator(connections.size()); | ||
| i.hasPrevious(); ) { | ||
| Connection connection = i.previous(); | ||
| if (!connection.isAlive() || connection.isExpired(keepAliveDurationNs)) { | ||
| i.remove(); | ||
| expiredConnections.add(connection); | ||
| if (expiredConnections.size() == MAX_CONNECTIONS_TO_CLEANUP) break; | ||
| } else if (connection.isIdle()) { | ||
| idleConnectionCount++; | ||
| } | ||
| } | ||
|
|
||
| for (ListIterator<Connection> i = connections.listIterator(connections.size()); | ||
| i.hasPrevious() && idleConnectionCount > maxIdleConnections; ) { | ||
| Connection connection = i.previous(); | ||
| if (connection.isIdle()) { | ||
| expiredConnections.add(connection); | ||
| i.remove(); | ||
| --idleConnectionCount; | ||
| } | ||
| } | ||
| } | ||
| for (Connection expiredConnection : expiredConnections) { | ||
| Util.closeQuietly(expiredConnection); | ||
| } | ||
| return null; | ||
| } | ||
| }; | ||
|
|
||
| public ConnectionPool(int maxIdleConnections, long keepAliveDurationMs) { | ||
| this.maxIdleConnections = maxIdleConnections; | ||
| this.keepAliveDurationNs = keepAliveDurationMs * 1000 * 1000; | ||
| } | ||
|
|
||
| /** | ||
| * Returns a snapshot of the connections in this pool, ordered from newest to | ||
| * oldest. Waits for the cleanup callable to run if it is currently scheduled. | ||
| */ | ||
| List<Connection> getConnections() { | ||
| waitForCleanupCallableToRun(); | ||
| synchronized (this) { | ||
| return new ArrayList<Connection>(connections); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Blocks until the executor service has processed all currently enqueued | ||
| * jobs. | ||
| */ | ||
| private void waitForCleanupCallableToRun() { | ||
| try { | ||
| executorService.submit(new Runnable() { | ||
| @Override public void run() { | ||
| } | ||
| }).get(); | ||
| } catch (Exception e) { | ||
| throw new AssertionError(); | ||
| } | ||
| } | ||
|
|
||
| public static ConnectionPool getDefault() { | ||
| return systemDefault; | ||
| } | ||
|
|
||
| /** Returns total number of connections in the pool. */ | ||
| public synchronized int getConnectionCount() { | ||
| return connections.size(); | ||
| } | ||
|
|
||
| /** Returns total number of spdy connections in the pool. */ | ||
| public synchronized int getSpdyConnectionCount() { | ||
| int total = 0; | ||
| for (Connection connection : connections) { | ||
| if (connection.isSpdy()) total++; | ||
| } | ||
| return total; | ||
| } | ||
|
|
||
| /** Returns total number of http connections in the pool. */ | ||
| public synchronized int getHttpConnectionCount() { | ||
| int total = 0; | ||
| for (Connection connection : connections) { | ||
| if (!connection.isSpdy()) total++; | ||
| } | ||
| return total; | ||
| } | ||
|
|
||
| /** Returns a recycled connection to {@code address}, or null if no such connection exists. */ | ||
| public synchronized Connection get(Address address) { | ||
| Connection foundConnection = null; | ||
| for (ListIterator<Connection> i = connections.listIterator(connections.size()); | ||
| i.hasPrevious(); ) { | ||
| Connection connection = i.previous(); | ||
| if (!connection.getRoute().getAddress().equals(address) | ||
| || !connection.isAlive() | ||
| || System.nanoTime() - connection.getIdleStartTimeNs() >= keepAliveDurationNs) { | ||
| continue; | ||
| } | ||
| i.remove(); | ||
| if (!connection.isSpdy()) { | ||
| try { | ||
| Platform.get().tagSocket(connection.getSocket()); | ||
| } catch (SocketException e) { | ||
| Util.closeQuietly(connection); | ||
| // When unable to tag, skip recycling and close | ||
| Platform.get().logW("Unable to tagSocket(): " + e); | ||
| continue; | ||
| } | ||
| } | ||
| foundConnection = connection; | ||
| break; | ||
| } | ||
|
|
||
| if (foundConnection != null && foundConnection.isSpdy()) { | ||
| connections.addFirst(foundConnection); // Add it back after iteration. | ||
| } | ||
|
|
||
| executorService.submit(connectionsCleanupCallable); | ||
| return foundConnection; | ||
| } | ||
|
|
||
| /** | ||
| * Gives {@code connection} to the pool. The pool may store the connection, | ||
| * or close it, as its policy describes. | ||
| * | ||
| * <p>It is an error to use {@code connection} after calling this method. | ||
| */ | ||
| public void recycle(Connection connection) { | ||
| if (connection.isSpdy()) { | ||
| return; | ||
| } | ||
|
|
||
| if (!connection.isAlive()) { | ||
| Util.closeQuietly(connection); | ||
| return; | ||
| } | ||
|
|
||
| try { | ||
| Platform.get().untagSocket(connection.getSocket()); | ||
| } catch (SocketException e) { | ||
| // When unable to remove tagging, skip recycling and close. | ||
| Platform.get().logW("Unable to untagSocket(): " + e); | ||
| Util.closeQuietly(connection); | ||
| return; | ||
| } | ||
|
|
||
| synchronized (this) { | ||
| connections.addFirst(connection); | ||
| connection.resetIdleStartTime(); | ||
| } | ||
|
|
||
| executorService.submit(connectionsCleanupCallable); | ||
| } | ||
|
|
||
| /** | ||
| * Shares the SPDY connection with the pool. Callers to this method may | ||
| * continue to use {@code connection}. | ||
| */ | ||
| public void maybeShare(Connection connection) { | ||
| executorService.submit(connectionsCleanupCallable); | ||
| if (!connection.isSpdy()) { | ||
| // Only SPDY connections are sharable. | ||
| return; | ||
| } | ||
| if (connection.isAlive()) { | ||
| synchronized (this) { | ||
| connections.addFirst(connection); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** Close and remove all connections in the pool. */ | ||
| public void evictAll() { | ||
| List<Connection> connections; | ||
| synchronized (this) { | ||
| connections = new ArrayList<Connection>(this.connections); | ||
| this.connections.clear(); | ||
| } | ||
|
|
||
| for (Connection connection : connections) { | ||
| Util.closeQuietly(connection); | ||
| } | ||
| } | ||
| } |
| @@ -0,0 +1,86 @@ | ||
| /* | ||
| * Copyright (C) 2013 Square, Inc. | ||
| * | ||
| * Licensed 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.squareup.okhttp; | ||
|
|
||
| import com.squareup.okhttp.internal.http.ResponseHeaders; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.util.ArrayList; | ||
| import java.util.LinkedHashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.concurrent.LinkedBlockingQueue; | ||
| import java.util.concurrent.ThreadPoolExecutor; | ||
| import java.util.concurrent.TimeUnit; | ||
|
|
||
| final class Dispatcher { | ||
| // TODO: thread pool size should be configurable; possibly configurable per host. | ||
| private final ThreadPoolExecutor executorService = new ThreadPoolExecutor( | ||
| 8, 8, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); | ||
| private final Map<Object, List<Job>> enqueuedJobs = new LinkedHashMap<Object, List<Job>>(); | ||
|
|
||
| public synchronized void enqueue( | ||
| OkHttpClient client, Request request, Response.Receiver responseReceiver) { | ||
| Job job = new Job(this, client, request, responseReceiver); | ||
| List<Job> jobsForTag = enqueuedJobs.get(request.tag()); | ||
| if (jobsForTag == null) { | ||
| jobsForTag = new ArrayList<Job>(2); | ||
| enqueuedJobs.put(request.tag(), jobsForTag); | ||
| } | ||
| jobsForTag.add(job); | ||
| executorService.execute(job); | ||
| } | ||
|
|
||
| public synchronized void cancel(Object tag) { | ||
| List<Job> jobs = enqueuedJobs.remove(tag); | ||
| if (jobs == null) return; | ||
| for (Job job : jobs) { | ||
| executorService.remove(job); | ||
| } | ||
| } | ||
|
|
||
| synchronized void finished(Job job) { | ||
| List<Job> jobs = enqueuedJobs.get(job.tag()); | ||
| if (jobs != null) jobs.remove(job); | ||
| } | ||
|
|
||
| static class RealResponseBody extends Response.Body { | ||
| private final ResponseHeaders responseHeaders; | ||
| private final InputStream in; | ||
|
|
||
| RealResponseBody(ResponseHeaders responseHeaders, InputStream in) { | ||
| this.responseHeaders = responseHeaders; | ||
| this.in = in; | ||
| } | ||
|
|
||
| @Override public boolean ready() throws IOException { | ||
| return true; | ||
| } | ||
|
|
||
| @Override public MediaType contentType() { | ||
| String contentType = responseHeaders.getContentType(); | ||
| return contentType != null ? MediaType.parse(contentType) : null; | ||
| } | ||
|
|
||
| @Override public long contentLength() { | ||
| return responseHeaders.getContentLength(); | ||
| } | ||
|
|
||
| @Override public InputStream byteStream() throws IOException { | ||
| return in; | ||
| } | ||
| } | ||
| } |
| @@ -0,0 +1,59 @@ | ||
| /* | ||
| * Copyright (C) 2013 Square, Inc. | ||
| * | ||
| * Licensed 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.squareup.okhttp; | ||
|
|
||
| /** | ||
| * A failure attempting to retrieve an HTTP response. | ||
| * | ||
| * <h3>Warning: Experimental OkHttp 2.0 API</h3> | ||
| * This class is in beta. APIs are subject to change! | ||
| */ | ||
| /* OkHttp 2.0: public */ class Failure { | ||
| private final Request request; | ||
| private final Throwable exception; | ||
|
|
||
| private Failure(Builder builder) { | ||
| this.request = builder.request; | ||
| this.exception = builder.exception; | ||
| } | ||
|
|
||
| public Request request() { | ||
| return request; | ||
| } | ||
|
|
||
| public Throwable exception() { | ||
| return exception; | ||
| } | ||
|
|
||
| public static class Builder { | ||
| private Request request; | ||
| private Throwable exception; | ||
|
|
||
| public Builder request(Request request) { | ||
| this.request = request; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder exception(Throwable exception) { | ||
| this.exception = exception; | ||
| return this; | ||
| } | ||
|
|
||
| public Failure build() { | ||
| return new Failure(this); | ||
| } | ||
| } | ||
| } |
| @@ -0,0 +1,232 @@ | ||
| /* | ||
| * Copyright (C) 2013 Square, Inc. | ||
| * | ||
| * Licensed 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.squareup.okhttp; | ||
|
|
||
| import com.squareup.okhttp.internal.http.HttpAuthenticator; | ||
| import com.squareup.okhttp.internal.http.HttpEngine; | ||
| import com.squareup.okhttp.internal.http.HttpTransport; | ||
| import com.squareup.okhttp.internal.http.HttpsEngine; | ||
| import com.squareup.okhttp.internal.http.Policy; | ||
| import com.squareup.okhttp.internal.http.RawHeaders; | ||
| import java.io.IOException; | ||
| import java.net.HttpURLConnection; | ||
| import java.net.ProtocolException; | ||
| import java.net.Proxy; | ||
| import java.net.URL; | ||
|
|
||
| import static com.squareup.okhttp.internal.Util.getEffectivePort; | ||
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MOVED_PERM; | ||
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MOVED_TEMP; | ||
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_MULT_CHOICE; | ||
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_PROXY_AUTH; | ||
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_SEE_OTHER; | ||
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_TEMP_REDIRECT; | ||
| import static com.squareup.okhttp.internal.http.HttpURLConnectionImpl.HTTP_UNAUTHORIZED; | ||
|
|
||
| final class Job implements Runnable, Policy { | ||
| private final Dispatcher dispatcher; | ||
| private final OkHttpClient client; | ||
| private final Response.Receiver responseReceiver; | ||
|
|
||
| /** The request; possibly a consequence of redirects or auth headers. */ | ||
| private Request request; | ||
|
|
||
| public Job(Dispatcher dispatcher, OkHttpClient client, Request request, | ||
| Response.Receiver responseReceiver) { | ||
| this.dispatcher = dispatcher; | ||
| this.client = client; | ||
| this.request = request; | ||
| this.responseReceiver = responseReceiver; | ||
| } | ||
|
|
||
| @Override public int getChunkLength() { | ||
| return request.body().contentLength() == -1 ? HttpTransport.DEFAULT_CHUNK_LENGTH : -1; | ||
| } | ||
|
|
||
| @Override public long getFixedContentLength() { | ||
| return request.body().contentLength(); | ||
| } | ||
|
|
||
| @Override public boolean getUseCaches() { | ||
| return false; // TODO. | ||
| } | ||
|
|
||
| @Override public HttpURLConnection getHttpConnectionToCache() { | ||
| return null; | ||
| } | ||
|
|
||
| @Override public URL getURL() { | ||
| return request.url(); | ||
| } | ||
|
|
||
| @Override public long getIfModifiedSince() { | ||
| return 0; // For HttpURLConnection only. We let the cache drive this. | ||
| } | ||
|
|
||
| @Override public boolean usingProxy() { | ||
| return false; // We let the connection decide this. | ||
| } | ||
|
|
||
| @Override public void setSelectedProxy(Proxy proxy) { | ||
| // Do nothing. | ||
| } | ||
|
|
||
| Object tag() { | ||
| return request.tag(); | ||
| } | ||
|
|
||
| @Override public void run() { | ||
| try { | ||
| Response response = execute(); | ||
| responseReceiver.onResponse(response); | ||
| } catch (IOException e) { | ||
| responseReceiver.onFailure(new Failure.Builder() | ||
| .request(request) | ||
| .exception(e) | ||
| .build()); | ||
| } finally { | ||
| // TODO: close the response body | ||
| // TODO: release the HTTP engine (potentially multiple!) | ||
| dispatcher.finished(this); | ||
| } | ||
| } | ||
|
|
||
| private Response execute() throws IOException { | ||
| Connection connection = null; | ||
| Response redirectedBy = null; | ||
|
|
||
| while (true) { | ||
| HttpEngine engine = newEngine(connection); | ||
|
|
||
| Request.Body body = request.body(); | ||
| if (body != null) { | ||
| MediaType contentType = body.contentType(); | ||
| if (contentType == null) throw new IllegalStateException("contentType == null"); | ||
| if (engine.getRequestHeaders().getContentType() == null) { | ||
| engine.getRequestHeaders().setContentType(contentType.toString()); | ||
| } | ||
| } | ||
|
|
||
| engine.sendRequest(); | ||
|
|
||
| if (body != null) { | ||
| body.writeTo(engine.getRequestBody()); | ||
| } | ||
|
|
||
| engine.readResponse(); | ||
|
|
||
| int responseCode = engine.getResponseCode(); | ||
| Dispatcher.RealResponseBody responseBody = new Dispatcher.RealResponseBody( | ||
| engine.getResponseHeaders(), engine.getResponseBody()); | ||
|
|
||
| Response response = new Response.Builder(request, responseCode) | ||
| .rawHeaders(engine.getResponseHeaders().getHeaders()) | ||
| .body(responseBody) | ||
| .redirectedBy(redirectedBy) | ||
| .build(); | ||
|
|
||
| Request redirect = processResponse(engine, response); | ||
|
|
||
| if (redirect == null) { | ||
| engine.automaticallyReleaseConnectionToPool(); | ||
| return response; | ||
| } | ||
|
|
||
| // TODO: fail if too many redirects | ||
| // TODO: fail if not following redirects | ||
| // TODO: release engine | ||
|
|
||
| connection = sameConnection(request, redirect) ? engine.getConnection() : null; | ||
| redirectedBy = response; | ||
| request = redirect; | ||
| } | ||
| } | ||
|
|
||
| HttpEngine newEngine(Connection connection) throws IOException { | ||
| String protocol = request.url().getProtocol(); | ||
| RawHeaders requestHeaders = request.rawHeaders(); | ||
| if (protocol.equals("http")) { | ||
| return new HttpEngine(client, this, request.method(), requestHeaders, connection, null); | ||
| } else if (protocol.equals("https")) { | ||
| return new HttpsEngine(client, this, request.method(), requestHeaders, connection, null); | ||
| } else { | ||
| throw new AssertionError(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Figures out the HTTP request to make in response to receiving {@code | ||
| * response}. This will either add authentication headers or follow | ||
| * redirects. If a follow-up is either unnecessary or not applicable, this | ||
| * returns null. | ||
| */ | ||
| private Request processResponse(HttpEngine engine, Response response) throws IOException { | ||
| Request request = response.request(); | ||
| Proxy selectedProxy = engine.getConnection() != null | ||
| ? engine.getConnection().getRoute().getProxy() | ||
| : client.getProxy(); | ||
| int responseCode = response.code(); | ||
|
|
||
| switch (responseCode) { | ||
| case HTTP_PROXY_AUTH: | ||
| if (selectedProxy.type() != Proxy.Type.HTTP) { | ||
| throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy"); | ||
| } | ||
| // fall-through | ||
| case HTTP_UNAUTHORIZED: | ||
| RawHeaders successorRequestHeaders = request.rawHeaders(); | ||
| boolean credentialsFound = HttpAuthenticator.processAuthHeader(client.getAuthenticator(), | ||
| response.code(), response.rawHeaders(), successorRequestHeaders, selectedProxy, | ||
| this.request.url()); | ||
| return credentialsFound | ||
| ? request.newBuilder().rawHeaders(successorRequestHeaders).build() | ||
| : null; | ||
|
|
||
| case HTTP_MULT_CHOICE: | ||
| case HTTP_MOVED_PERM: | ||
| case HTTP_MOVED_TEMP: | ||
| case HTTP_SEE_OTHER: | ||
| case HTTP_TEMP_REDIRECT: | ||
| String method = request.method(); | ||
| if (responseCode == HTTP_TEMP_REDIRECT && !method.equals("GET") && !method.equals("HEAD")) { | ||
| // "If the 307 status code is received in response to a request other than GET or HEAD, | ||
| // the user agent MUST NOT automatically redirect the request" | ||
| return null; | ||
| } | ||
|
|
||
| String location = response.header("Location"); | ||
| if (location == null) { | ||
| return null; | ||
| } | ||
|
|
||
| URL url = new URL(request.url(), location); | ||
| if (!url.getProtocol().equals("https") && !url.getProtocol().equals("http")) { | ||
| return null; // Don't follow redirects to unsupported protocols. | ||
| } | ||
|
|
||
| return this.request.newBuilder().url(url).build(); | ||
|
|
||
| default: | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| private boolean sameConnection(Request a, Request b) { | ||
| return a.url().getHost().equals(b.url().getHost()) | ||
| && getEffectivePort(a.url()) == getEffectivePort(b.url()) | ||
| && a.url().getProtocol().equals(b.url().getProtocol()); | ||
| } | ||
| } |
| @@ -0,0 +1,120 @@ | ||
| /* | ||
| * Copyright (C) 2013 Square, Inc. | ||
| * | ||
| * Licensed 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.squareup.okhttp; | ||
|
|
||
| import java.nio.charset.Charset; | ||
| import java.util.Locale; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| /** | ||
| * An <a href="http://tools.ietf.org/html/rfc2045">RFC 2045</a> Media Type, | ||
| * appropriate to describe the content type of an HTTP request or response body. | ||
| */ | ||
| public final class MediaType { | ||
| private static final String TOKEN = "([a-zA-Z0-9-!#$%&'*+.^_`{|}~]+)"; | ||
| private static final String QUOTED = "\"([^\"]*)\""; | ||
| private static final Pattern TYPE_SUBTYPE = Pattern.compile(TOKEN + "/" + TOKEN); | ||
| private static final Pattern PARAMETER = Pattern.compile( | ||
| ";\\s*" + TOKEN + "=(?:" + TOKEN + "|" + QUOTED + ")"); | ||
|
|
||
| private final String mediaType; | ||
| private final String type; | ||
| private final String subtype; | ||
| private final String charset; | ||
|
|
||
| private MediaType(String mediaType, String type, String subtype, String charset) { | ||
| this.mediaType = mediaType; | ||
| this.type = type; | ||
| this.subtype = subtype; | ||
| this.charset = charset; | ||
| } | ||
|
|
||
| /** | ||
| * Returns a media type for {@code string}, or null if {@code string} is not a | ||
| * well-formed media type. | ||
| */ | ||
| public static MediaType parse(String string) { | ||
| Matcher typeSubtype = TYPE_SUBTYPE.matcher(string); | ||
| if (!typeSubtype.lookingAt()) return null; | ||
| String type = typeSubtype.group(1).toLowerCase(Locale.US); | ||
| String subtype = typeSubtype.group(2).toLowerCase(Locale.US); | ||
|
|
||
| String charset = null; | ||
| Matcher parameter = PARAMETER.matcher(string); | ||
| for (int s = typeSubtype.end(); s < string.length(); s = parameter.end()) { | ||
| parameter.region(s, string.length()); | ||
| if (!parameter.lookingAt()) return null; // This is not a well-formed media type. | ||
|
|
||
| String name = parameter.group(1); | ||
| if (name == null || !name.equalsIgnoreCase("charset")) continue; | ||
| if (charset != null) throw new IllegalArgumentException("Multiple charsets: " + string); | ||
| charset = parameter.group(2) != null | ||
| ? parameter.group(2) // Value is a token. | ||
| : parameter.group(3); // Value is a quoted string. | ||
| } | ||
|
|
||
| return new MediaType(string, type, subtype, charset); | ||
| } | ||
|
|
||
| /** | ||
| * Returns the high-level media type, such as "text", "image", "audio", | ||
| * "video", or "application". | ||
| */ | ||
| public String type() { | ||
| return type; | ||
| } | ||
|
|
||
| /** | ||
| * Returns a specific media subtype, such as "plain" or "png", "mpeg", | ||
| * "mp4" or "xml". | ||
| */ | ||
| public String subtype() { | ||
| return subtype; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the charset of this media type, or null if this media type doesn't | ||
| * specify a charset. | ||
| */ | ||
| public Charset charset() { | ||
| return charset != null ? Charset.forName(charset) : null; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the charset of this media type, or {@code defaultValue} if this | ||
| * media type doesn't specify a charset. | ||
| */ | ||
| public Charset charset(Charset defaultValue) { | ||
| return charset != null ? Charset.forName(charset) : defaultValue; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the encoded media type, like "text/plain; charset=utf-8", | ||
| * appropriate for use in a Content-Type header. | ||
| */ | ||
| @Override public String toString() { | ||
| return mediaType; | ||
| } | ||
|
|
||
| @Override public boolean equals(Object o) { | ||
| return o instanceof MediaType && ((MediaType) o).mediaType.equals(mediaType); | ||
| } | ||
|
|
||
| @Override public int hashCode() { | ||
| return mediaType.hashCode(); | ||
| } | ||
| } |
| @@ -0,0 +1,123 @@ | ||
| /* | ||
| * Copyright (C) 2013 Square, Inc. | ||
| * | ||
| * Licensed 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.squareup.okhttp; | ||
|
|
||
| import com.squareup.okhttp.internal.Base64; | ||
| import java.io.IOException; | ||
| import java.io.UnsupportedEncodingException; | ||
| import java.net.Proxy; | ||
| import java.net.URL; | ||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Responds to authentication challenges from the remote web or proxy server by | ||
| * returning credentials. | ||
| */ | ||
| public interface OkAuthenticator { | ||
| /** | ||
| * Returns a credential that satisfies the authentication challenge made by | ||
| * {@code url}. Returns null if the challenge cannot be satisfied. This method | ||
| * is called in response to an HTTP 401 unauthorized status code sent by the | ||
| * origin server. | ||
| * | ||
| * @param challenges parsed "WWW-Authenticate" challenge headers from the HTTP | ||
| * response. | ||
| */ | ||
| Credential authenticate(Proxy proxy, URL url, List<Challenge> challenges) throws IOException; | ||
|
|
||
| /** | ||
| * Returns a credential that satisfies the authentication challenge made by | ||
| * {@code proxy}. Returns null if the challenge cannot be satisfied. This | ||
| * method is called in response to an HTTP 401 unauthorized status code sent | ||
| * by the proxy server. | ||
| * | ||
| * @param challenges parsed "Proxy-Authenticate" challenge headers from the | ||
| * HTTP response. | ||
| */ | ||
| Credential authenticateProxy(Proxy proxy, URL url, List<Challenge> challenges) throws IOException; | ||
|
|
||
| /** An RFC 2617 challenge. */ | ||
| public final class Challenge { | ||
| private final String scheme; | ||
| private final String realm; | ||
|
|
||
| public Challenge(String scheme, String realm) { | ||
| this.scheme = scheme; | ||
| this.realm = realm; | ||
| } | ||
|
|
||
| /** Returns the authentication scheme, like {@code Basic}. */ | ||
| public String getScheme() { | ||
| return scheme; | ||
| } | ||
|
|
||
| /** Returns the protection space. */ | ||
| public String getRealm() { | ||
| return realm; | ||
| } | ||
|
|
||
| @Override public boolean equals(Object o) { | ||
| return o instanceof Challenge | ||
| && ((Challenge) o).scheme.equals(scheme) | ||
| && ((Challenge) o).realm.equals(realm); | ||
| } | ||
|
|
||
| @Override public int hashCode() { | ||
| return scheme.hashCode() + 31 * realm.hashCode(); | ||
| } | ||
|
|
||
| @Override public String toString() { | ||
| return scheme + " realm=\"" + realm + "\""; | ||
| } | ||
| } | ||
|
|
||
| /** An RFC 2617 credential. */ | ||
| public final class Credential { | ||
| private final String headerValue; | ||
|
|
||
| private Credential(String headerValue) { | ||
| this.headerValue = headerValue; | ||
| } | ||
|
|
||
| /** Returns an auth credential for the Basic scheme. */ | ||
| public static Credential basic(String userName, String password) { | ||
| try { | ||
| String usernameAndPassword = userName + ":" + password; | ||
| byte[] bytes = usernameAndPassword.getBytes("ISO-8859-1"); | ||
| String encoded = Base64.encode(bytes); | ||
| return new Credential("Basic " + encoded); | ||
| } catch (UnsupportedEncodingException e) { | ||
| throw new AssertionError(); | ||
| } | ||
| } | ||
|
|
||
| public String getHeaderValue() { | ||
| return headerValue; | ||
| } | ||
|
|
||
| @Override public boolean equals(Object o) { | ||
| return o instanceof Credential && ((Credential) o).headerValue.equals(headerValue); | ||
| } | ||
|
|
||
| @Override public int hashCode() { | ||
| return headerValue.hashCode(); | ||
| } | ||
|
|
||
| @Override public String toString() { | ||
| return headerValue; | ||
| } | ||
| } | ||
| } |
| @@ -0,0 +1,56 @@ | ||
| /* | ||
| * Copyright (C) 2013 Square, Inc. | ||
| * | ||
| * Licensed 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.squareup.okhttp; | ||
|
|
||
| import java.io.IOException; | ||
| import java.net.CacheRequest; | ||
| import java.net.CacheResponse; | ||
| import java.net.HttpURLConnection; | ||
| import java.net.URI; | ||
| import java.net.URLConnection; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
|
|
||
| /** | ||
| * An extended response cache API. Unlike {@link java.net.ResponseCache}, this | ||
| * interface supports conditional caching and statistics. | ||
| * | ||
| * <h3>Warning: Experimental OkHttp 2.0 API</h3> | ||
| * This class is in beta. APIs are subject to change! | ||
| */ | ||
| public interface OkResponseCache { | ||
| CacheResponse get(URI uri, String requestMethod, Map<String, List<String>> requestHeaders) | ||
| throws IOException; | ||
|
|
||
| CacheRequest put(URI uri, URLConnection urlConnection) throws IOException; | ||
|
|
||
| /** Remove any cache entries for the supplied {@code uri} if the request method invalidates. */ | ||
| void maybeRemove(String requestMethod, URI uri) throws IOException; | ||
|
|
||
| /** | ||
| * Handles a conditional request hit by updating the stored cache response | ||
| * with the headers from {@code httpConnection}. The cached response body is | ||
| * not updated. If the stored response has changed since {@code | ||
| * conditionalCacheHit} was returned, this does nothing. | ||
| */ | ||
| void update(CacheResponse conditionalCacheHit, HttpURLConnection connection) throws IOException; | ||
|
|
||
| /** Track an conditional GET that was satisfied by this cache. */ | ||
| void trackConditionalCacheHit(); | ||
|
|
||
| /** Track an HTTP response being satisfied by {@code source}. */ | ||
| void trackResponse(ResponseSource source); | ||
| } |
| @@ -0,0 +1,284 @@ | ||
| /* | ||
| * Copyright (C) 2013 Square, Inc. | ||
| * | ||
| * Licensed 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.squareup.okhttp; | ||
|
|
||
| import com.squareup.okhttp.internal.Util; | ||
| import com.squareup.okhttp.internal.http.RawHeaders; | ||
| import java.io.File; | ||
| import java.io.FileInputStream; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.io.OutputStream; | ||
| import java.io.UnsupportedEncodingException; | ||
| import java.net.MalformedURLException; | ||
| import java.net.URL; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
|
|
||
| /** | ||
| * An HTTP request. Instances of this class are immutable if their {@link #body} | ||
| * is null or itself immutable. | ||
| * | ||
| * <h3>Warning: Experimental OkHttp 2.0 API</h3> | ||
| * This class is in beta. APIs are subject to change! | ||
| */ | ||
| /* OkHttp 2.0: public */ final class Request { | ||
| private final URL url; | ||
| private final String method; | ||
| private final RawHeaders headers; | ||
| private final Body body; | ||
| private final Object tag; | ||
|
|
||
| private Request(Builder builder) { | ||
| this.url = builder.url; | ||
| this.method = builder.method; | ||
| this.headers = new RawHeaders(builder.headers); | ||
| this.body = builder.body; | ||
| this.tag = builder.tag != null ? builder.tag : this; | ||
| } | ||
|
|
||
| public URL url() { | ||
| return url; | ||
| } | ||
|
|
||
| public String urlString() { | ||
| return url.toString(); | ||
| } | ||
|
|
||
| public String method() { | ||
| return method; | ||
| } | ||
|
|
||
| public String header(String name) { | ||
| return headers.get(name); | ||
| } | ||
|
|
||
| public List<String> headers(String name) { | ||
| return headers.values(name); | ||
| } | ||
|
|
||
| public Set<String> headerNames() { | ||
| return headers.names(); | ||
| } | ||
|
|
||
| RawHeaders rawHeaders() { | ||
| return new RawHeaders(headers); | ||
| } | ||
|
|
||
| public int headerCount() { | ||
| return headers.length(); | ||
| } | ||
|
|
||
| public String headerName(int index) { | ||
| return headers.getFieldName(index); | ||
| } | ||
|
|
||
| public String headerValue(int index) { | ||
| return headers.getValue(index); | ||
| } | ||
|
|
||
| public Body body() { | ||
| return body; | ||
| } | ||
|
|
||
| public Object tag() { | ||
| return tag; | ||
| } | ||
|
|
||
| Builder newBuilder() { | ||
| return new Builder(url) | ||
| .method(method, body) | ||
| .rawHeaders(headers) | ||
| .tag(tag); | ||
| } | ||
|
|
||
| public abstract static class Body { | ||
| /** Returns the Content-Type header for this body. */ | ||
| public abstract MediaType contentType(); | ||
|
|
||
| /** | ||
| * Returns the number of bytes that will be written to {@code out} in a call | ||
| * to {@link #writeTo}, or -1 if that count is unknown. | ||
| */ | ||
| public long contentLength() { | ||
| return -1; | ||
| } | ||
|
|
||
| /** Writes the content of this request to {@code out}. */ | ||
| public abstract void writeTo(OutputStream out) throws IOException; | ||
|
|
||
| /** | ||
| * Returns a new request body that transmits {@code content}. If {@code | ||
| * contentType} lacks a charset, this will use UTF-8. | ||
| */ | ||
| public static Body create(MediaType contentType, String content) { | ||
| contentType = contentType.charset() != null | ||
| ? contentType | ||
| : MediaType.parse(contentType + "; charset=utf-8"); | ||
| try { | ||
| byte[] bytes = content.getBytes(contentType.charset().name()); | ||
| return create(contentType, bytes); | ||
| } catch (UnsupportedEncodingException e) { | ||
| throw new AssertionError(); | ||
| } | ||
| } | ||
|
|
||
| /** Returns a new request body that transmits {@code content}. */ | ||
| public static Body create(final MediaType contentType, final byte[] content) { | ||
| if (contentType == null) throw new NullPointerException("contentType == null"); | ||
| if (content == null) throw new NullPointerException("content == null"); | ||
|
|
||
| return new Body() { | ||
| @Override public MediaType contentType() { | ||
| return contentType; | ||
| } | ||
|
|
||
| @Override public long contentLength() { | ||
| return content.length; | ||
| } | ||
|
|
||
| @Override public void writeTo(OutputStream out) throws IOException { | ||
| out.write(content); | ||
| } | ||
| }; | ||
| } | ||
|
|
||
| /** Returns a new request body that transmits the content of {@code file}. */ | ||
| public static Body create(final MediaType contentType, final File file) { | ||
| if (contentType == null) throw new NullPointerException("contentType == null"); | ||
| if (file == null) throw new NullPointerException("content == null"); | ||
|
|
||
| return new Body() { | ||
| @Override public MediaType contentType() { | ||
| return contentType; | ||
| } | ||
|
|
||
| @Override public long contentLength() { | ||
| return file.length(); | ||
| } | ||
|
|
||
| @Override public void writeTo(OutputStream out) throws IOException { | ||
| long length = contentLength(); | ||
| if (length == 0) return; | ||
|
|
||
| InputStream in = null; | ||
| try { | ||
| in = new FileInputStream(file); | ||
| byte[] buffer = new byte[(int) Math.min(8192, length)]; | ||
| for (int c; (c = in.read(buffer)) != -1; ) { | ||
| out.write(buffer, 0, c); | ||
| } | ||
| } finally { | ||
| Util.closeQuietly(in); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| } | ||
|
|
||
| public static class Builder { | ||
| private URL url; | ||
| private String method = "GET"; | ||
| private RawHeaders headers = new RawHeaders(); | ||
| private Body body; | ||
| private Object tag; | ||
|
|
||
| public Builder(String url) { | ||
| url(url); | ||
| } | ||
|
|
||
| public Builder(URL url) { | ||
| url(url); | ||
| } | ||
|
|
||
| public Builder url(String url) { | ||
| try { | ||
| this.url = new URL(url); | ||
| return this; | ||
| } catch (MalformedURLException e) { | ||
| throw new IllegalArgumentException("Malformed URL: " + url); | ||
| } | ||
| } | ||
|
|
||
| public Builder url(URL url) { | ||
| if (url == null) throw new IllegalStateException("url == null"); | ||
| this.url = url; | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the header named {@code name} to {@code value}. If this request | ||
| * already has any headers with that name, they are all replaced. | ||
| */ | ||
| public Builder header(String name, String value) { | ||
| headers.set(name, value); | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Adds a header with {@code name} and {@code value}. Prefer this method for | ||
| * multiply-valued headers like "Cookie". | ||
| */ | ||
| public Builder addHeader(String name, String value) { | ||
| headers.add(name, value); | ||
| return this; | ||
| } | ||
|
|
||
| Builder rawHeaders(RawHeaders rawHeaders) { | ||
| headers = new RawHeaders(rawHeaders); | ||
| return this; | ||
| } | ||
|
|
||
| public Builder get() { | ||
| return method("GET", null); | ||
| } | ||
|
|
||
| public Builder head() { | ||
| return method("HEAD", null); | ||
| } | ||
|
|
||
| public Builder post(Body body) { | ||
| return method("POST", body); | ||
| } | ||
|
|
||
| public Builder put(Body body) { | ||
| return method("PUT", body); | ||
| } | ||
|
|
||
| public Builder method(String method, Body body) { | ||
| if (method == null || method.length() == 0) { | ||
| throw new IllegalArgumentException("method == null || method.length() == 0"); | ||
| } | ||
| this.method = method; | ||
| this.body = body; | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Attaches {@code tag} to the request. It can be used later to cancel the | ||
| * request. If the tag is unspecified or null, the request is canceled by | ||
| * using the request itself as the tag. | ||
| */ | ||
| public Builder tag(Object tag) { | ||
| this.tag = tag; | ||
| return this; | ||
| } | ||
|
|
||
| public Request build() { | ||
| return new Request(this); | ||
| } | ||
| } | ||
| } |
| @@ -0,0 +1,290 @@ | ||
| /* | ||
| * Copyright (C) 2013 Square, Inc. | ||
| * | ||
| * Licensed 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.squareup.okhttp; | ||
|
|
||
| import com.squareup.okhttp.internal.Util; | ||
| import com.squareup.okhttp.internal.http.RawHeaders; | ||
| import java.io.ByteArrayOutputStream; | ||
| import java.io.IOException; | ||
| import java.io.InputStream; | ||
| import java.io.InputStreamReader; | ||
| import java.io.Reader; | ||
| import java.nio.charset.Charset; | ||
| import java.util.List; | ||
| import java.util.Set; | ||
|
|
||
| import static com.squareup.okhttp.internal.Util.UTF_8; | ||
|
|
||
| /** | ||
| * An HTTP response. Instances of this class are not immutable: the response | ||
| * body is a one-shot value that may be consumed only once. All other properties | ||
| * are immutable. | ||
| * | ||
| * <h3>Warning: Experimental OkHttp 2.0 API</h3> | ||
| * This class is in beta. APIs are subject to change! | ||
| */ | ||
| /* OkHttp 2.0: public */ final class Response { | ||
| private final Request request; | ||
| private final int code; | ||
| private final RawHeaders headers; | ||
| private final Body body; | ||
| private final Response redirectedBy; | ||
|
|
||
| private Response(Builder builder) { | ||
| this.request = builder.request; | ||
| this.code = builder.code; | ||
| this.headers = new RawHeaders(builder.headers); | ||
| this.body = builder.body; | ||
| this.redirectedBy = builder.redirectedBy; | ||
| } | ||
|
|
||
| /** | ||
| * The wire-level request that initiated this HTTP response. This is usually | ||
| * <strong>not</strong> the same request instance provided to the HTTP client: | ||
| * <ul> | ||
| * <li>It may be transformed by the HTTP client. For example, the client | ||
| * may have added its own {@code Content-Encoding} header to enable | ||
| * response compression. | ||
| * <li>It may be the request generated in response to an HTTP redirect. | ||
| * In this case the request URL may be different than the initial | ||
| * request URL. | ||
| * </ul> | ||
| */ | ||
| public Request request() { | ||
| return request; | ||
| } | ||
|
|
||
| public int code() { | ||
| return code; | ||
| } | ||
|
|
||
| public String header(String name) { | ||
| return header(name, null); | ||
| } | ||
|
|
||
| public String header(String name, String defaultValue) { | ||
| String result = headers.get(name); | ||
| return result != null ? result : defaultValue; | ||
| } | ||
|
|
||
| public List<String> headers(String name) { | ||
| return headers.values(name); | ||
| } | ||
|
|
||
| public Set<String> headerNames() { | ||
| return headers.names(); | ||
| } | ||
|
|
||
| public int headerCount() { | ||
| return headers.length(); | ||
| } | ||
|
|
||
| public String headerName(int index) { | ||
| return headers.getFieldName(index); | ||
| } | ||
|
|
||
| RawHeaders rawHeaders() { | ||
| return new RawHeaders(headers); | ||
| } | ||
|
|
||
| public String headerValue(int index) { | ||
| return headers.getValue(index); | ||
| } | ||
|
|
||
| public Body body() { | ||
| return body; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the response for the HTTP redirect that triggered this response, or | ||
| * null if this response wasn't triggered by an automatic redirect. The body | ||
| * of the returned response should not be read because it has already been | ||
| * consumed by the redirecting client. | ||
| */ | ||
| public Response redirectedBy() { | ||
| return redirectedBy; | ||
| } | ||
|
|
||
| public abstract static class Body { | ||
| /** Multiple calls to {@link #charStream()} must return the same instance. */ | ||
| private Reader reader; | ||
|
|
||
| /** | ||
| * Returns true if further data from this response body should be read at | ||
| * this time. For asynchronous transports like SPDY and HTTP/2.0, this will | ||
| * return false once all locally-available body bytes have been read. | ||
| * | ||
| * <p>Clients with many concurrent downloads can use this method to reduce | ||
| * the number of idle threads blocking on reads. See {@link | ||
| * Receiver#onResponse} for details. | ||
| */ | ||
| // <h3>Body.ready() vs. InputStream.available()</h3> | ||
| // TODO: Can we fix response bodies to implement InputStream.available well? | ||
| // The deflater implementation is broken by default but we could do better. | ||
| public abstract boolean ready() throws IOException; | ||
|
|
||
| public abstract MediaType contentType(); | ||
|
|
||
| /** | ||
| * Returns the number of bytes in that will returned by {@link #bytes}, or | ||
| * {@link #byteStream}, or -1 if unknown. | ||
| */ | ||
| public abstract long contentLength(); | ||
|
|
||
| public abstract InputStream byteStream() throws IOException; | ||
|
|
||
| public final byte[] bytes() throws IOException { | ||
| long contentLength = contentLength(); | ||
| if (contentLength > Integer.MAX_VALUE) { | ||
| throw new IOException("Cannot buffer entire body for content length: " + contentLength); | ||
| } | ||
|
|
||
| if (contentLength != -1) { | ||
| byte[] content = new byte[(int) contentLength]; | ||
| InputStream in = byteStream(); | ||
| Util.readFully(in, content); | ||
| if (in.read() != -1) throw new IOException("Content-Length and stream length disagree"); | ||
| return content; | ||
|
|
||
| } else { | ||
| ByteArrayOutputStream out = new ByteArrayOutputStream(); | ||
| Util.copy(byteStream(), out); | ||
| return out.toByteArray(); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Returns the response as a character stream decoded with the charset | ||
| * of the Content-Type header. If that header is either absent or lacks a | ||
| * charset, this will attempt to decode the response body as UTF-8. | ||
| */ | ||
| public final Reader charStream() throws IOException { | ||
| if (reader == null) { | ||
| reader = new InputStreamReader(byteStream(), charset()); | ||
| } | ||
| return reader; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the response as a string decoded with the charset of the | ||
| * Content-Type header. If that header is either absent or lacks a charset, | ||
| * this will attempt to decode the response body as UTF-8. | ||
| */ | ||
| public final String string() throws IOException { | ||
| return new String(bytes(), charset().name()); | ||
| } | ||
|
|
||
| private Charset charset() { | ||
| MediaType contentType = contentType(); | ||
| return contentType != null ? contentType.charset(UTF_8) : UTF_8; | ||
| } | ||
| } | ||
|
|
||
| public interface Receiver { | ||
| /** | ||
| * Called when the request could not be executed due to a connectivity | ||
| * problem or timeout. Because networks can fail during an exchange, it is | ||
| * possible that the remote server accepted the request before the failure. | ||
| */ | ||
| void onFailure(Failure failure); | ||
|
|
||
| /** | ||
| * Called when the HTTP response was successfully returned by the remote | ||
| * server. The receiver may proceed to read the response body with the | ||
| * response's {@link #body} method. | ||
| * | ||
| * <p>Note that transport-layer success (receiving a HTTP response code, | ||
| * headers and body) does not necessarily indicate application-layer | ||
| * success: {@code response} may still indicate an unhappy HTTP response | ||
| * code like 404 or 500. | ||
| * | ||
| * <h3>Non-blocking responses</h3> | ||
| * | ||
| * <p>Receivers do not need to block while waiting for the response body to | ||
| * download. Instead, they can get called back as data arrives. Use {@link | ||
| * Body#ready} to check if bytes should be read immediately. While there is | ||
| * data ready, read it. If there isn't, return false: receivers will be | ||
| * called back with {@code onResponse()} as additional data is downloaded. | ||
| * | ||
| * <p>Return true to indicate that the receiver has finished handling the | ||
| * response body. If the response body has unread data, it will be | ||
| * discarded. | ||
| * | ||
| * <p>When the response body has been fully consumed the returned value is | ||
| * undefined. | ||
| * | ||
| * <p>The current implementation of {@link Body#ready} always returns true | ||
| * when the underlying transport is HTTP/1. This results in blocking on that | ||
| * transport. For effective non-blocking your server must support SPDY or | ||
| * HTTP/2. | ||
| */ | ||
| boolean onResponse(Response response) throws IOException; | ||
| } | ||
|
|
||
| public static class Builder { | ||
| private final Request request; | ||
| private final int code; | ||
| private RawHeaders headers = new RawHeaders(); | ||
| private Body body; | ||
| private Response redirectedBy; | ||
|
|
||
| public Builder(Request request, int code) { | ||
| if (request == null) throw new IllegalArgumentException("request == null"); | ||
| if (code <= 0) throw new IllegalArgumentException("code <= 0"); | ||
| this.request = request; | ||
| this.code = code; | ||
| } | ||
|
|
||
| /** | ||
| * Sets the header named {@code name} to {@code value}. If this request | ||
| * already has any headers with that name, they are all replaced. | ||
| */ | ||
| public Builder header(String name, String value) { | ||
| headers.set(name, value); | ||
| return this; | ||
| } | ||
|
|
||
| /** | ||
| * Adds a header with {@code name} and {@code value}. Prefer this method for | ||
| * multiply-valued headers like "Set-Cookie". | ||
| */ | ||
| public Builder addHeader(String name, String value) { | ||
| headers.add(name, value); | ||
| return this; | ||
| } | ||
|
|
||
| Builder rawHeaders(RawHeaders rawHeaders) { | ||
| headers = new RawHeaders(rawHeaders); | ||
| return this; | ||
| } | ||
|
|
||
| public Builder body(Body body) { | ||
| this.body = body; | ||
| return this; | ||
| } | ||
|
|
||
| public Builder redirectedBy(Response redirectedBy) { | ||
| this.redirectedBy = redirectedBy; | ||
| return this; | ||
| } | ||
|
|
||
| public Response build() { | ||
| if (request == null) throw new IllegalStateException("Response has no request."); | ||
| if (code == -1) throw new IllegalStateException("Response has no code."); | ||
| return new Response(this); | ||
| } | ||
| } | ||
| } |
| @@ -0,0 +1,37 @@ | ||
| /* | ||
| * Copyright (C) 2011 The Android Open Source Project | ||
| * | ||
| * Licensed 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.squareup.okhttp; | ||
|
|
||
| /** The source of an HTTP response. */ | ||
| public enum ResponseSource { | ||
|
|
||
| /** The response was returned from the local cache. */ | ||
| CACHE, | ||
|
|
||
| /** | ||
| * The response is available in the cache but must be validated with the | ||
| * network. The cache result will be used if it is still valid; otherwise | ||
| * the network's response will be used. | ||
| */ | ||
| CONDITIONAL_CACHE, | ||
|
|
||
| /** The response was returned from the network. */ | ||
| NETWORK; | ||
|
|
||
| public boolean requiresConnection() { | ||
| return this == CONDITIONAL_CACHE || this == NETWORK; | ||
| } | ||
| } |
| @@ -0,0 +1,91 @@ | ||
| /* | ||
| * Copyright (C) 2013 Square, Inc. | ||
| * | ||
| * Licensed 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.squareup.okhttp; | ||
|
|
||
| import java.net.InetSocketAddress; | ||
| import java.net.Proxy; | ||
|
|
||
| /** Represents the route used by a connection to reach an endpoint. */ | ||
| public class Route { | ||
| final Address address; | ||
| final Proxy proxy; | ||
| final InetSocketAddress inetSocketAddress; | ||
| final boolean modernTls; | ||
|
|
||
| public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress, | ||
| boolean modernTls) { | ||
| if (address == null) throw new NullPointerException("address == null"); | ||
| if (proxy == null) throw new NullPointerException("proxy == null"); | ||
| if (inetSocketAddress == null) throw new NullPointerException("inetSocketAddress == null"); | ||
| this.address = address; | ||
| this.proxy = proxy; | ||
| this.inetSocketAddress = inetSocketAddress; | ||
| this.modernTls = modernTls; | ||
| } | ||
|
|
||
| /** Returns the {@link Address} of this route. */ | ||
| public Address getAddress() { | ||
| return address; | ||
| } | ||
|
|
||
| /** | ||
| * Returns the {@link Proxy} of this route. | ||
| * | ||
| * <strong>Warning:</strong> This may be different than the proxy returned | ||
| * by {@link #getAddress}! That is the proxy that the user asked to be | ||
| * connected to; this returns the proxy that they were actually connected | ||
| * to. The two may disagree when a proxy selector selects a different proxy | ||
| * for a connection. | ||
| */ | ||
| public Proxy getProxy() { | ||
| return proxy; | ||
| } | ||
|
|
||
| /** Returns the {@link InetSocketAddress} of this route. */ | ||
| public InetSocketAddress getSocketAddress() { | ||
| return inetSocketAddress; | ||
| } | ||
|
|
||
| /** Returns true if this route uses modern TLS. */ | ||
| public boolean isModernTls() { | ||
| return modernTls; | ||
| } | ||
|
|
||
| /** Returns a copy of this route with flipped TLS mode. */ | ||
| Route flipTlsMode() { | ||
| return new Route(address, proxy, inetSocketAddress, !modernTls); | ||
| } | ||
|
|
||
| @Override public boolean equals(Object obj) { | ||
| if (obj instanceof Route) { | ||
| Route other = (Route) obj; | ||
| return (address.equals(other.address) | ||
| && proxy.equals(other.proxy) | ||
| && inetSocketAddress.equals(other.inetSocketAddress) | ||
| && modernTls == other.modernTls); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| @Override public int hashCode() { | ||
| int result = 17; | ||
| result = 31 * result + address.hashCode(); | ||
| result = 31 * result + proxy.hashCode(); | ||
| result = 31 * result + inetSocketAddress.hashCode(); | ||
| result = result + (modernTls ? (31 * result) : 0); | ||
| return result; | ||
| } | ||
| } |
| @@ -0,0 +1,57 @@ | ||
| /* | ||
| * Copyright (C) 2013 Square, Inc. | ||
| * | ||
| * Licensed 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.squareup.okhttp; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.LinkedHashSet; | ||
| import java.util.Set; | ||
| import javax.net.ssl.SSLHandshakeException; | ||
|
|
||
| /** | ||
| * A blacklist of failed routes to avoid when creating a new connection to a | ||
| * target address. This is used so that OkHttp can learn from its mistakes: if | ||
| * there was a failure attempting to connect to a specific IP address, proxy | ||
| * server or TLS mode, that failure is remembered and alternate routes are | ||
| * preferred. | ||
| */ | ||
| public final class RouteDatabase { | ||
| private final Set<Route> failedRoutes = new LinkedHashSet<Route>(); | ||
|
|
||
| /** Records a failure connecting to {@code failedRoute}. */ | ||
| public synchronized void failed(Route failedRoute, IOException failure) { | ||
| failedRoutes.add(failedRoute); | ||
|
|
||
| if (!(failure instanceof SSLHandshakeException)) { | ||
| // If the problem was not related to SSL then it will also fail with | ||
| // a different TLS mode therefore we can be proactive about it. | ||
| failedRoutes.add(failedRoute.flipTlsMode()); | ||
| } | ||
| } | ||
|
|
||
| /** Records success connecting to {@code failedRoute}. */ | ||
| public synchronized void connected(Route route) { | ||
| failedRoutes.remove(route); | ||
| } | ||
|
|
||
| /** Returns true if {@code route} has failed recently and should be avoided. */ | ||
| public synchronized boolean shouldPostpone(Route route) { | ||
| return failedRoutes.contains(route); | ||
| } | ||
|
|
||
| public synchronized int failedRoutesCount() { | ||
| return failedRoutes.size(); | ||
| } | ||
| } |
| @@ -0,0 +1,75 @@ | ||
| /* | ||
| * Copyright (C) 2012 The Android Open Source Project | ||
| * | ||
| * Licensed 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.squareup.okhttp; | ||
|
|
||
| import com.squareup.okhttp.internal.http.RawHeaders; | ||
|
|
||
| import static com.squareup.okhttp.internal.Util.getDefaultPort; | ||
|
|
||
| /** | ||
| * Routing and authentication information sent to an HTTP proxy to create a | ||
| * HTTPS to an origin server. Everything in the tunnel request is sent | ||
| * unencrypted to the proxy server. | ||
| * | ||
| * <p>See <a href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817, Section | ||
| * 5.2</a>. | ||
| */ | ||
| public final class TunnelRequest { | ||
| final String host; | ||
| final int port; | ||
| final String userAgent; | ||
| final String proxyAuthorization; | ||
|
|
||
| /** | ||
| * @param host the origin server's hostname. Not null. | ||
| * @param port the origin server's port, like 80 or 443. | ||
| * @param userAgent the client's user-agent. Not null. | ||
| * @param proxyAuthorization proxy authorization, or null if the proxy is | ||
| * used without an authorization header. | ||
| */ | ||
| public TunnelRequest(String host, int port, String userAgent, String proxyAuthorization) { | ||
| if (host == null) throw new NullPointerException("host == null"); | ||
| if (userAgent == null) throw new NullPointerException("userAgent == null"); | ||
| this.host = host; | ||
| this.port = port; | ||
| this.userAgent = userAgent; | ||
| this.proxyAuthorization = proxyAuthorization; | ||
| } | ||
|
|
||
| /** | ||
| * If we're creating a TLS tunnel, send only the minimum set of headers. | ||
| * This avoids sending potentially sensitive data like HTTP cookies to | ||
| * the proxy unencrypted. | ||
| */ | ||
| RawHeaders getRequestHeaders() { | ||
| RawHeaders result = new RawHeaders(); | ||
| result.setRequestLine("CONNECT " + host + ":" + port + " HTTP/1.1"); | ||
|
|
||
| // Always set Host and User-Agent. | ||
| result.set("Host", port == getDefaultPort("https") ? host : (host + ":" + port)); | ||
| result.set("User-Agent", userAgent); | ||
|
|
||
| // Copy over the Proxy-Authorization header if it exists. | ||
| if (proxyAuthorization != null) { | ||
| result.set("Proxy-Authorization", proxyAuthorization); | ||
| } | ||
|
|
||
| // Always set the Proxy-Connection to Keep-Alive for the benefit of | ||
| // HTTP/1.0 proxies like Squid. | ||
| result.set("Proxy-Connection", "Keep-Alive"); | ||
| return result; | ||
| } | ||
| } |
| @@ -0,0 +1,45 @@ | ||
| /* | ||
| * Copyright (C) 2010 The Android Open Source Project | ||
| * | ||
| * Licensed 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.squareup.okhttp.internal; | ||
|
|
||
| import java.io.IOException; | ||
| import java.io.OutputStream; | ||
|
|
||
| /** | ||
| * An output stream for an HTTP request body. | ||
| * | ||
| * <p>Since a single socket's output stream may be used to write multiple HTTP | ||
| * requests to the same server, subclasses should not close the socket stream. | ||
| */ | ||
| public abstract class AbstractOutputStream extends OutputStream { | ||
| protected boolean closed; | ||
|
|
||
| @Override public final void write(int data) throws IOException { | ||
| write(new byte[] { (byte) data }); | ||
| } | ||
|
|
||
| protected final void checkNotClosed() throws IOException { | ||
| if (closed) { | ||
| throw new IOException("stream closed"); | ||
| } | ||
| } | ||
|
|
||
| /** Returns true if this stream was closed locally. */ | ||
| public boolean isClosed() { | ||
| return closed; | ||
| } | ||
| } |
| @@ -0,0 +1,164 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
|
|
||
| /** | ||
| * @author Alexander Y. Kleymenov | ||
| */ | ||
|
|
||
| package com.squareup.okhttp.internal; | ||
|
|
||
| import java.io.UnsupportedEncodingException; | ||
|
|
||
| import static com.squareup.okhttp.internal.Util.EMPTY_BYTE_ARRAY; | ||
|
|
||
| /** | ||
| * <a href="http://www.ietf.org/rfc/rfc2045.txt">Base64</a> encoder/decoder. | ||
| * In violation of the RFC, this encoder doesn't wrap lines at 76 columns. | ||
| */ | ||
| public final class Base64 { | ||
| private Base64() { | ||
| } | ||
|
|
||
| public static byte[] decode(byte[] in) { | ||
| return decode(in, in.length); | ||
| } | ||
|
|
||
| public static byte[] decode(byte[] in, int len) { | ||
| // approximate output length | ||
| int length = len / 4 * 3; | ||
| // return an empty array on empty or short input without padding | ||
| if (length == 0) { | ||
| return EMPTY_BYTE_ARRAY; | ||
| } | ||
| // temporary array | ||
| byte[] out = new byte[length]; | ||
| // number of padding characters ('=') | ||
| int pad = 0; | ||
| byte chr; | ||
| // compute the number of the padding characters | ||
| // and adjust the length of the input | ||
| for (; ; len--) { | ||
| chr = in[len - 1]; | ||
| // skip the neutral characters | ||
| if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) { | ||
| continue; | ||
| } | ||
| if (chr == '=') { | ||
| pad++; | ||
| } else { | ||
| break; | ||
| } | ||
| } | ||
| // index in the output array | ||
| int outIndex = 0; | ||
| // index in the input array | ||
| int inIndex = 0; | ||
| // holds the value of the input character | ||
| int bits = 0; | ||
| // holds the value of the input quantum | ||
| int quantum = 0; | ||
| for (int i = 0; i < len; i++) { | ||
| chr = in[i]; | ||
| // skip the neutral characters | ||
| if ((chr == '\n') || (chr == '\r') || (chr == ' ') || (chr == '\t')) { | ||
| continue; | ||
| } | ||
| if ((chr >= 'A') && (chr <= 'Z')) { | ||
| // char ASCII value | ||
| // A 65 0 | ||
| // Z 90 25 (ASCII - 65) | ||
| bits = chr - 65; | ||
| } else if ((chr >= 'a') && (chr <= 'z')) { | ||
| // char ASCII value | ||
| // a 97 26 | ||
| // z 122 51 (ASCII - 71) | ||
| bits = chr - 71; | ||
| } else if ((chr >= '0') && (chr <= '9')) { | ||
| // char ASCII value | ||
| // 0 48 52 | ||
| // 9 57 61 (ASCII + 4) | ||
| bits = chr + 4; | ||
| } else if (chr == '+') { | ||
| bits = 62; | ||
| } else if (chr == '/') { | ||
| bits = 63; | ||
| } else { | ||
| return null; | ||
| } | ||
| // append the value to the quantum | ||
| quantum = (quantum << 6) | (byte) bits; | ||
| if (inIndex % 4 == 3) { | ||
| // 4 characters were read, so make the output: | ||
| out[outIndex++] = (byte) (quantum >> 16); | ||
| out[outIndex++] = (byte) (quantum >> 8); | ||
| out[outIndex++] = (byte) quantum; | ||
| } | ||
| inIndex++; | ||
| } | ||
| if (pad > 0) { | ||
| // adjust the quantum value according to the padding | ||
| quantum = quantum << (6 * pad); | ||
| // make output | ||
| out[outIndex++] = (byte) (quantum >> 16); | ||
| if (pad == 1) { | ||
| out[outIndex++] = (byte) (quantum >> 8); | ||
| } | ||
| } | ||
| // create the resulting array | ||
| byte[] result = new byte[outIndex]; | ||
| System.arraycopy(out, 0, result, 0, outIndex); | ||
| return result; | ||
| } | ||
|
|
||
| private static final byte[] MAP = new byte[] { | ||
| 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', | ||
| 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', | ||
| 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', | ||
| '5', '6', '7', '8', '9', '+', '/' | ||
| }; | ||
|
|
||
| public static String encode(byte[] in) { | ||
| int length = (in.length + 2) * 4 / 3; | ||
| byte[] out = new byte[length]; | ||
| int index = 0, end = in.length - in.length % 3; | ||
| for (int i = 0; i < end; i += 3) { | ||
| out[index++] = MAP[(in[i] & 0xff) >> 2]; | ||
| out[index++] = MAP[((in[i] & 0x03) << 4) | ((in[i + 1] & 0xff) >> 4)]; | ||
| out[index++] = MAP[((in[i + 1] & 0x0f) << 2) | ((in[i + 2] & 0xff) >> 6)]; | ||
| out[index++] = MAP[(in[i + 2] & 0x3f)]; | ||
| } | ||
| switch (in.length % 3) { | ||
| case 1: | ||
| out[index++] = MAP[(in[end] & 0xff) >> 2]; | ||
| out[index++] = MAP[(in[end] & 0x03) << 4]; | ||
| out[index++] = '='; | ||
| out[index++] = '='; | ||
| break; | ||
| case 2: | ||
| out[index++] = MAP[(in[end] & 0xff) >> 2]; | ||
| out[index++] = MAP[((in[end] & 0x03) << 4) | ((in[end + 1] & 0xff) >> 4)]; | ||
| out[index++] = MAP[((in[end + 1] & 0x0f) << 2)]; | ||
| out[index++] = '='; | ||
| break; | ||
| } | ||
| try { | ||
| return new String(out, 0, index, "US-ASCII"); | ||
| } catch (UnsupportedEncodingException e) { | ||
| throw new AssertionError(e); | ||
| } | ||
| } | ||
| } |