View Javadoc

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   * HttpClient implementation of Request interface
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      // ------------------------ authenticators -------------------------------------------
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     // ------------------------ various setters -------------------------------------------
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             // see https://extranet.atlassian.com/display/~doflynn/2008/05/19/HttpClient+leaks+sockets+into+CLOSE_WAIT
263             final HttpConnectionManager httpConnectionManager = httpClient.getHttpConnectionManager();
264             if (httpConnectionManager != null)
265             {
266                 httpConnectionManager.closeIdleConnections(0);
267             }
268         }
269     }
270 
271     /* (non-Javadoc)
272      * @see com.atlassian.sal.api.net.Request#execute()
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                 // Read everything the server has to say before closing
303                 // the stream, or the server would get a unexpected
304                 // "connection closed" error.
305             }
306         }
307         catch (final IOException e)
308         {
309             // Ignore, we're already done with the response anyway.
310         }
311         finally
312         {
313             shutdownStream(body);
314         }
315     }
316 
317     /**
318      * Unconditionally close an <code>InputStream</code>.
319      * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored.
320      *
321      * @param input A (possibly null) InputStream
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             // Do nothing
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     // -------------------------------------------------- private methods ------------------------------------------------------------------
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             // execute the method.
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                     // The response is invalid and did not provide the new location for
412                     // the resource.  Report an error or possibly handle the response
413                     // like a 404 Not Found error.
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;    // only POST and PUT method can apply
436         }
437         // Add post parameters
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         // Set request body
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 }