1 package com.atlassian.sal.core.net;
2
3 import com.atlassian.sal.api.net.Request;
4 import com.atlassian.sal.api.net.ResponseException;
5 import com.atlassian.sal.api.net.ResponseHandler;
6 import com.atlassian.sal.api.net.ReturningResponseHandler;
7 import com.atlassian.sal.api.net.auth.Authenticator;
8 import com.atlassian.sal.api.user.UserManager;
9 import com.atlassian.sal.core.net.auth.BaseAuthenticator;
10 import com.atlassian.sal.core.net.auth.HttpClientAuthenticator;
11 import com.atlassian.sal.core.net.auth.SeraphAuthenticator;
12 import com.atlassian.sal.core.net.auth.TrustedTokenAuthenticator;
13 import com.atlassian.sal.core.trusted.CertificateFactory;
14 import org.apache.commons.httpclient.Header;
15 import org.apache.commons.httpclient.HttpClient;
16 import org.apache.commons.httpclient.HttpConnectionManager;
17 import org.apache.commons.httpclient.HttpMethod;
18 import org.apache.commons.httpclient.URI;
19 import org.apache.commons.httpclient.methods.DeleteMethod;
20 import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
21 import org.apache.commons.httpclient.methods.GetMethod;
22 import org.apache.commons.httpclient.methods.HeadMethod;
23 import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
24 import org.apache.commons.httpclient.methods.OptionsMethod;
25 import org.apache.commons.httpclient.methods.PostMethod;
26 import org.apache.commons.httpclient.methods.PutMethod;
27 import org.apache.commons.httpclient.methods.TraceMethod;
28 import org.apache.commons.httpclient.params.HttpConnectionManagerParams;
29 import org.apache.commons.lang.StringUtils;
30 import org.apache.log4j.Logger;
31
32 import java.io.ByteArrayInputStream;
33 import java.io.IOException;
34 import java.io.InputStream;
35 import java.io.UnsupportedEncodingException;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collections;
39 import java.util.HashMap;
40 import java.util.List;
41 import java.util.Map;
42
43
44
45
46 public class HttpClientRequest implements Request<HttpClientRequest, HttpClientResponse>
47 {
48 private static final Logger log = Logger.getLogger(HttpClientRequest.class);
49
50 public static final int MAX_REDIRECTS = 3;
51
52 private final Request.MethodType methodType;
53 private String url;
54 private final Map<String, List<String>> parameters = new HashMap<String, List<String>>();
55 private final Map<String, List<String>> headers = new HashMap<String, List<String>>();
56 private final List<HttpClientAuthenticator> authenticators = new ArrayList<HttpClientAuthenticator>();
57 private final CertificateFactory certificateFactory;
58
59 private final HttpClient httpClient;
60 private final UserManager userManager;
61 private String requestBody;
62 private String requestContentType;
63 private boolean followRedirects = true;
64
65 public HttpClientRequest(final HttpClient httpClient, final MethodType methodType, final String url,
66 final CertificateFactory certificateFactory, final UserManager userManager)
67 {
68 this.httpClient = httpClient;
69 this.methodType = methodType;
70 this.url = url;
71 this.certificateFactory = certificateFactory;
72 this.userManager = userManager;
73 }
74
75 public HttpClientRequest setUrl(final String url)
76 {
77 this.url = url;
78 return this;
79 }
80
81
82
83 public HttpClientRequest addAuthentication(final Authenticator authenticator)
84 {
85 if (authenticator instanceof HttpClientAuthenticator)
86 {
87 this.authenticators.add((HttpClientAuthenticator) authenticator);
88 }
89 else
90 {
91 log.warn("Authenticator '" + authenticator + "'is not instance of " + HttpClientAuthenticator.class.getName());
92 }
93 return this;
94 }
95
96 public HttpClientRequest addTrustedTokenAuthentication()
97 {
98 final TrustedTokenAuthenticator trustedTokenAuthenticator = new TrustedTokenAuthenticator(
99 userManager.getRemoteUsername(), certificateFactory);
100
101 this.authenticators.add(trustedTokenAuthenticator);
102 return this;
103 }
104
105 public HttpClientRequest addTrustedTokenAuthentication(final String username)
106 {
107 final TrustedTokenAuthenticator trustedTokenAuthenticator = new TrustedTokenAuthenticator(username,
108 certificateFactory);
109
110 this.authenticators.add(trustedTokenAuthenticator);
111 return this;
112 }
113
114 public HttpClientRequest addBasicAuthentication(final String username, final String password)
115 {
116 this.authenticators.add(new BaseAuthenticator(username, password));
117 return this;
118 }
119
120 public HttpClientRequest addSeraphAuthentication(final String username, final String password)
121 {
122 this.authenticators.add(new SeraphAuthenticator(username, password));
123 return this;
124 }
125
126
127
128 public HttpClientRequest setConnectionTimeout(final int connectionTimeout)
129 {
130 final HttpConnectionManagerParams params = httpClient.getHttpConnectionManager().getParams();
131 params.setConnectionTimeout(connectionTimeout);
132 return this;
133 }
134
135 public HttpClientRequest setSoTimeout(final int soTimeout)
136 {
137 final HttpConnectionManagerParams params = httpClient.getHttpConnectionManager().getParams();
138 params.setSoTimeout(soTimeout);
139 return this;
140 }
141
142 public HttpClientRequest setRequestBody(final String requestBody)
143 {
144 this.requestBody = requestBody;
145 if (methodType != MethodType.POST && methodType != MethodType.PUT)
146 {
147 throw new IllegalArgumentException("Only POST and PUT methods can have request body");
148 }
149 return this;
150 }
151
152 public HttpClientRequest setEntity(Object entity)
153 {
154 throw new UnsupportedOperationException("This SAL request does not support object marshaling. Use the RequestFactory component instead.");
155 }
156
157 public HttpClientRequest setRequestContentType(final String requestContentType)
158 {
159 this.requestContentType = requestContentType;
160 return this;
161 }
162
163 public HttpClientRequest addRequestParameters(final String... params)
164 {
165 if (methodType != MethodType.POST)
166 {
167 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.");
168 }
169
170 if (params.length % 2 != 0)
171 {
172 throw new IllegalArgumentException("You must enter an even number of arguments");
173 }
174
175 for (int i = 0; i < params.length; i += 2)
176 {
177 final String name = params[i];
178 final String value = params[i + 1];
179 List<String> list = parameters.get(name);
180 if (list == null)
181 {
182 list = new ArrayList<String>();
183 parameters.put(name, list);
184 }
185 list.add(value);
186 }
187 return this;
188 }
189
190 public HttpClientRequest addHeader(final String headerName, final String headerValue)
191 {
192 List<String> list = headers.get(headerName);
193 if (list == null)
194 {
195 list = new ArrayList<String>();
196 headers.put(headerName, list);
197 }
198 list.add(headerValue);
199 return this;
200 }
201
202 public HttpClientRequest setHeader(final String headerName, final String headerValue)
203 {
204 headers.put(headerName, new ArrayList<String>(Arrays.asList(headerValue)));
205 return this;
206 }
207
208 public HttpClientRequest setFollowRedirects(boolean follow)
209 {
210 this.followRedirects = follow;
211 return this;
212 }
213
214 public HttpClientRequest addHeaders(final String... params)
215 {
216 if (params.length % 2 != 0)
217 {
218 throw new IllegalArgumentException("You must enter even number of arguments");
219 }
220
221 for (int i = 0; i < params.length; i += 2)
222 {
223 final String name = params[i];
224 final String value = params[i + 1];
225 List<String> list = headers.get(name);
226 if (list == null)
227 {
228 list = new ArrayList<String>();
229 headers.put(name, list);
230 }
231 list.add(value);
232 }
233 return this;
234 }
235
236 public <E> E executeAndReturn(ReturningResponseHandler<HttpClientResponse, E> httpClientResponseResponseHandler)
237 throws ResponseException
238 {
239 final HttpMethod method = makeMethod();
240 processHeaders(method);
241 processAuthenticator(method);
242 processParameters(method);
243 if (log.isDebugEnabled())
244 {
245 final Header[] requestHeaders = method.getRequestHeaders();
246 log.debug("Calling " + method.getName() + " " + this.url + " with headers " + (requestHeaders == null ? "none" : Arrays.asList(requestHeaders).toString()));
247 }
248 method.setRequestHeader("Connection", "close");
249 try
250 {
251 executeMethod(method, 0);
252 return httpClientResponseResponseHandler.handle(new HttpClientResponse(method));
253 }
254 catch (IOException ioe)
255 {
256 throw new ResponseException(ioe);
257 }
258 finally
259 {
260 exhaustResponseContents(method);
261 method.releaseConnection();
262
263 final HttpConnectionManager httpConnectionManager = httpClient.getHttpConnectionManager();
264 if (httpConnectionManager != null)
265 {
266 httpConnectionManager.closeIdleConnections(0);
267 }
268 }
269 }
270
271
272
273
274 public void execute(final ResponseHandler<HttpClientResponse> responseHandler)
275 throws ResponseException
276 {
277 executeAndReturn(new ReturningResponseHandler<HttpClientResponse, Void>()
278 {
279 public Void handle(final HttpClientResponse response) throws ResponseException
280 {
281 responseHandler.handle(response);
282 return null;
283 }
284 });
285 }
286
287 private static void exhaustResponseContents(final HttpMethod response)
288 {
289 InputStream body = null;
290 try
291 {
292 body = response.getResponseBodyAsStream();
293 if (body == null)
294 {
295 return;
296 }
297 final byte[] buf = new byte[512];
298 @SuppressWarnings("unused")
299 int bytesRead = 0;
300 while ((bytesRead = body.read(buf)) != -1)
301 {
302
303
304
305 }
306 }
307 catch (final IOException e)
308 {
309
310 }
311 finally
312 {
313 shutdownStream(body);
314 }
315 }
316
317
318
319
320
321
322
323 public static void shutdownStream(final InputStream input)
324 {
325 if (null == input)
326 {
327 return;
328 }
329
330 try
331 {
332 input.close();
333 }
334 catch (final IOException ioe)
335 {
336
337 }
338 }
339
340 public String execute() throws ResponseException
341 {
342 return executeAndReturn(new ReturningResponseHandler<HttpClientResponse, String>()
343 {
344 public String handle(final HttpClientResponse response) throws ResponseException
345 {
346 if (!response.isSuccessful())
347 {
348 throw new ResponseException("Unexpected response received. Status code: " + response.getStatusCode());
349 }
350 return response.getResponseBodyAsString();
351 }
352 });
353 }
354
355
356
357
358 protected HttpMethod makeMethod()
359 {
360 final HttpMethod method;
361 switch (methodType)
362 {
363 case POST:
364 method = new PostMethod(url);
365 break;
366 case PUT:
367 method = new PutMethod(url);
368 break;
369 case DELETE:
370 method = new DeleteMethod(url);
371 break;
372 case OPTIONS:
373 method = new OptionsMethod(url);
374 break;
375 case HEAD:
376 method = new HeadMethod(url);
377 break;
378 case TRACE:
379 method = new TraceMethod(url);
380 break;
381 default:
382 method = new GetMethod(url);
383 break;
384 }
385 return method;
386 }
387
388 private void executeMethod(final HttpMethod method, int redirectCounter) throws IOException
389 {
390 if (++redirectCounter > MAX_REDIRECTS)
391 {
392 throw new IOException("Maximum number of redirects (" + MAX_REDIRECTS + ") reached.");
393 }
394 else
395 {
396
397 final int statusCode = httpClient.executeMethod(method);
398
399 if (followRedirects && statusCode >= 300 && statusCode <= 399)
400 {
401 String redirectLocation;
402 final Header locationHeader = method.getResponseHeader("location");
403 if (locationHeader != null)
404 {
405 redirectLocation = locationHeader.getValue();
406 method.setURI(new URI(redirectLocation, true));
407 executeMethod(method, redirectCounter);
408 }
409 else
410 {
411
412
413
414 throw new IOException("HTTP response returned redirect code " + statusCode + " but did not provide a location header");
415 }
416 }
417 }
418 }
419
420 private void processHeaders(final HttpMethod method)
421 {
422 for (final String headerName : this.headers.keySet())
423 {
424 for (final String headerValue : this.headers.get(headerName))
425 {
426 method.addRequestHeader(headerName, headerValue);
427 }
428 }
429 }
430
431 private void processParameters(final HttpMethod method)
432 {
433 if (!(method instanceof EntityEnclosingMethod))
434 {
435 return;
436 }
437
438 if ((method instanceof PostMethod) && !this.parameters.isEmpty())
439 {
440 final PostMethod postMethod = (PostMethod) method;
441 postMethod.setRequestHeader("Content-type", "application/x-www-form-urlencoded; charset=UTF-8");
442 for (final String parameterName : this.parameters.keySet())
443 {
444 for (final String parameterValue : this.parameters.get(parameterName))
445 {
446 postMethod.addParameter(parameterName, parameterValue);
447 }
448 }
449 return;
450 }
451
452
453 if (this.requestBody != null)
454 {
455 final EntityEnclosingMethod entityEnclosingMethod = (EntityEnclosingMethod) method;
456 final String contentType = requestContentType + "; charset=UTF-8";
457 ByteArrayInputStream inputStream;
458 try
459 {
460 inputStream = new ByteArrayInputStream(requestBody.getBytes("UTF-8"));
461 }
462 catch (final UnsupportedEncodingException e)
463 {
464 throw new RuntimeException(e);
465 }
466 entityEnclosingMethod.setRequestEntity(new InputStreamRequestEntity(inputStream, contentType));
467
468 }
469 }
470
471 private void processAuthenticator(final HttpMethod method)
472 {
473 for (final HttpClientAuthenticator authenticator : authenticators)
474 {
475 authenticator.process(httpClient, method);
476 }
477 }
478
479 public Map<String, List<String>> getHeaders()
480 {
481 return Collections.unmodifiableMap(headers);
482 }
483
484 public MethodType getMethodType()
485 {
486 return methodType;
487 }
488
489 @Override
490 public String toString()
491 {
492 return methodType + " " + url + ", Parameters: " + parameters +
493 (StringUtils.isBlank(requestBody) ? "" : "\nRequest body:\n" + requestBody);
494 }
495
496
497 }