HurlStack.java revision d3207beaf19336cda461da53d1bdf1ba3d57df12
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.volley.toolbox; 18 19import com.android.volley.AuthFailureError; 20import com.android.volley.Request; 21import com.android.volley.Request.Method; 22 23import org.apache.http.Header; 24import org.apache.http.HttpEntity; 25import org.apache.http.HttpResponse; 26import org.apache.http.HttpStatus; 27import org.apache.http.ProtocolVersion; 28import org.apache.http.StatusLine; 29import org.apache.http.entity.BasicHttpEntity; 30import org.apache.http.message.BasicHeader; 31import org.apache.http.message.BasicHttpResponse; 32import org.apache.http.message.BasicStatusLine; 33 34import java.io.DataOutputStream; 35import java.io.IOException; 36import java.io.InputStream; 37import java.net.HttpURLConnection; 38import java.net.URL; 39import java.util.HashMap; 40import java.util.List; 41import java.util.Map; 42import java.util.Map.Entry; 43 44import javax.net.ssl.HttpsURLConnection; 45import javax.net.ssl.SSLSocketFactory; 46 47/** 48 * An {@link HttpStack} based on {@link HttpURLConnection}. 49 */ 50public class HurlStack implements HttpStack { 51 52 private static final String HEADER_CONTENT_TYPE = "Content-Type"; 53 54 /** 55 * An interface for transforming URLs before use. 56 */ 57 public interface UrlRewriter { 58 /** 59 * Returns a URL to use instead of the provided one, or null to indicate 60 * this URL should not be used at all. 61 */ 62 public String rewriteUrl(String originalUrl); 63 } 64 65 private final UrlRewriter mUrlRewriter; 66 private final SSLSocketFactory mSslSocketFactory; 67 68 public HurlStack() { 69 this(null); 70 } 71 72 /** 73 * @param urlRewriter Rewriter to use for request URLs 74 */ 75 public HurlStack(UrlRewriter urlRewriter) { 76 this(urlRewriter, null); 77 } 78 79 /** 80 * @param urlRewriter Rewriter to use for request URLs 81 * @param sslSocketFactory SSL factory to use for HTTPS connections 82 */ 83 public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) { 84 mUrlRewriter = urlRewriter; 85 mSslSocketFactory = sslSocketFactory; 86 } 87 88 @Override 89 public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) 90 throws IOException, AuthFailureError { 91 String url = request.getUrl(); 92 HashMap<String, String> map = new HashMap<String, String>(); 93 map.putAll(request.getHeaders()); 94 map.putAll(additionalHeaders); 95 if (mUrlRewriter != null) { 96 String rewritten = mUrlRewriter.rewriteUrl(url); 97 if (rewritten == null) { 98 throw new IOException("URL blocked by rewriter: " + url); 99 } 100 url = rewritten; 101 } 102 URL parsedUrl = new URL(url); 103 HttpURLConnection connection = openConnection(parsedUrl, request); 104 for (String headerName : map.keySet()) { 105 connection.addRequestProperty(headerName, map.get(headerName)); 106 } 107 setConnectionParametersForRequest(connection, request); 108 // Initialize HttpResponse with data from the HttpURLConnection. 109 ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1); 110 int responseCode = connection.getResponseCode(); 111 if (responseCode == -1) { 112 // -1 is returned by getResponseCode() if the response code could not be retrieved. 113 // Signal to the caller that something was wrong with the connection. 114 throw new IOException("Could not retrieve response code from HttpUrlConnection."); 115 } 116 StatusLine responseStatus = new BasicStatusLine(protocolVersion, 117 connection.getResponseCode(), connection.getResponseMessage()); 118 BasicHttpResponse response = new BasicHttpResponse(responseStatus); 119 if (hasResponseBody(request.getMethod(), responseStatus.getStatusCode())) { 120 response.setEntity(entityFromConnection(connection)); 121 } 122 for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) { 123 if (header.getKey() != null) { 124 Header h = new BasicHeader(header.getKey(), header.getValue().get(0)); 125 response.addHeader(h); 126 } 127 } 128 return response; 129 } 130 131 /** 132 * Checks if a response message contains a body. 133 * @see <a href="https://tools.ietf.org/html/rfc7230#section-3.3">RFC 7230 section 3.3</a> 134 * @param requestMethod request method 135 * @param responseCode response status code 136 * @return whether the response has a body 137 */ 138 private static boolean hasResponseBody(int requestMethod, int responseCode) { 139 return requestMethod != Request.Method.HEAD 140 && !(HttpStatus.SC_CONTINUE <= responseCode && responseCode < HttpStatus.SC_OK) 141 && responseCode != HttpStatus.SC_NO_CONTENT 142 && responseCode != HttpStatus.SC_NOT_MODIFIED; 143 } 144 145 /** 146 * Initializes an {@link HttpEntity} from the given {@link HttpURLConnection}. 147 * @param connection 148 * @return an HttpEntity populated with data from <code>connection</code>. 149 */ 150 private static HttpEntity entityFromConnection(HttpURLConnection connection) { 151 BasicHttpEntity entity = new BasicHttpEntity(); 152 InputStream inputStream; 153 try { 154 inputStream = connection.getInputStream(); 155 } catch (IOException ioe) { 156 inputStream = connection.getErrorStream(); 157 } 158 entity.setContent(inputStream); 159 entity.setContentLength(connection.getContentLength()); 160 entity.setContentEncoding(connection.getContentEncoding()); 161 entity.setContentType(connection.getContentType()); 162 return entity; 163 } 164 165 /** 166 * Create an {@link HttpURLConnection} for the specified {@code url}. 167 */ 168 protected HttpURLConnection createConnection(URL url) throws IOException { 169 return (HttpURLConnection) url.openConnection(); 170 } 171 172 /** 173 * Opens an {@link HttpURLConnection} with parameters. 174 * @param url 175 * @return an open connection 176 * @throws IOException 177 */ 178 private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException { 179 HttpURLConnection connection = createConnection(url); 180 181 int timeoutMs = request.getTimeoutMs(); 182 connection.setConnectTimeout(timeoutMs); 183 connection.setReadTimeout(timeoutMs); 184 connection.setUseCaches(false); 185 connection.setDoInput(true); 186 187 // use caller-provided custom SslSocketFactory, if any, for HTTPS 188 if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) { 189 ((HttpsURLConnection)connection).setSSLSocketFactory(mSslSocketFactory); 190 } 191 192 return connection; 193 } 194 195 @SuppressWarnings("deprecation") 196 /* package */ static void setConnectionParametersForRequest(HttpURLConnection connection, 197 Request<?> request) throws IOException, AuthFailureError { 198 switch (request.getMethod()) { 199 case Method.DEPRECATED_GET_OR_POST: 200 // This is the deprecated way that needs to be handled for backwards compatibility. 201 // If the request's post body is null, then the assumption is that the request is 202 // GET. Otherwise, it is assumed that the request is a POST. 203 byte[] postBody = request.getPostBody(); 204 if (postBody != null) { 205 // Prepare output. There is no need to set Content-Length explicitly, 206 // since this is handled by HttpURLConnection using the size of the prepared 207 // output stream. 208 connection.setDoOutput(true); 209 connection.setRequestMethod("POST"); 210 connection.addRequestProperty(HEADER_CONTENT_TYPE, 211 request.getPostBodyContentType()); 212 DataOutputStream out = new DataOutputStream(connection.getOutputStream()); 213 out.write(postBody); 214 out.close(); 215 } 216 break; 217 case Method.GET: 218 // Not necessary to set the request method because connection defaults to GET but 219 // being explicit here. 220 connection.setRequestMethod("GET"); 221 break; 222 case Method.DELETE: 223 connection.setRequestMethod("DELETE"); 224 break; 225 case Method.POST: 226 connection.setRequestMethod("POST"); 227 addBodyIfExists(connection, request); 228 break; 229 case Method.PUT: 230 connection.setRequestMethod("PUT"); 231 addBodyIfExists(connection, request); 232 break; 233 case Method.HEAD: 234 connection.setRequestMethod("HEAD"); 235 break; 236 case Method.OPTIONS: 237 connection.setRequestMethod("OPTIONS"); 238 break; 239 case Method.TRACE: 240 connection.setRequestMethod("TRACE"); 241 break; 242 case Method.PATCH: 243 connection.setRequestMethod("PATCH"); 244 addBodyIfExists(connection, request); 245 break; 246 default: 247 throw new IllegalStateException("Unknown method type."); 248 } 249 } 250 251 private static void addBodyIfExists(HttpURLConnection connection, Request<?> request) 252 throws IOException, AuthFailureError { 253 byte[] body = request.getBody(); 254 if (body != null) { 255 connection.setDoOutput(true); 256 connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType()); 257 DataOutputStream out = new DataOutputStream(connection.getOutputStream()); 258 out.write(body); 259 out.close(); 260 } 261 } 262} 263