1 package com.atlassian.sal.core.net;
2
3 import com.atlassian.sal.api.net.Request;
4 import com.atlassian.sal.api.net.RequestFilePart;
5 import com.atlassian.sal.api.net.ResponseConnectTimeoutException;
6 import com.atlassian.sal.api.net.ResponseException;
7 import com.atlassian.sal.api.net.ResponseHandler;
8 import com.atlassian.sal.api.net.ResponseProtocolException;
9 import com.atlassian.sal.api.net.ResponseReadTimeoutException;
10 import com.atlassian.sal.api.net.ResponseStatusException;
11 import com.atlassian.sal.api.net.ResponseTransportException;
12 import com.atlassian.sal.api.net.ReturningResponseHandler;
13 import com.atlassian.sal.api.net.auth.Authenticator;
14 import com.atlassian.sal.api.user.UserManager;
15 import com.atlassian.sal.core.net.auth.BaseAuthenticator;
16 import com.atlassian.sal.core.net.auth.HttpClientAuthenticator;
17 import com.atlassian.sal.core.net.auth.SeraphAuthenticator;
18 import com.atlassian.sal.core.net.auth.TrustedTokenAuthenticator;
19 import com.atlassian.sal.core.trusted.CertificateFactory;
20 import org.apache.commons.httpclient.ConnectTimeoutException;
21 import org.apache.commons.httpclient.Header;
22 import org.apache.commons.httpclient.HttpClient;
23 import org.apache.commons.httpclient.HttpConnectionManager;
24 import org.apache.commons.httpclient.HttpException;
25 import org.apache.commons.httpclient.HttpMethod;
26 import org.apache.commons.httpclient.NoHttpResponseException;
27 import org.apache.commons.httpclient.URI;
28 import org.apache.commons.httpclient.methods.DeleteMethod;
29 import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
30 import org.apache.commons.httpclient.methods.GetMethod;
31 import org.apache.commons.httpclient.methods.HeadMethod;
32 import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
33 import org.apache.commons.httpclient.methods.OptionsMethod;
34 import org.apache.commons.httpclient.methods.PostMethod;
35 import org.apache.commons.httpclient.methods.PutMethod;
36 import org.apache.commons.httpclient.methods.TraceMethod;
37 import org.apache.commons.httpclient.methods.multipart.FilePart;
38 import org.apache.commons.httpclient.methods.multipart.FilePartSource;
39 import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
40 import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
41 import org.apache.commons.httpclient.params.HttpMethodParams;
42 import org.apache.commons.lang.StringUtils;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
45
46 import java.io.ByteArrayInputStream;
47 import java.io.IOException;
48 import java.io.InputStream;
49 import java.io.UnsupportedEncodingException;
50 import java.net.SocketException;
51 import java.util.ArrayList;
52 import java.util.Arrays;
53 import java.util.Collections;
54 import java.util.HashMap;
55 import java.util.List;
56 import java.util.Map;
57
58
59
60
61 public class HttpClientRequest implements Request<HttpClientRequest, HttpClientResponse>
62 {
63 private static final Logger log = LoggerFactory.getLogger(HttpClientRequest.class);
64
65 public static final int MAX_REDIRECTS = 3;
66
67 private final Request.MethodType methodType;
68 private String url;
69 private final Map<String, List<String>> parameters = new HashMap<String, List<String>>();
70 private final Map<String, List<String>> headers = new HashMap<String, List<String>>();
71 private final List<HttpClientAuthenticator> authenticators = new ArrayList<HttpClientAuthenticator>();
72 private final CertificateFactory certificateFactory;
73
74 private final HttpClient httpClient;
75 private final UserManager userManager;
76 private String requestBody;
77 private String requestContentType;
78 private boolean followRedirects = true;
79 private List<RequestFilePart> files;
80
81 public HttpClientRequest(final HttpClient httpClient, final MethodType methodType, final String url,
82 final CertificateFactory certificateFactory, final UserManager userManager)
83 {
84 this.httpClient = httpClient;
85 this.methodType = methodType;
86 this.url = url;
87 this.certificateFactory = certificateFactory;
88 this.userManager = userManager;
89 if (isEntityEnclosingMethod())
90 {
91 followRedirects = false;
92 }
93 }
94
95 public HttpClientRequest setUrl(final String url)
96 {
97 this.url = url;
98
99
100 configureProxy();
101 return this;
102 }
103
104
105
106 public HttpClientRequest addAuthentication(final Authenticator authenticator)
107 {
108 if (authenticator instanceof HttpClientAuthenticator)
109 {
110 this.authenticators.add((HttpClientAuthenticator) authenticator);
111 }
112 else
113 {
114 log.warn("Authenticator '" + authenticator + "'is not instance of " + HttpClientAuthenticator.class.getName());
115 }
116 return this;
117 }
118
119 public HttpClientRequest addTrustedTokenAuthentication()
120 {
121 final TrustedTokenAuthenticator trustedTokenAuthenticator = new TrustedTokenAuthenticator(
122 userManager.getRemoteUsername(), certificateFactory);
123
124 this.authenticators.add(trustedTokenAuthenticator);
125 return this;
126 }
127
128 public HttpClientRequest addTrustedTokenAuthentication(final String username)
129 {
130 final TrustedTokenAuthenticator trustedTokenAuthenticator = new TrustedTokenAuthenticator(username,
131 certificateFactory);
132
133 this.authenticators.add(trustedTokenAuthenticator);
134 return this;
135 }
136
137 public HttpClientRequest addBasicAuthentication(final String username, final String password)
138 {
139 this.authenticators.add(new BaseAuthenticator(username, password));
140 return this;
141 }
142
143 public HttpClientRequest addSeraphAuthentication(final String username, final String password)
144 {
145 this.authenticators.add(new SeraphAuthenticator(username, password));
146 return this;
147 }
148
149
150
151 public HttpClientRequest setConnectionTimeout(final int connectionTimeout)
152 {
153 final HttpConnectionManagerParams params = httpClient.getHttpConnectionManager().getParams();
154 params.setConnectionTimeout(connectionTimeout);
155 return this;
156 }
157
158 public HttpClientRequest setSoTimeout(final int soTimeout)
159 {
160 final HttpConnectionManagerParams params = httpClient.getHttpConnectionManager().getParams();
161 params.setSoTimeout(soTimeout);
162 return this;
163 }
164
165 public HttpClientRequest setRequestBody(final String requestBody)
166 {
167 if (!isEntityEnclosingMethod())
168 {
169 throw new IllegalArgumentException("Only POST and PUT methods can have request body");
170 }
171 if (files != null)
172 {
173 throw new IllegalStateException("This request contains already file parts! The request body can only be set, if the request does not contain file parts.");
174 }
175 this.requestBody = requestBody;
176 return this;
177 }
178
179 public HttpClientRequest setFiles(final List<RequestFilePart> files)
180 {
181 if (files == null)
182 {
183 throw new IllegalArgumentException("Files cannot be null");
184 }
185 if (!isEntityEnclosingMethod())
186 {
187 throw new IllegalArgumentException("Only POST and PUT methods can have a multi part body with file parts.");
188 }
189 if (requestBody != null)
190 {
191 throw new IllegalStateException("The request body is not empty! The request can only have file parts if the request is empty.");
192 }
193 this.files = files;
194 return this;
195 }
196
197 private boolean isEntityEnclosingMethod()
198 {
199 return (methodType == MethodType.POST || methodType == MethodType.PUT);
200 }
201
202 public HttpClientRequest setEntity(Object entity)
203 {
204 throw new UnsupportedOperationException("This SAL request does not support object marshaling. Use the RequestFactory component instead.");
205 }
206
207 public HttpClientRequest setRequestContentType(final String requestContentType)
208 {
209 this.requestContentType = requestContentType;
210 return this;
211 }
212
213 public HttpClientRequest addRequestParameters(final String... params)
214 {
215 if (methodType != MethodType.POST)
216 {
217 throw new UnsupportedOperationException("Only POST methods accept request parameters. For all other HTTP methods types http parameters have to be part of the URL string.");
218 }
219
220 if (params.length % 2 != 0)
221 {
222 throw new IllegalArgumentException("You must enter an even number of arguments");
223 }
224
225 for (int i = 0; i < params.length; i += 2)
226 {
227 final String name = params[i];
228 final String value = params[i + 1];
229 List<String> list = parameters.get(name);
230 if (list == null)
231 {
232 list = new ArrayList<String>();
233 parameters.put(name, list);
234 }
235 list.add(value);
236 }
237 return this;
238 }
239
240 public HttpClientRequest addHeader(final String headerName, final String headerValue)
241 {
242 List<String> list = headers.get(headerName);
243 if (list == null)
244 {
245 list = new ArrayList<String>();
246 headers.put(headerName, list);
247 }
248 list.add(headerValue);
249 return this;
250 }
251
252 public HttpClientRequest setHeader(final String headerName, final String headerValue)
253 {
254 headers.put(headerName, new ArrayList<String>(Arrays.asList(headerValue)));
255 return this;
256 }
257
258 public HttpClientRequest setFollowRedirects(boolean follow)
259 {
260 if (isEntityEnclosingMethod() && follow)
261 {
262 throw new IllegalStateException("Entity enclosing requests cannot be redirected without user intervention!");
263 }
264 this.followRedirects = follow;
265 return this;
266 }
267
268 public HttpClientRequest addHeaders(final String... params)
269 {
270 if (params.length % 2 != 0)
271 {
272 throw new IllegalArgumentException("You must enter even number of arguments");
273 }
274
275 for (int i = 0; i < params.length; i += 2)
276 {
277 final String name = params[i];
278 final String value = params[i + 1];
279 List<String> list = headers.get(name);
280 if (list == null)
281 {
282 list = new ArrayList<String>();
283 headers.put(name, list);
284 }
285 list.add(value);
286 }
287 return this;
288 }
289
290 public <E> E executeAndReturn(ReturningResponseHandler<HttpClientResponse, E> httpClientResponseResponseHandler)
291 throws ResponseException
292 {
293 final HttpMethod method = makeMethod();
294 method.setFollowRedirects(followRedirects);
295 processHeaders(method);
296 processAuthenticator(method);
297 processParameters(method);
298 if (log.isDebugEnabled())
299 {
300
301 final Header[] requestHeaders = method.getRequestHeaders();
302 String headerStr = requestHeaders == null ? "none" : Arrays.asList(requestHeaders).toString();
303 log.debug("Calling {} {} with headers: {}", new Object[]{ method.getName(), this.url, headerStr });
304 }
305 method.setRequestHeader("Connection", "close");
306 ResponseException rethrown = null;
307 try
308 {
309 executeMethod(method, 0);
310 return httpClientResponseResponseHandler.handle(new HttpClientResponse(method));
311 }
312 catch (ConnectTimeoutException cte)
313 {
314
315 rethrown = new ResponseConnectTimeoutException(cte.getMessage(), cte);
316 throw rethrown;
317 }
318 catch (SocketException se)
319 {
320 rethrown = new ResponseTransportException(se.getMessage(), se);
321 throw rethrown;
322 }
323 catch (NoHttpResponseException nhre)
324 {
325 rethrown = new ResponseReadTimeoutException(nhre.getMessage(), nhre);
326 throw rethrown;
327 }
328 catch (HttpException he)
329 {
330 rethrown = new ResponseProtocolException(he.getMessage(), he);
331 throw rethrown;
332 }
333 catch (IOException ioe)
334 {
335 rethrown = new ResponseException(ioe);
336 throw rethrown;
337 }
338 finally
339 {
340 if (rethrown != null)
341 {
342 log.debug("Call to {} {} failed with {}, message: {}", new Object[]{ method.getName(), this.url, rethrown.getClass().getSimpleName(), rethrown.getMessage() });
343 }
344 else
345 {
346 log.debug("Called {} {} with response status: {}", new Object[]{ method.getName(), this.url, method.getStatusLine() });
347 }
348
349 exhaustResponseContents(method);
350 method.releaseConnection();
351
352 final HttpConnectionManager httpConnectionManager = httpClient.getHttpConnectionManager();
353 if (httpConnectionManager != null)
354 {
355 httpConnectionManager.closeIdleConnections(0);
356 }
357 }
358 }
359
360
361
362
363 public void execute(final ResponseHandler<HttpClientResponse> responseHandler)
364 throws ResponseException
365 {
366 executeAndReturn(new ReturningResponseHandler<HttpClientResponse, Void>()
367 {
368 public Void handle(final HttpClientResponse response) throws ResponseException
369 {
370 responseHandler.handle(response);
371 return null;
372 }
373 });
374 }
375
376 private static void exhaustResponseContents(final HttpMethod response)
377 {
378 InputStream body = null;
379 try
380 {
381 body = response.getResponseBodyAsStream();
382 if (body == null)
383 {
384 return;
385 }
386 final byte[] buf = new byte[512];
387 @SuppressWarnings("unused")
388 int bytesRead = 0;
389 while ((bytesRead = body.read(buf)) != -1)
390 {
391
392
393
394 }
395 }
396 catch (final IOException e)
397 {
398
399 }
400 finally
401 {
402 shutdownStream(body);
403 }
404 }
405
406
407
408
409
410
411
412 public static void shutdownStream(final InputStream input)
413 {
414 if (null == input)
415 {
416 return;
417 }
418
419 try
420 {
421 input.close();
422 }
423 catch (final IOException ioe)
424 {
425
426 }
427 }
428
429 public String execute() throws ResponseException
430 {
431 return executeAndReturn(new ReturningResponseHandler<HttpClientResponse, String>()
432 {
433 public String handle(final HttpClientResponse response) throws ResponseException
434 {
435 if (!response.isSuccessful())
436 {
437 throw new ResponseStatusException("Unexpected response received. Status code: " + response.getStatusCode(),
438 response);
439 }
440 return response.getResponseBodyAsString();
441 }
442 });
443 }
444
445
446
447
448 protected HttpMethod makeMethod()
449 {
450 final HttpMethod method;
451 switch (methodType)
452 {
453 case POST:
454 method = new PostMethod(url);
455 break;
456 case PUT:
457 method = new PutMethod(url);
458 break;
459 case DELETE:
460 method = new DeleteMethod(url);
461 break;
462 case OPTIONS:
463 method = new OptionsMethod(url);
464 break;
465 case HEAD:
466 method = new HeadMethod(url);
467 break;
468 case TRACE:
469 method = new TraceMethod(url);
470 break;
471 default:
472 method = new GetMethod(url);
473 break;
474 }
475 return method;
476 }
477
478
479
480
481
482 protected void configureProxy()
483 {
484 new HttpClientProxyConfig().configureProxy(this.httpClient, this.url);
485 }
486
487 private void executeMethod(final HttpMethod method, int redirectCounter) throws IOException, ResponseProtocolException
488 {
489 if (++redirectCounter > MAX_REDIRECTS)
490 {
491
492 throw new ResponseProtocolException(new IOException("Maximum number of redirects (" + MAX_REDIRECTS + ") reached."));
493 }
494 else
495 {
496
497 final int statusCode = httpClient.executeMethod(method);
498
499 if (followRedirects && statusCode >= 300 && statusCode <= 399)
500 {
501 String redirectLocation;
502 final Header locationHeader = method.getResponseHeader("location");
503 if (locationHeader != null)
504 {
505 redirectLocation = locationHeader.getValue();
506 method.setURI(new URI(redirectLocation, true));
507 executeMethod(method, redirectCounter);
508 }
509 else
510 {
511
512
513
514
515 throw new ResponseProtocolException(new IOException("HTTP response returned redirect code " + statusCode + " but did not provide a location header"));
516 }
517 }
518 }
519 }
520
521 private void processHeaders(final HttpMethod method)
522 {
523 for (final String headerName : this.headers.keySet())
524 {
525 for (final String headerValue : this.headers.get(headerName))
526 {
527 method.addRequestHeader(headerName, headerValue);
528 }
529 }
530 }
531
532 private void processParameters(final HttpMethod method)
533 {
534 if (!(method instanceof EntityEnclosingMethod))
535 {
536 return;
537 }
538 EntityEnclosingMethod entityEnclosingMethod = (EntityEnclosingMethod) method;
539
540 if ((entityEnclosingMethod instanceof PostMethod) && !this.parameters.isEmpty())
541 {
542 final PostMethod postMethod = (PostMethod) method;
543 postMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
544 for (final String parameterName : this.parameters.keySet())
545 {
546 for (final String parameterValue : this.parameters.get(parameterName))
547 {
548 postMethod.addParameter(parameterName, parameterValue);
549 }
550 }
551 return;
552 }
553
554
555 if (this.requestBody != null)
556 {
557 setRequestBody(entityEnclosingMethod);
558 }
559
560 if (files != null && !files.isEmpty())
561 {
562 setFileParts(entityEnclosingMethod);
563 }
564 }
565
566 private void setFileParts(final EntityEnclosingMethod entityEnclosingMethod)
567 {
568 final List<FilePart> fileParts = new ArrayList<FilePart>();
569 for (RequestFilePart file : files)
570 {
571 try
572 {
573 FilePartSource partSource = new FilePartSource(file.getFileName(), file.getFile());
574 FilePart filePart = new FilePart(file.getParameterName(), partSource, file.getContentType(), null);
575 fileParts.add(filePart);
576 }
577 catch (IOException e)
578 {
579 throw new RuntimeException(e);
580 }
581 }
582 MultipartRequestEntity entity = new MultipartRequestEntity(fileParts.toArray(new FilePart[fileParts.size()]), new HttpMethodParams());
583 entityEnclosingMethod.setRequestEntity(entity);
584 }
585
586 private void setRequestBody(final EntityEnclosingMethod method)
587 {
588 final String contentType = requestContentType + "; charset=UTF-8";
589 ByteArrayInputStream inputStream;
590 try
591 {
592 inputStream = new ByteArrayInputStream(requestBody.getBytes("UTF-8"));
593 }
594 catch (final UnsupportedEncodingException e)
595 {
596 throw new RuntimeException(e);
597 }
598 method.setRequestEntity(new InputStreamRequestEntity(inputStream, contentType));
599 }
600
601 private void processAuthenticator(final HttpMethod method)
602 {
603 for (final HttpClientAuthenticator authenticator : authenticators)
604 {
605 authenticator.process(httpClient, method);
606 }
607 }
608
609 public Map<String, List<String>> getHeaders()
610 {
611 return Collections.unmodifiableMap(headers);
612 }
613
614 public MethodType getMethodType()
615 {
616 return methodType;
617 }
618
619 @Override
620 public String toString()
621 {
622 return methodType + " " + url + ", Parameters: " + parameters +
623 (StringUtils.isBlank(requestBody) ? "" : "\nRequest body:\n" + requestBody);
624 }
625
626
627 }