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.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   * HttpClient implementation of Request interface
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          // Reconfigure the proxy setting for the new URL
99          // as it may or may not need to go through the system proxy
100         configureProxy();
101         return this;
102     }
103 
104     // ------------------------ authenticators -------------------------------------------
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     // ------------------------ various setters -------------------------------------------
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             // see https://extranet.atlassian.com/display/~doflynn/2008/05/19/HttpClient+leaks+sockets+into+CLOSE_WAIT
334             final HttpConnectionManager httpConnectionManager = httpClient.getHttpConnectionManager();
335             if (httpConnectionManager != null)
336             {
337                 httpConnectionManager.closeIdleConnections(0);
338             }
339         }
340     }
341 
342     /* (non-Javadoc)
343      * @see com.atlassian.sal.api.net.Request#execute()
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                 // Read everything the server has to say before closing
374                 // the stream, or the server would get a unexpected
375                 // "connection closed" error.
376             }
377         }
378         catch (final IOException e)
379         {
380             // Ignore, we're already done with the response anyway.
381         }
382         finally
383         {
384             shutdownStream(body);
385         }
386     }
387 
388     /**
389      * Unconditionally close an <code>InputStream</code>.
390      * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored.
391      *
392      * @param input A (possibly null) InputStream
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             // Do nothing
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     // -------------------------------------------------- private methods ------------------------------------------------------------------
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      * Configures the proxy for the underlying HttpClient.
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             // Putting the error message inside a wrapped IOException for backward compatibility
474             throw new ResponseProtocolException(new IOException("Maximum number of redirects (" + MAX_REDIRECTS + ") reached."));
475         }
476         else
477         {
478             // execute the method.
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                     // The response is invalid and did not provide the new location for
494                     // the resource.  Report an error or possibly handle the response
495                     // like a 404 Not Found error.
496                     // (Putting the error message inside a wrapped IOException for backward compatibility)
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;    // only POST and PUT method can apply
519         }
520         EntityEnclosingMethod entityEnclosingMethod = (EntityEnclosingMethod) method;
521         // Add post parameters
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         // Set request body
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 }