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
21 import org.apache.commons.httpclient.ConnectTimeoutException;
22 import org.apache.commons.httpclient.Header;
23 import org.apache.commons.httpclient.HttpClient;
24 import org.apache.commons.httpclient.HttpConnectionManager;
25 import org.apache.commons.httpclient.HttpException;
26 import org.apache.commons.httpclient.HttpMethod;
27 import org.apache.commons.httpclient.NoHttpResponseException;
28 import org.apache.commons.httpclient.URI;
29 import org.apache.commons.httpclient.methods.DeleteMethod;
30 import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
31 import org.apache.commons.httpclient.methods.GetMethod;
32 import org.apache.commons.httpclient.methods.HeadMethod;
33 import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
34 import org.apache.commons.httpclient.methods.OptionsMethod;
35 import org.apache.commons.httpclient.methods.PostMethod;
36 import org.apache.commons.httpclient.methods.PutMethod;
37 import org.apache.commons.httpclient.methods.TraceMethod;
38 import org.apache.commons.httpclient.methods.multipart.FilePart;
39 import org.apache.commons.httpclient.methods.multipart.FilePartSource;
40 import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
41 import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
42 import org.apache.commons.httpclient.params.HttpMethodParams;
43 import org.apache.commons.lang.StringUtils;
44 import org.apache.log4j.Logger;
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 = Logger.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 final Header[] requestHeaders = method.getRequestHeaders();
301 log.debug("Calling " + method.getName() + " " + this.url + " with headers " + (requestHeaders == null ? "none" : Arrays.asList(requestHeaders).toString()));
302 }
303 method.setRequestHeader("Connection", "close");
304 try
305 {
306 executeMethod(method, 0);
307 return httpClientResponseResponseHandler.handle(new HttpClientResponse(method));
308 }
309 catch (ConnectTimeoutException cte)
310 {
311 throw new ResponseConnectTimeoutException(cte.getMessage(), cte);
312 }
313 catch (SocketException se)
314 {
315 throw new ResponseTransportException(se.getMessage(), se);
316 }
317 catch (NoHttpResponseException nhre)
318 {
319 throw new ResponseReadTimeoutException(nhre.getMessage(), nhre);
320 }
321 catch (HttpException he)
322 {
323 throw new ResponseProtocolException(he.getMessage(), he);
324 }
325 catch (IOException ioe)
326 {
327 throw new ResponseException(ioe);
328 }
329 finally
330 {
331 exhaustResponseContents(method);
332 method.releaseConnection();
333
334 final HttpConnectionManager httpConnectionManager = httpClient.getHttpConnectionManager();
335 if (httpConnectionManager != null)
336 {
337 httpConnectionManager.closeIdleConnections(0);
338 }
339 }
340 }
341
342
343
344
345 public void execute(final ResponseHandler<HttpClientResponse> responseHandler)
346 throws ResponseException
347 {
348 executeAndReturn(new ReturningResponseHandler<HttpClientResponse, Void>()
349 {
350 public Void handle(final HttpClientResponse response) throws ResponseException
351 {
352 responseHandler.handle(response);
353 return null;
354 }
355 });
356 }
357
358 private static void exhaustResponseContents(final HttpMethod response)
359 {
360 InputStream body = null;
361 try
362 {
363 body = response.getResponseBodyAsStream();
364 if (body == null)
365 {
366 return;
367 }
368 final byte[] buf = new byte[512];
369 @SuppressWarnings("unused")
370 int bytesRead = 0;
371 while ((bytesRead = body.read(buf)) != -1)
372 {
373
374
375
376 }
377 }
378 catch (final IOException e)
379 {
380
381 }
382 finally
383 {
384 shutdownStream(body);
385 }
386 }
387
388
389
390
391
392
393
394 public static void shutdownStream(final InputStream input)
395 {
396 if (null == input)
397 {
398 return;
399 }
400
401 try
402 {
403 input.close();
404 }
405 catch (final IOException ioe)
406 {
407
408 }
409 }
410
411 public String execute() throws ResponseException
412 {
413 return executeAndReturn(new ReturningResponseHandler<HttpClientResponse, String>()
414 {
415 public String handle(final HttpClientResponse response) throws ResponseException
416 {
417 if (!response.isSuccessful())
418 {
419 throw new ResponseStatusException("Unexpected response received. Status code: " + response.getStatusCode(),
420 response);
421 }
422 return response.getResponseBodyAsString();
423 }
424 });
425 }
426
427
428
429
430 protected HttpMethod makeMethod()
431 {
432 final HttpMethod method;
433 switch (methodType)
434 {
435 case POST:
436 method = new PostMethod(url);
437 break;
438 case PUT:
439 method = new PutMethod(url);
440 break;
441 case DELETE:
442 method = new DeleteMethod(url);
443 break;
444 case OPTIONS:
445 method = new OptionsMethod(url);
446 break;
447 case HEAD:
448 method = new HeadMethod(url);
449 break;
450 case TRACE:
451 method = new TraceMethod(url);
452 break;
453 default:
454 method = new GetMethod(url);
455 break;
456 }
457 return method;
458 }
459
460
461
462
463
464 protected void configureProxy()
465 {
466 new HttpClientProxyConfig().configureProxy(this.httpClient, this.url);
467 }
468
469 private void executeMethod(final HttpMethod method, int redirectCounter) throws IOException, ResponseProtocolException
470 {
471 if (++redirectCounter > MAX_REDIRECTS)
472 {
473
474 throw new ResponseProtocolException(new IOException("Maximum number of redirects (" + MAX_REDIRECTS + ") reached."));
475 }
476 else
477 {
478
479 final int statusCode = httpClient.executeMethod(method);
480
481 if (followRedirects && statusCode >= 300 && statusCode <= 399)
482 {
483 String redirectLocation;
484 final Header locationHeader = method.getResponseHeader("location");
485 if (locationHeader != null)
486 {
487 redirectLocation = locationHeader.getValue();
488 method.setURI(new URI(redirectLocation, true));
489 executeMethod(method, redirectCounter);
490 }
491 else
492 {
493
494
495
496
497 throw new ResponseProtocolException(new IOException("HTTP response returned redirect code " + statusCode + " but did not provide a location header"));
498 }
499 }
500 }
501 }
502
503 private void processHeaders(final HttpMethod method)
504 {
505 for (final String headerName : this.headers.keySet())
506 {
507 for (final String headerValue : this.headers.get(headerName))
508 {
509 method.addRequestHeader(headerName, headerValue);
510 }
511 }
512 }
513
514 private void processParameters(final HttpMethod method)
515 {
516 if (!(method instanceof EntityEnclosingMethod))
517 {
518 return;
519 }
520 EntityEnclosingMethod entityEnclosingMethod = (EntityEnclosingMethod) method;
521
522 if ((entityEnclosingMethod instanceof PostMethod) && !this.parameters.isEmpty())
523 {
524 final PostMethod postMethod = (PostMethod) method;
525 postMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
526 for (final String parameterName : this.parameters.keySet())
527 {
528 for (final String parameterValue : this.parameters.get(parameterName))
529 {
530 postMethod.addParameter(parameterName, parameterValue);
531 }
532 }
533 return;
534 }
535
536
537 if (this.requestBody != null)
538 {
539 setRequestBody(entityEnclosingMethod);
540 }
541
542 if (files != null && !files.isEmpty())
543 {
544 setFileParts(entityEnclosingMethod);
545 }
546 }
547
548 private void setFileParts(final EntityEnclosingMethod entityEnclosingMethod)
549 {
550 final List<FilePart> fileParts = new ArrayList<FilePart>();
551 for (RequestFilePart file : files)
552 {
553 try
554 {
555 FilePartSource partSource = new FilePartSource(file.getFileName(), file.getFile());
556 FilePart filePart = new FilePart(file.getParameterName(), partSource, file.getContentType(), null);
557 fileParts.add(filePart);
558 }
559 catch (IOException e)
560 {
561 throw new RuntimeException(e);
562 }
563 }
564 MultipartRequestEntity entity = new MultipartRequestEntity(fileParts.toArray(new FilePart[fileParts.size()]), new HttpMethodParams());
565 entityEnclosingMethod.setRequestEntity(entity);
566 }
567
568 private void setRequestBody(final EntityEnclosingMethod method)
569 {
570 final String contentType = requestContentType + "; charset=UTF-8";
571 ByteArrayInputStream inputStream;
572 try
573 {
574 inputStream = new ByteArrayInputStream(requestBody.getBytes("UTF-8"));
575 }
576 catch (final UnsupportedEncodingException e)
577 {
578 throw new RuntimeException(e);
579 }
580 method.setRequestEntity(new InputStreamRequestEntity(inputStream, contentType));
581 }
582
583 private void processAuthenticator(final HttpMethod method)
584 {
585 for (final HttpClientAuthenticator authenticator : authenticators)
586 {
587 authenticator.process(httpClient, method);
588 }
589 }
590
591 public Map<String, List<String>> getHeaders()
592 {
593 return Collections.unmodifiableMap(headers);
594 }
595
596 public MethodType getMethodType()
597 {
598 return methodType;
599 }
600
601 @Override
602 public String toString()
603 {
604 return methodType + " " + url + ", Parameters: " + parameters +
605 (StringUtils.isBlank(requestBody) ? "" : "\nRequest body:\n" + requestBody);
606 }
607
608
609 }