1 package com.xtremelabs.robolectric.tester.org.apache.http;
2 
3 import com.xtremelabs.robolectric.Robolectric;
4 import com.xtremelabs.robolectric.shadows.HttpResponseGenerator;
5 import org.apache.http.*;
6 import org.apache.http.client.RequestDirector;
7 import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
8 import org.apache.http.conn.ConnectTimeoutException;
9 import org.apache.http.params.HttpConnectionParams;
10 import org.apache.http.params.HttpParams;
11 import org.apache.http.protocol.HttpContext;
12 
13 import java.io.IOException;
14 import java.net.URI;
15 import java.util.ArrayList;
16 import java.util.HashMap;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.regex.Pattern;
20 
21 public class FakeHttpLayer {
22     List<HttpResponseGenerator> pendingHttpResponses = new ArrayList<HttpResponseGenerator>();
23     List<HttpRequestInfo> httpRequestInfos = new ArrayList<HttpRequestInfo>();
24     List<HttpEntityStub.ResponseRule> httpResponseRules = new ArrayList<HttpEntityStub.ResponseRule>();
25     HttpResponse defaultHttpResponse;
26     private HttpResponse defaultResponse;
27     private boolean interceptHttpRequests = true;
28 
getLastSentHttpRequestInfo()29     public HttpRequestInfo getLastSentHttpRequestInfo() {
30         List<HttpRequestInfo> requestInfos = Robolectric.getFakeHttpLayer().getSentHttpRequestInfos();
31         if (requestInfos.isEmpty()) {
32             return null;
33         }
34         return requestInfos.get(requestInfos.size() - 1);
35     }
36 
addPendingHttpResponse(int statusCode, String responseBody, Header... headers)37     public void addPendingHttpResponse(int statusCode, String responseBody, Header... headers) {
38         addPendingHttpResponse(new TestHttpResponse(statusCode, responseBody, headers));
39     }
40 
addPendingHttpResponse(final HttpResponse httpResponse)41     public void addPendingHttpResponse(final HttpResponse httpResponse) {
42         addPendingHttpResponse(new HttpResponseGenerator() {
43             @Override
44             public HttpResponse getResponse(HttpRequest request) {
45                 return httpResponse;
46             }
47         });
48     }
49 
addPendingHttpResponse(HttpResponseGenerator httpResponseGenerator)50     public void addPendingHttpResponse(HttpResponseGenerator httpResponseGenerator) {
51         pendingHttpResponses.add(httpResponseGenerator);
52     }
53 
addHttpResponseRule(String method, String uri, HttpResponse response)54     public void addHttpResponseRule(String method, String uri, HttpResponse response) {
55         addHttpResponseRule(new DefaultRequestMatcher(method, uri), response);
56     }
57 
addHttpResponseRule(String uri, HttpResponse response)58     public void addHttpResponseRule(String uri, HttpResponse response) {
59         addHttpResponseRule(new UriRequestMatcher(uri), response);
60     }
61 
addHttpResponseRule(String uri, String response)62     public void addHttpResponseRule(String uri, String response) {
63         addHttpResponseRule(new UriRequestMatcher(uri), new TestHttpResponse(200, response));
64     }
65 
addHttpResponseRule(RequestMatcher requestMatcher, HttpResponse response)66     public void addHttpResponseRule(RequestMatcher requestMatcher, HttpResponse response) {
67         addHttpResponseRule(new RequestMatcherResponseRule(requestMatcher, response));
68     }
69 
70     /**
71      * Add a response rule.
72      *
73      * @param requestMatcher Request matcher
74      * @param responses      A list of responses that are returned to matching requests in order from first to last.
75      */
addHttpResponseRule(RequestMatcher requestMatcher, List<? extends HttpResponse> responses)76     public void addHttpResponseRule(RequestMatcher requestMatcher, List<? extends HttpResponse> responses) {
77         addHttpResponseRule(new RequestMatcherResponseRule(requestMatcher, responses));
78     }
79 
addHttpResponseRule(HttpEntityStub.ResponseRule responseRule)80     public void addHttpResponseRule(HttpEntityStub.ResponseRule responseRule) {
81         httpResponseRules.add(0, responseRule);
82     }
83 
setDefaultHttpResponse(HttpResponse defaultHttpResponse)84     public void setDefaultHttpResponse(HttpResponse defaultHttpResponse) {
85         this.defaultHttpResponse = defaultHttpResponse;
86     }
87 
setDefaultHttpResponse(int statusCode, String responseBody)88     public void setDefaultHttpResponse(int statusCode, String responseBody) {
89         setDefaultHttpResponse(new TestHttpResponse(statusCode, responseBody));
90     }
91 
findResponse(HttpRequest httpRequest)92     private HttpResponse findResponse(HttpRequest httpRequest) throws HttpException, IOException {
93         if (!pendingHttpResponses.isEmpty()) {
94             return pendingHttpResponses.remove(0).getResponse(httpRequest);
95         }
96 
97         for (HttpEntityStub.ResponseRule httpResponseRule : httpResponseRules) {
98             if (httpResponseRule.matches(httpRequest)) {
99                 return httpResponseRule.getResponse();
100             }
101         }
102 
103         return defaultHttpResponse;
104     }
105 
emulateRequest(HttpHost httpHost, HttpRequest httpRequest, HttpContext httpContext, RequestDirector requestDirector)106     public HttpResponse emulateRequest(HttpHost httpHost, HttpRequest httpRequest, HttpContext httpContext, RequestDirector requestDirector) throws HttpException, IOException {
107         HttpResponse httpResponse = findResponse(httpRequest);
108 
109         if (httpResponse == null) {
110             throw new RuntimeException("Unexpected call to execute, no pending responses are available. See Robolectric.addPendingResponse(). Request was: " +
111                     httpRequest.getRequestLine().getMethod() + " " + httpRequest.getRequestLine().getUri());
112         } else {
113             HttpParams params = httpResponse.getParams();
114 
115             if (HttpConnectionParams.getConnectionTimeout(params) < 0) {
116                 throw new ConnectTimeoutException("Socket is not connected");
117             } else if (HttpConnectionParams.getSoTimeout(params) < 0) {
118                 throw new ConnectTimeoutException("The operation timed out");
119             }
120         }
121 
122         httpRequestInfos.add(new HttpRequestInfo(httpRequest, httpHost, httpContext, requestDirector));
123 
124         return httpResponse;
125     }
126 
hasPendingResponses()127     public boolean hasPendingResponses() {
128         return !pendingHttpResponses.isEmpty();
129     }
130 
hasRequestInfos()131     public boolean hasRequestInfos() {
132         return !httpRequestInfos.isEmpty();
133     }
134 
clearRequestInfos()135     public void clearRequestInfos() {
136         httpRequestInfos.clear();
137     }
138 
hasResponseRules()139     public boolean hasResponseRules() {
140         return !httpResponseRules.isEmpty();
141     }
142 
hasRequestMatchingRule(RequestMatcher rule)143     public boolean hasRequestMatchingRule(RequestMatcher rule) {
144         for (HttpRequestInfo requestInfo : httpRequestInfos) {
145             if (rule.matches(requestInfo.httpRequest)) {
146                 return true;
147             }
148         }
149         return false;
150     }
151 
getDefaultResponse()152     public HttpResponse getDefaultResponse() {
153         return defaultResponse;
154     }
155 
getSentHttpRequestInfo(int index)156     public HttpRequestInfo getSentHttpRequestInfo(int index) {
157         return httpRequestInfos.get(index);
158     }
159 
getSentHttpRequestInfos()160     public List<HttpRequestInfo> getSentHttpRequestInfos() {
161         return new ArrayList<HttpRequestInfo>(httpRequestInfos);
162     }
163 
clearHttpResponseRules()164     public void clearHttpResponseRules() {
165         httpResponseRules.clear();
166     }
167 
clearPendingHttpResponses()168     public void clearPendingHttpResponses() {
169         pendingHttpResponses.clear();
170     }
171 
172     /**
173      * You can disable Robolectric's fake HTTP layer temporarily
174      * by calling this method.
175      * @param interceptHttpRequests whether all HTTP requests should be
176      *                              intercepted (true by default)
177      */
interceptHttpRequests(boolean interceptHttpRequests)178     public void interceptHttpRequests(boolean interceptHttpRequests) {
179         this.interceptHttpRequests = interceptHttpRequests;
180     }
181 
isInterceptingHttpRequests()182     public boolean isInterceptingHttpRequests() {
183         return interceptHttpRequests;
184     }
185 
186     public static class RequestMatcherResponseRule implements HttpEntityStub.ResponseRule {
187         private RequestMatcher requestMatcher;
188         private HttpResponse responseToGive;
189         private IOException ioException;
190         private HttpException httpException;
191         private List<? extends HttpResponse> responses;
192 
RequestMatcherResponseRule(RequestMatcher requestMatcher, HttpResponse responseToGive)193         public RequestMatcherResponseRule(RequestMatcher requestMatcher, HttpResponse responseToGive) {
194             this.requestMatcher = requestMatcher;
195             this.responseToGive = responseToGive;
196         }
197 
RequestMatcherResponseRule(RequestMatcher requestMatcher, IOException ioException)198         public RequestMatcherResponseRule(RequestMatcher requestMatcher, IOException ioException) {
199             this.requestMatcher = requestMatcher;
200             this.ioException = ioException;
201         }
202 
RequestMatcherResponseRule(RequestMatcher requestMatcher, HttpException httpException)203         public RequestMatcherResponseRule(RequestMatcher requestMatcher, HttpException httpException) {
204             this.requestMatcher = requestMatcher;
205             this.httpException = httpException;
206         }
207 
RequestMatcherResponseRule(RequestMatcher requestMatcher, List<? extends HttpResponse> responses)208         public RequestMatcherResponseRule(RequestMatcher requestMatcher, List<? extends HttpResponse> responses) {
209             this.requestMatcher = requestMatcher;
210             this.responses = responses;
211         }
212 
213         @Override
matches(HttpRequest request)214         public boolean matches(HttpRequest request) {
215             return requestMatcher.matches(request);
216         }
217 
218         @Override
getResponse()219         public HttpResponse getResponse() throws HttpException, IOException {
220             if (httpException != null) throw httpException;
221             if (ioException != null) throw ioException;
222             if (responseToGive != null) {
223                 return responseToGive;
224             } else {
225                 if (responses.isEmpty()) {
226                     throw new RuntimeException("No more responses left to give");
227                 }
228                 return responses.remove(0);
229             }
230         }
231     }
232 
233     public static class DefaultRequestMatcher implements RequestMatcher {
234         private String method;
235         private String uri;
236 
DefaultRequestMatcher(String method, String uri)237         public DefaultRequestMatcher(String method, String uri) {
238             this.method = method;
239             this.uri = uri;
240         }
241 
242         @Override
matches(HttpRequest request)243         public boolean matches(HttpRequest request) {
244             return request.getRequestLine().getMethod().equals(method) &&
245                     request.getRequestLine().getUri().equals(uri);
246         }
247     }
248 
249     public static class UriRequestMatcher implements RequestMatcher {
250         private String uri;
251 
UriRequestMatcher(String uri)252         public UriRequestMatcher(String uri) {
253             this.uri = uri;
254         }
255 
256         @Override
matches(HttpRequest request)257         public boolean matches(HttpRequest request) {
258             return request.getRequestLine().getUri().equals(uri);
259         }
260     }
261 
262     public static class RequestMatcherBuilder implements RequestMatcher {
263         private String method, hostname, path;
264         private boolean noParams;
265         private Map<String, String> params = new HashMap<String, String>();
266         private Map<String, String> headers = new HashMap<String, String>();
267         private PostBodyMatcher postBodyMatcher;
268 
269         public interface PostBodyMatcher {
270             /**
271              * Hint: you can use EntityUtils.toString(actualPostBody) to help you implement your matches method.
272              *
273              * @param actualPostBody The post body of the actual request that we are matching against.
274              * @return true if you consider the body to match
275              * @throws IOException Get turned into a RuntimeException to cause your test to fail.
276              */
matches(HttpEntity actualPostBody)277             boolean matches(HttpEntity actualPostBody) throws IOException;
278         }
279 
method(String method)280         public RequestMatcherBuilder method(String method) {
281             this.method = method;
282             return this;
283         }
284 
host(String hostname)285         public RequestMatcherBuilder host(String hostname) {
286             this.hostname = hostname;
287             return this;
288         }
289 
path(String path)290         public RequestMatcherBuilder path(String path) {
291             if (path.startsWith("/")) {
292                 throw new RuntimeException("Path should not start with '/'");
293             }
294             this.path = "/" + path;
295             return this;
296         }
297 
param(String name, String value)298         public RequestMatcherBuilder param(String name, String value) {
299             params.put(name, value);
300             return this;
301         }
302 
noParams()303         public RequestMatcherBuilder noParams() {
304             noParams = true;
305             return this;
306         }
307 
postBody(PostBodyMatcher postBodyMatcher)308         public RequestMatcherBuilder postBody(PostBodyMatcher postBodyMatcher) {
309             this.postBodyMatcher = postBodyMatcher;
310             return this;
311         }
312 
header(String name, String value)313         public RequestMatcherBuilder header(String name, String value) {
314             headers.put(name, value);
315             return this;
316         }
317 
318         @Override
matches(HttpRequest request)319         public boolean matches(HttpRequest request) {
320             URI uri = URI.create(request.getRequestLine().getUri());
321             if (method != null && !method.equals(request.getRequestLine().getMethod())) {
322                 return false;
323             }
324             if (hostname != null && !hostname.equals(uri.getHost())) {
325                 return false;
326             }
327             if (path != null && !path.equals(uri.getRawPath())) {
328                 return false;
329             }
330             if (noParams && !uri.getRawQuery().equals(null)) {
331                 return false;
332             }
333             if (params.size() > 0) {
334                 Map<String, String> requestParams = ParamsParser.parseParams(request);
335                 if (!requestParams.equals(params)) {
336                     return false;
337                 }
338             }
339             if (headers.size() > 0) {
340                 Map<String, String> actualRequestHeaders = new HashMap<String, String>();
341                 for (Header header : request.getAllHeaders()) {
342                     actualRequestHeaders.put(header.getName(), header.getValue());
343                 }
344                 if (!headers.equals(actualRequestHeaders)) {
345                     return false;
346                 }
347             }
348             if (postBodyMatcher != null) {
349                 if (!(request instanceof HttpEntityEnclosingRequestBase)) {
350                     return false;
351                 }
352                 HttpEntityEnclosingRequestBase postOrPut = (HttpEntityEnclosingRequestBase) request;
353                 try {
354                     if (!postBodyMatcher.matches(postOrPut.getEntity())) {
355                         return false;
356                     }
357                 } catch (IOException e) {
358                     throw new RuntimeException(e);
359                 }
360             }
361             return true;
362         }
363 
getHostname()364         String getHostname() {
365             return hostname;
366         }
367 
getPath()368         String getPath() {
369             return path;
370         }
371 
getParam(String key)372         String getParam(String key) {
373             return params.get(key);
374         }
375 
getHeader(String key)376         String getHeader(String key) {
377             return headers.get(key);
378         }
379 
isNoParams()380         boolean isNoParams() {
381             return noParams;
382         }
383 
getMethod()384         String getMethod() {
385             return method;
386         }
387     }
388 
389     public static class UriRegexMatcher implements RequestMatcher {
390         private String method;
391         private final Pattern uriRegex;
392 
UriRegexMatcher(String method, String uriRegex)393         public UriRegexMatcher(String method, String uriRegex) {
394             this.method = method;
395             this.uriRegex = Pattern.compile(uriRegex);
396         }
397 
398         @Override
matches(HttpRequest request)399         public boolean matches(HttpRequest request) {
400             return request.getRequestLine().getMethod().equals(method) &&
401                     uriRegex.matcher(request.getRequestLine().getUri()).matches();
402         }
403     }
404 }
405