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  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   * HttpClient implementation of Request interface
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          // 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             // Still use an isDebugEnabled here so we don't waste time getting the headers if logging off.
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             // We keep a copy of the exception solely to log it in a common 'finally' line.
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             // see https://extranet.atlassian.com/display/~doflynn/2008/05/19/HttpClient+leaks+sockets+into+CLOSE_WAIT
352             final HttpConnectionManager httpConnectionManager = httpClient.getHttpConnectionManager();
353             if (httpConnectionManager != null)
354             {
355                 httpConnectionManager.closeIdleConnections(0);
356             }
357         }
358     }
359 
360     /* (non-Javadoc)
361      * @see com.atlassian.sal.api.net.Request#execute()
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                 // Read everything the server has to say before closing
392                 // the stream, or the server would get a unexpected
393                 // "connection closed" error.
394             }
395         }
396         catch (final IOException e)
397         {
398             // Ignore, we're already done with the response anyway.
399         }
400         finally
401         {
402             shutdownStream(body);
403         }
404     }
405 
406     /**
407      * Unconditionally close an <code>InputStream</code>.
408      * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored.
409      *
410      * @param input A (possibly null) InputStream
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             // Do nothing
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     // -------------------------------------------------- private methods ------------------------------------------------------------------
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      * Configures the proxy for the underlying HttpClient.
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             // Putting the error message inside a wrapped IOException for backward compatibility
492             throw new ResponseProtocolException(new IOException("Maximum number of redirects (" + MAX_REDIRECTS + ") reached."));
493         }
494         else
495         {
496             // execute the method.
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                     // The response is invalid and did not provide the new location for
512                     // the resource.  Report an error or possibly handle the response
513                     // like a 404 Not Found error.
514                     // (Putting the error message inside a wrapped IOException for backward compatibility)
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;    // only POST and PUT method can apply
537         }
538         EntityEnclosingMethod entityEnclosingMethod = (EntityEnclosingMethod) method;
539         // Add post parameters
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         // Set request body
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 }