Home | History | Annotate | Download | only in toolbox
      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 
     17 package com.android.volley.toolbox;
     18 
     19 import com.android.volley.AuthFailureError;
     20 import com.android.volley.Request;
     21 import com.android.volley.Request.Method;
     22 
     23 import org.apache.http.Header;
     24 import org.apache.http.HttpEntity;
     25 import org.apache.http.HttpResponse;
     26 import org.apache.http.ProtocolVersion;
     27 import org.apache.http.StatusLine;
     28 import org.apache.http.entity.BasicHttpEntity;
     29 import org.apache.http.message.BasicHeader;
     30 import org.apache.http.message.BasicHttpResponse;
     31 import org.apache.http.message.BasicStatusLine;
     32 
     33 import java.io.DataOutputStream;
     34 import java.io.IOException;
     35 import java.io.InputStream;
     36 import java.net.HttpURLConnection;
     37 import java.net.URL;
     38 import java.util.HashMap;
     39 import java.util.List;
     40 import java.util.Map;
     41 import java.util.Map.Entry;
     42 
     43 import javax.net.ssl.HttpsURLConnection;
     44 import javax.net.ssl.SSLSocketFactory;
     45 
     46 /**
     47  * An {@link HttpStack} based on {@link HttpURLConnection}.
     48  */
     49 public class HurlStack implements HttpStack {
     50 
     51     private static final String HEADER_CONTENT_TYPE = "Content-Type";
     52 
     53     /**
     54      * An interface for transforming URLs before use.
     55      */
     56     public interface UrlRewriter {
     57         /**
     58          * Returns a URL to use instead of the provided one, or null to indicate
     59          * this URL should not be used at all.
     60          */
     61         public String rewriteUrl(String originalUrl);
     62     }
     63 
     64     private final UrlRewriter mUrlRewriter;
     65     private final SSLSocketFactory mSslSocketFactory;
     66 
     67     public HurlStack() {
     68         this(null);
     69     }
     70 
     71     /**
     72      * @param urlRewriter Rewriter to use for request URLs
     73      */
     74     public HurlStack(UrlRewriter urlRewriter) {
     75         this(urlRewriter, null);
     76     }
     77 
     78     /**
     79      * @param urlRewriter Rewriter to use for request URLs
     80      * @param sslSocketFactory SSL factory to use for HTTPS connections
     81      */
     82     public HurlStack(UrlRewriter urlRewriter, SSLSocketFactory sslSocketFactory) {
     83         mUrlRewriter = urlRewriter;
     84         mSslSocketFactory = sslSocketFactory;
     85     }
     86 
     87     @Override
     88     public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
     89             throws IOException, AuthFailureError {
     90         String url = request.getUrl();
     91         HashMap<String, String> map = new HashMap<String, String>();
     92         map.putAll(request.getHeaders());
     93         map.putAll(additionalHeaders);
     94         if (mUrlRewriter != null) {
     95             String rewritten = mUrlRewriter.rewriteUrl(url);
     96             if (rewritten == null) {
     97                 throw new IOException("URL blocked by rewriter: " + url);
     98             }
     99             url = rewritten;
    100         }
    101         URL parsedUrl = new URL(url);
    102         HttpURLConnection connection = openConnection(parsedUrl, request);
    103         for (String headerName : map.keySet()) {
    104             connection.addRequestProperty(headerName, map.get(headerName));
    105         }
    106         setConnectionParametersForRequest(connection, request);
    107         // Initialize HttpResponse with data from the HttpURLConnection.
    108         ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
    109         int responseCode = connection.getResponseCode();
    110         if (responseCode == -1) {
    111             // -1 is returned by getResponseCode() if the response code could not be retrieved.
    112             // Signal to the caller that something was wrong with the connection.
    113             throw new IOException("Could not retrieve response code from HttpUrlConnection.");
    114         }
    115         StatusLine responseStatus = new BasicStatusLine(protocolVersion,
    116                 connection.getResponseCode(), connection.getResponseMessage());
    117         BasicHttpResponse response = new BasicHttpResponse(responseStatus);
    118         response.setEntity(entityFromConnection(connection));
    119         for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
    120             if (header.getKey() != null) {
    121                 Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
    122                 response.addHeader(h);
    123             }
    124         }
    125         return response;
    126     }
    127 
    128     /**
    129      * Initializes an {@link HttpEntity} from the given {@link HttpURLConnection}.
    130      * @param connection
    131      * @return an HttpEntity populated with data from <code>connection</code>.
    132      */
    133     private static HttpEntity entityFromConnection(HttpURLConnection connection) {
    134         BasicHttpEntity entity = new BasicHttpEntity();
    135         InputStream inputStream;
    136         try {
    137             inputStream = connection.getInputStream();
    138         } catch (IOException ioe) {
    139             inputStream = connection.getErrorStream();
    140         }
    141         entity.setContent(inputStream);
    142         entity.setContentLength(connection.getContentLength());
    143         entity.setContentEncoding(connection.getContentEncoding());
    144         entity.setContentType(connection.getContentType());
    145         return entity;
    146     }
    147 
    148     /**
    149      * Create an {@link HttpURLConnection} for the specified {@code url}.
    150      */
    151     protected HttpURLConnection createConnection(URL url) throws IOException {
    152         return (HttpURLConnection) url.openConnection();
    153     }
    154 
    155     /**
    156      * Opens an {@link HttpURLConnection} with parameters.
    157      * @param url
    158      * @return an open connection
    159      * @throws IOException
    160      */
    161     private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
    162         HttpURLConnection connection = createConnection(url);
    163 
    164         int timeoutMs = request.getTimeoutMs();
    165         connection.setConnectTimeout(timeoutMs);
    166         connection.setReadTimeout(timeoutMs);
    167         connection.setUseCaches(false);
    168         connection.setDoInput(true);
    169 
    170         // use caller-provided custom SslSocketFactory, if any, for HTTPS
    171         if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
    172             ((HttpsURLConnection)connection).setSSLSocketFactory(mSslSocketFactory);
    173         }
    174 
    175         return connection;
    176     }
    177 
    178     @SuppressWarnings("deprecation")
    179     /* package */ static void setConnectionParametersForRequest(HttpURLConnection connection,
    180             Request<?> request) throws IOException, AuthFailureError {
    181         switch (request.getMethod()) {
    182             case Method.DEPRECATED_GET_OR_POST:
    183                 // This is the deprecated way that needs to be handled for backwards compatibility.
    184                 // If the request's post body is null, then the assumption is that the request is
    185                 // GET.  Otherwise, it is assumed that the request is a POST.
    186                 byte[] postBody = request.getPostBody();
    187                 if (postBody != null) {
    188                     // Prepare output. There is no need to set Content-Length explicitly,
    189                     // since this is handled by HttpURLConnection using the size of the prepared
    190                     // output stream.
    191                     connection.setDoOutput(true);
    192                     connection.setRequestMethod("POST");
    193                     connection.addRequestProperty(HEADER_CONTENT_TYPE,
    194                             request.getPostBodyContentType());
    195                     DataOutputStream out = new DataOutputStream(connection.getOutputStream());
    196                     out.write(postBody);
    197                     out.close();
    198                 }
    199                 break;
    200             case Method.GET:
    201                 // Not necessary to set the request method because connection defaults to GET but
    202                 // being explicit here.
    203                 connection.setRequestMethod("GET");
    204                 break;
    205             case Method.DELETE:
    206                 connection.setRequestMethod("DELETE");
    207                 break;
    208             case Method.POST:
    209                 connection.setRequestMethod("POST");
    210                 addBodyIfExists(connection, request);
    211                 break;
    212             case Method.PUT:
    213                 connection.setRequestMethod("PUT");
    214                 addBodyIfExists(connection, request);
    215                 break;
    216             default:
    217                 throw new IllegalStateException("Unknown method type.");
    218         }
    219     }
    220 
    221     private static void addBodyIfExists(HttpURLConnection connection, Request<?> request)
    222             throws IOException, AuthFailureError {
    223         byte[] body = request.getBody();
    224         if (body != null) {
    225             connection.setDoOutput(true);
    226             connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType());
    227             DataOutputStream out = new DataOutputStream(connection.getOutputStream());
    228             out.write(body);
    229             out.close();
    230         }
    231     }
    232 }
    233