View Javadoc

1   /**
2    * Copyright (C) 2005-2009 Alfresco Software Limited.
3    *
4    * This file is part of the Spring Surf Extension project.
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  
19  package org.springframework.extensions.webscripts.connector;
20  
21  import java.io.ByteArrayInputStream;
22  import java.io.ByteArrayOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.io.UnsupportedEncodingException;
27  import java.net.ConnectException;
28  import java.net.MalformedURLException;
29  import java.net.SocketTimeoutException;
30  import java.net.URL;
31  import java.net.UnknownHostException;
32  import java.util.Enumeration;
33  import java.util.Map;
34  
35  import javax.servlet.http.HttpServletRequest;
36  import javax.servlet.http.HttpServletResponse;
37  
38  import org.apache.commons.httpclient.ConnectTimeoutException;
39  import org.apache.commons.httpclient.Header;
40  import org.apache.commons.httpclient.HttpClient;
41  import org.apache.commons.httpclient.methods.DeleteMethod;
42  import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
43  import org.apache.commons.httpclient.methods.GetMethod;
44  import org.apache.commons.httpclient.methods.HeadMethod;
45  import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
46  import org.apache.commons.httpclient.methods.PostMethod;
47  import org.apache.commons.httpclient.methods.PutMethod;
48  import org.apache.commons.httpclient.params.HttpClientParams;
49  import org.apache.commons.logging.Log;
50  import org.apache.commons.logging.LogFactory;
51  import org.springframework.extensions.surf.exception.WebScriptsPlatformException;
52  import org.springframework.extensions.surf.util.Base64;
53  
54  /**
55   * Remote client for for accessing data from remote URLs.
56   * <p>
57   * Can be used as a Script root object for simple HTTP requests.
58   * <p>
59   * Generally remote URLs will be "data" webscripts (i.e. returning XML/JSON) called from
60   * web-tier script objects and will be housed within an Alfresco Repository server.
61   * <p>
62   * Supports HTTP methods of GET, DELETE, PUT and POST of body content data.
63   * <p>
64   * A 'Response' is returned containing the response data stream as a String and the Status
65   * object representing the status code and error information if any. Methods supplying an
66   * InputStream will force a POST and methods supplying an OutputStream will stream the result
67   * directly to it and not generate a response in the 'Response' object. 
68   * 
69   * @author Kevin Roast
70   */
71  public class RemoteClient extends AbstractClient
72  {
73      private static Log logger = LogFactory.getLog(RemoteClient.class);
74      
75      private static final String CHARSETEQUALS = "charset=";
76      private static final int BUFFERSIZE = 4096;
77      
78      private static final int CONNECT_TIMEOUT = 5000;  // 5 seconds
79      private static final int READ_TIMEOUT = 90000;    // 90 seconds 
80      
81      private Map<String, String> cookies;
82      private String defaultEncoding;
83      private String ticket;
84      private String ticketName = "alf_ticket";
85      private String requestContentType = "application/octet-stream";
86      private HttpMethod requestMethod = HttpMethod.GET;
87      
88      private String username;
89      private String password;
90      
91      private Map<String, String> requestProperties;
92      
93      // special http error codes used internally to detect specific error
94      public static final int SC_REMOTE_CONN_TIMEOUT = 499;
95      public static final int SC_REMOTE_CONN_NOHOST  = 498;
96  
97      // Redirect status codes
98      public static final int SC_MOVED_TEMPORARILY = 302;
99      public static final int SC_MOVED_PERMANENTLY = 301;
100     public static final int SC_SEE_OTHER = 303;
101     public static final int SC_TEMPORARY_REDIRECT = 307;
102 
103     private static final int MAX_REDIRECTS = 10;
104 
105 
106     /**
107      * Construction
108      * 
109      * @param endpoint         HTTP API endpoint of remote Alfresco server webapp
110      *                         For example http://servername:8080/alfresco
111      */
112     public RemoteClient(String endpoint)
113     {
114         this(endpoint, null);
115     }
116 
117     /**
118      * Construction
119      * 
120      * @param endpoint         HTTP API endpoint of remote Alfresco server webapp
121      *                         For example http://servername:8080/alfresco
122      * @param defaultEncoding  Encoding to use when converting responses that do not specify one
123      */
124     public RemoteClient(String endpoint, String defaultEncoding)
125     {
126         super(endpoint);
127         this.defaultEncoding = defaultEncoding;
128     }
129 
130     /**
131      * Sets the authentication ticket to use. Will be used for all future call() requests.
132      * 
133      * @param ticket
134      */
135     public void setTicket(String ticket)
136     {
137         this.ticket = ticket;
138     }
139     
140     /**
141      * Returns the authentication ticket
142      * 
143      * @return
144      */
145     public String getTicket()
146     {
147         return this.ticket;
148     }
149 
150     /**
151      * Sets the authentication ticket name to use.  Will be used for all future call() requests.
152      * 
153      * This allows the ticket mechanism to be repurposed for non-Alfresco
154      * implementations that may require similar argument passing
155      * 
156      * @param ticket
157      */
158     public void setTicketName(String ticketName)
159     {
160         this.ticketName = ticketName;
161     }
162     
163     /**
164      * @return the authentication ticket name to use
165      */
166     public String getTicketName()
167     {
168         return this.ticketName;
169     }
170     
171     /**
172      * Basic HTTP auth. Will be used for all future call() requests.
173      * 
174      * @param user
175      * @param pass
176      */
177     public void setUsernamePassword(String user, String pass)
178     {
179         this.username = user;
180         this.password = pass;
181     }
182 
183     /**
184      * @param requestContentType     the POST request "Content-Type" header value to set
185      *        NOTE: this value is reset to the default of GET after a call() is made. 
186      */
187     public void setRequestContentType(String contentType)
188     {
189         if (requestContentType != null && requestContentType.length() != 0)
190         {
191             this.requestContentType = contentType;
192         }
193     }
194 
195     /**
196      * @param requestMethod  the request Method to set i.e. one of GET/POST/PUT/DELETE etc.
197      *        if not set, GET will be assumed unless an InputStream is supplied during call()
198      *        in which case POST will be used unless the request method overrides it with PUT.
199      *        NOTE: this value is reset to the default of GET after a call() is made. 
200      */
201     public void setRequestMethod(HttpMethod method)
202     {
203         if (method != null)
204         {
205             this.requestMethod = method;
206         }
207     }
208     
209     /**
210      * Allows for additional request properties to be set onto this object
211      * These request properties are applied to the connection when
212      * the connection is called. Will be used for all future call() requests.
213      * 
214      * @param requestProperties
215      */
216     public void setRequestProperties(Map<String, String> requestProperties)
217     {
218         this.requestProperties = requestProperties;
219     }
220     
221     /**
222      * Provides a set of cookies for state transfer. This set of cookies is maintained through any redirects followed by
223      * the client (e.g. redirect through SSO host).
224      * 
225      * @param cookies the cookies
226      */
227     public void setCookies(Map<String, String> cookies)
228     {
229         this.cookies = cookies;
230     }
231     
232     /**
233      * Gets the current set of cookies for state transfer. This set of cookies is maintained through any redirects
234      * followed by the client (e.g. redirect through SSO host).
235      * 
236      * @return the cookies
237      */
238     public Map<String, String> getCookies()
239     {
240         return this.cookies;
241     }
242 
243     /**
244      * Call a remote WebScript uri. The endpoint as supplied in the constructor will be used
245      * as the prefix for the full WebScript url.
246      * 
247      * This API is generally called from a script host.
248      * 
249      * @param uri     WebScript URI - for example /test/myscript?arg=value
250      * 
251      * @return Response object from the call {@link Response}
252      */
253     public Response call(String uri)
254     {
255         return call(uri, true, null);
256     }
257     
258     /**
259      * Call a remote WebScript uri, passing the supplied body as a POST request (unless the
260      * request method is set to override as say PUT).
261      * 
262      * @param uri    Uri to call on the endpoint
263      * @param body   Body of the POST request.
264      * 
265      * @return Response object from the call {@link Response}
266      */
267     public Response call(String uri, String body)
268     {
269         try
270         {
271             byte[] bytes = body.getBytes("UTF-8");
272             return call(uri, true, new ByteArrayInputStream(bytes));
273         }
274         catch (UnsupportedEncodingException e)
275         {
276             throw new WebScriptsPlatformException("Encoding not supported.", e);
277         }
278     }
279 
280     /**
281      * Call a remote WebScript uri. The endpoint as supplied in the constructor will be used
282      * as the prefix for the full WebScript url.
283      * 
284      * @param uri    WebScript URI - for example /test/myscript?arg=value
285      * @param in     The optional InputStream to the call - if supplied a POST will be performed
286      * 
287      * @return Response object from the call {@link Response}
288      */
289     public Response call(String uri, InputStream in)
290     {
291         return call(uri, true, in);
292     }
293 
294     /**
295      * Call a remote WebScript uri. The endpoint as supplied in the constructor will be used
296      * as the prefix for the full WebScript url.
297      * 
298      * @param uri    WebScript URI - for example /test/myscript?arg=value
299      * @param buildResponseString   True to build a String result automatically based on the response
300      *                              encoding, false to instead return the InputStream in the Response.
301      * @param in     The optional InputStream to the call - if supplied a POST will be performed
302      * 
303      * @return Response object from the call {@link Response}
304      */
305     public Response call(String uri, boolean buildResponseString, InputStream in)
306     {
307         if (in != null)
308         {
309             // we have been supplied an input for the request - either POST or PUT
310             if (this.requestMethod != HttpMethod.POST && this.requestMethod != HttpMethod.PUT)
311             {
312                 this.requestMethod = HttpMethod.POST;
313             }
314         }
315         
316         Response result;
317         ResponseStatus status = new ResponseStatus();
318         try
319         {
320             ByteArrayOutputStream bOut = new ByteArrayOutputStream(BUFFERSIZE);
321             String encoding = service(buildURL(uri), in, bOut, status);
322             if (buildResponseString)
323             {
324                 String data;
325                 if (encoding != null)
326                 {
327                     data = bOut.toString(encoding);
328                 }
329                 else
330                 {
331                     data = (defaultEncoding != null ? bOut.toString(defaultEncoding) : bOut.toString());
332                 }
333                 result = new Response(data, status);
334             }
335             else
336             {
337                 result = new Response(new ByteArrayInputStream(bOut.toByteArray()), status);
338             }
339             result.setEncoding(encoding);
340         }
341         catch (IOException ioErr)
342         {
343             if (logger.isDebugEnabled())
344                 logger.debug("Error status " + status.getCode() + " " + status.getMessage());
345 
346             // error information already applied to Status object during service() call
347             result = new Response(status);
348         }
349 
350         return result;
351     }
352 
353     /**
354      * Call a remote WebScript uri. The endpoint as supplied in the constructor will be used
355      * as the prefix for the full WebScript url.
356      * 
357      * @param uri    WebScript URI - for example /test/myscript?arg=value
358      * @param out    OutputStream to stream successful response to - will be closed automatically.
359      *               A response data string will not therefore be available in the Response object.
360      *               If remote call fails the OutputStream will not be modified or closed.
361      * 
362      * @return Response object from the call {@link Response}
363      */
364     public Response call(String uri, OutputStream out)
365     {
366         return call(uri, null, out);
367     }
368 
369     /**
370      * Call a remote WebScript uri. The endpoint as supplied in the constructor will be used
371      * as the prefix for the full WebScript url.
372      * 
373      * @param uri    WebScript URI - for example /test/myscript?arg=value
374      * @param in     The optional InputStream to the call - if supplied a POST will be performed
375      * @param out    OutputStream to stream response to - will be closed automatically.
376      *               A response data string will not therefore be available in the Response object.
377      *               If remote call returns a status code then any available error response will be
378      *               streamed into the output.
379      *               If remote call fails completely the OutputStream will not be modified or closed.
380      * 
381      * @return Response object from the call {@link Response}
382      */
383     public Response call(String uri, InputStream in, OutputStream out)
384     {
385         if (in != null)
386         {
387             // we have been supplied an input for the request - either POST or PUT
388             if (this.requestMethod != HttpMethod.POST && this.requestMethod != HttpMethod.PUT)
389             {
390                 this.requestMethod = HttpMethod.POST;
391             }
392         }
393         
394         Response result;
395         ResponseStatus status = new ResponseStatus();
396         try
397         {
398             String encoding = service(buildURL(uri), in, out, status);
399             result = new Response(status);
400             result.setEncoding(encoding);
401         }
402         catch (IOException ioErr)
403         {
404             if (logger.isDebugEnabled())
405                 logger.debug("Error status " + status.getCode() + " " + status.getMessage());
406 
407             // error information already applied to Status object during service() call
408             result = new Response(status);
409         }
410 
411         return result;
412     }
413 
414     /**
415      * Call a remote WebScript uri. The endpoint as supplied in the constructor will be used
416      * as the prefix for the full WebScript url.
417      * 
418      * @param uri    WebScript URI - for example /test/myscript?arg=value
419      * @param req    HttpServletRequest the request to retrieve input and headers etc. from
420      * @param res    HttpServletResponse the response to stream response to - will be closed automatically.
421      *               A response data string will not therefore be available in the Response object.
422      *               The HTTP method to be used should be set via the setter otherwise GET will be assumed
423      *               and the InputStream will not be retrieve from the request.
424      *               If remote call returns a status code then any available error response will be
425      *               streamed into the response object. 
426      *               If remote call fails completely the OutputStream will not be modified or closed.
427      * 
428      * @return Response object from the call {@link Response}
429      */
430     public Response call(String uri, HttpServletRequest req, HttpServletResponse res)
431     {
432         Response result;
433         ResponseStatus status = new ResponseStatus();
434         try
435         {
436             boolean isPush = (requestMethod == HttpMethod.POST || requestMethod == HttpMethod.PUT);
437             String encoding = service(
438                     buildURL(uri),
439                     isPush ? req.getInputStream() : null,
440                     res != null ? res.getOutputStream() : null,
441                     req, res, status);
442             result = new Response(status);
443             result.setEncoding(encoding);
444         }
445         catch (IOException ioErr)
446         {
447             if (logger.isDebugEnabled())
448                 logger.debug("Error status " + status.getCode() + " " + status.getMessage());
449 
450             // error information already applied to Status object during service() call
451             result = new Response(status);
452         }
453         
454         return result;
455     }
456     
457     /**
458      * Pre-processes the response, propagating cookies and deciding whether a redirect is required
459      * 
460      * @param method
461      *            the executed method
462      * @throws MalformedURLException
463      */
464     protected URL processResponse(URL url, org.apache.commons.httpclient.HttpMethod method)
465             throws MalformedURLException
466     {
467         String redirectLocation = null;
468         for (Header header : method.getResponseHeaders())
469         {
470             String headerName = header.getName();            
471             if (this.cookies != null && headerName.equalsIgnoreCase("set-cookie"))
472             {
473                 String headerValue = header.getValue();
474                 
475                 int z = headerValue.indexOf('=');
476                 if (z != -1)
477                 {
478                     String cookieName = headerValue.substring(0, z);
479                     String cookieValue = headerValue.substring(z + 1, headerValue.length());
480                     int y = cookieValue.indexOf(';');
481                     if (y != -1)
482                     {
483                         cookieValue = cookieValue.substring(0, y);
484                     }
485                     
486                     // store cookie back
487                     if (logger.isDebugEnabled())
488                         logger.debug("RemoteClient found set-cookie: " + cookieName + " = " + cookieValue);
489                     
490                     this.cookies.put(cookieName, cookieValue);
491                 }
492             }
493             if (headerName.equalsIgnoreCase("Location"))
494             {
495                 switch (method.getStatusCode())
496                 {
497                     case RemoteClient.SC_MOVED_TEMPORARILY:
498                     case RemoteClient.SC_MOVED_PERMANENTLY:
499                     case RemoteClient.SC_SEE_OTHER:
500                     case RemoteClient.SC_TEMPORARY_REDIRECT:
501                         redirectLocation = header.getValue();
502                 }
503             }
504         }
505         return redirectLocation == null ? null : new URL(url, redirectLocation);
506     }
507     
508     /**
509      * Build the URL object based on the supplied uri and configured endpoint. Ticket
510      * will be appiled as an argument if available.
511      * 
512      * @param uri     URI to build URL against
513      * 
514      * @return the URL object representing the call.
515      * 
516      * @throws MalformedURLException
517      */
518     private URL buildURL(String uri) throws MalformedURLException
519     {
520         URL url;
521         // TODO: DC - check support for abs urls
522         String resolvedUri = uri.startsWith(endpoint) ? uri : endpoint + uri;
523         if (getTicket() == null)
524         {
525             url = new URL(resolvedUri);
526         }
527         else
528         {
529             url = new URL(resolvedUri +
530                     (uri.lastIndexOf('?') == -1 ? ("?"+getTicketName()+"="+getTicket()) : ("&"+getTicketName()+"="+getTicket())));
531         }
532         return url;
533     }
534 
535     /**
536      * Service a remote URL and write the the result into an output stream.
537      * If an InputStream is provided then a POST will be performed with the content
538      * pushed to the url. Otherwise a standard GET will be performed.
539      * 
540      * @param url    The URL to open and retrieve data from
541      * @param in     The optional InputStream - if set a POST will be performed
542      * @param out    The OutputStream to write result to
543      * @param status The status object to apply the response code too
544      * 
545      * @return encoding specified by the source URL - may be null
546      * 
547      * @throws IOException
548      */
549     private String service(URL url, InputStream in, OutputStream out, ResponseStatus status)
550         throws IOException
551     {
552         return service(url, in, out, null, null, status);
553     }
554 
555     /**
556      * Service a remote URL and write the the result into an output stream.
557      * If an InputStream is provided then a POST will be performed with the content
558      * pushed to the url. Otherwise a standard GET will be performed.
559      * 
560      * @param url    The URL to open and retrieve data from
561      * @param in     The optional InputStream - if set a POST or similar will be performed
562      * @param out    The OutputStream to write result to
563      * @param res    Optional HttpServletResponse - to which response headers will be copied - i.e. proxied
564      * @param status The status object to apply the response code too
565      * 
566      * @return encoding specified by the source URL - may be null
567      * 
568      * @throws IOException
569      */
570     private String service(URL url, InputStream in, OutputStream out,
571             HttpServletRequest req, HttpServletResponse res, ResponseStatus status)
572         throws IOException
573     {
574         final boolean trace = logger.isTraceEnabled();
575         final boolean debug = logger.isDebugEnabled();
576         if (debug)
577         {
578             logger.debug("Executing " + "(" + requestMethod + ") " + url.toString());
579             if (in != null)  logger.debug(" - InputStream supplied - will push...");
580             if (out != null) logger.debug(" - OutputStream supplied - will stream response...");
581             if (req != null && res != null) logger.debug(" - Full Proxy mode between servlet request and response...");
582         }
583         
584         HttpClient client = new HttpClient();
585         
586         final HttpClientParams params = client.getParams();
587         params.setBooleanParameter("http.connection.stalecheck", false);
588         params.setBooleanParameter("http.tcp.nodelay", true);
589         if (!debug)
590         {
591             params.setIntParameter("http.connection.timeout", CONNECT_TIMEOUT);
592             params.setIntParameter("http.socket.timeout", READ_TIMEOUT);
593         }
594         
595         URL redirectURL = url;
596         int responseCode;
597         org.apache.commons.httpclient.HttpMethod method = null;
598         int retries = 0;
599         // Only process redirects if we are not processing a 'push'
600         int maxRetries = in == null ? MAX_REDIRECTS : 1;
601         try
602         {
603             do
604             {
605                 // Release a previous method that we processed due to a redirect
606                 if (method != null)
607                 {
608                     method.releaseConnection();
609                     method = null;
610                 }
611 
612                 switch (this.requestMethod)
613                 {
614                     default:
615                     case GET:
616                         method = new GetMethod(redirectURL.toString());
617                         break;
618                     case PUT:
619                         method = new PutMethod(redirectURL.toString());
620                         break;
621                     case POST:
622                         method = new PostMethod(redirectURL.toString());
623                         break;
624                     case DELETE:
625                         method = new DeleteMethod(redirectURL.toString());
626                         break;
627                     case HEAD:
628                         method = new HeadMethod(redirectURL.toString());
629                         break;
630                 }
631                 
632                 // Switch off automatic redirect handling as we want to process them ourselves and maintain cookies
633                 method.setFollowRedirects(false);
634 
635                 // proxy over any headers from the request stream to proxied request
636                 if (req != null)
637                 {
638                     Enumeration<String> headers = req.getHeaderNames();
639                     while (headers.hasMoreElements())
640                     {
641                         String key = headers.nextElement();
642                         if (key != null)
643                         {
644                             method.setRequestHeader(key, req.getHeader(key));
645                             if (trace)
646                                 logger.trace("Proxy request header: " + key + "=" + req.getHeader(key));
647                         }
648                     }
649                 }
650 
651                 // apply request properties, allows for the assignment and override of specific header properties
652                 if (this.requestProperties != null && this.requestProperties.size() != 0)
653                 {
654                     for (Map.Entry<String, String> entry : requestProperties.entrySet())
655                     {
656                         String headerName = entry.getKey();
657                         String headerValue = this.requestProperties.get(headerName);
658                         method.setRequestHeader(headerName, headerValue);
659                         if (trace)
660                             logger.trace("Set request header: " + headerName + "=" + headerValue);
661                     }
662                 }
663 
664                 // Apply cookies
665                 if (this.cookies != null && !this.cookies.isEmpty())
666                 {
667                     StringBuilder builder = new StringBuilder(128);
668                     for (Map.Entry<String, String> entry : this.cookies.entrySet())
669                     {
670                         if (builder.length() != 0)
671                         {
672                             builder.append(';');
673                         }
674                         builder.append(entry.getKey());
675                         builder.append('=');
676                         builder.append(entry.getValue());
677                     }
678 
679                     String cookieString = builder.toString();
680 
681                     if (debug)
682                         logger.debug("Setting cookie header: " + cookieString);
683                     method.setRequestHeader("Cookie", cookieString);
684                 }
685                 
686                 // HTTP basic auth support
687                 if (this.username != null && this.password != null)
688                 {
689                     String auth = this.username + ':' + this.password;
690                     method.addRequestHeader("Authorization", "Basic " + Base64.encodeBytes(auth.getBytes()));
691                     if (debug)
692                         logger.debug("Applied HTTP Basic Authorization");
693                 }
694                 
695                 // prepare the POST/PUT entity data if input supplied
696                 if (in != null)
697                 {
698                     method.setRequestHeader("Content-Type", this.requestContentType);
699                     
700                     // apply content-length here if known (i.e. from proxied req)
701                     // if this is not set, then the content will be buffered in memory
702                     int contentLength = InputStreamRequestEntity.CONTENT_LENGTH_AUTO;
703                     if (req != null)
704                     {
705                         contentLength = req.getContentLength();
706                     }
707                     
708                     if (debug)
709                         logger.debug("Setting content-type=" + this.requestContentType + " content-length=" + contentLength);
710                     
711                     ((EntityEnclosingMethod) method).setRequestEntity(new InputStreamRequestEntity(in, contentLength));
712                     
713                     // apply any supplied POST request parameters
714                     if (req != null && contentLength == 0 && method instanceof PostMethod)
715                     {
716                         Map<String, String[]> postParams = req.getParameterMap();
717 
718                         if (postParams != null)
719                         {
720                             for (String key : postParams.keySet())
721                             {
722                                 String[] values = postParams.get(key);
723                                 for (int i = 0; i < values.length; i++)
724                                 {
725                                     ((PostMethod) method).addParameter(key, values[i]);
726                                 }
727                             }
728                         }
729                     }
730                 }
731                 
732                 // execute the method to get the response code
733                 responseCode = client.executeMethod(method);
734                 redirectURL = processResponse(redirectURL, method);
735             }
736             while (redirectURL != null && ++retries < maxRetries);
737             
738             // proxy over if required
739             if (res != null)
740             {
741                 res.setStatus(responseCode);
742             }
743             status.setCode(responseCode);
744             if (debug) logger.debug("Response status code: " + responseCode);
745             
746             // walk over headers that are returned from the connection
747             // if we have a servlet response, proxy the headers back to the response
748             // otherwise, store headers on status
749             Header contentType = null;
750             Header contentLength = null;
751             for (Header header : method.getResponseHeaders())
752             {
753                 // NOTE: Tomcat does not appear to be obeying the servlet spec here.
754                 //       If you call setHeader() the spec says it will "clear existing values" - i.e. not
755                 //       add additional values to existing headers - but for Server and Transfer-Encoding
756                 //       if we set them, then two values are received in the response...
757                 // In addition handle the fact that the key can be null.
758                 final String key = header.getName();
759                 if (key != null)
760                 {
761                     if (!key.equalsIgnoreCase("Server") && !key.equalsIgnoreCase("Transfer-Encoding"))
762                     {
763                         if (res != null)
764                         {
765                             res.setHeader(key, header.getValue());
766                         }
767                         
768                         // store headers back onto status
769                         status.setHeader(key, header.getValue());
770                         
771                         if (trace) logger.trace("Response header: " + key + "=" + header.getValue()); 
772                     }
773                     
774                     // grab a reference to the the content-type header here if we find it
775                     if (contentType == null && key.equalsIgnoreCase("Content-Type"))
776                     {
777                         contentType = header;
778                     }
779                     // grab a reference to the content-length header here if we find it
780                     else if (contentLength == null && key.equalsIgnoreCase("Content-Length"))
781                     {
782                         contentLength = header;
783                     }
784                 }
785             }
786             
787             // locate response encoding from the headers
788             String encoding = null;
789             String ct = null;
790             if (contentType != null)
791             {
792                 ct = contentType.getValue();
793                 int csi = ct.indexOf(CHARSETEQUALS);
794                 if (csi != -1)
795                 {
796                     encoding = ct.substring(csi + CHARSETEQUALS.length());
797                 }
798             }
799             if (debug) logger.debug("Response encoding: " + contentType);
800             
801             // perform the stream write from the response to the output
802             int bufferSize = BUFFERSIZE;
803             if (contentLength != null)
804             {
805                 int length = Integer.parseInt(contentLength.getValue());
806                 if (length < bufferSize)
807                 {
808                     bufferSize = length;
809                 }
810             }
811             StringBuilder traceBuf = null;
812             if (trace)
813             {
814                 traceBuf = new StringBuilder(bufferSize);
815             }
816             boolean responseCommit = false;
817             if (responseCode != HttpServletResponse.SC_NOT_MODIFIED)
818             {
819                 InputStream input = method.getResponseBodyAsStream();
820                 if (input != null)
821                 {
822                     try
823                     {
824                         byte[] buffer = new byte[bufferSize];
825                         int read = input.read(buffer);
826                         if (read != -1) responseCommit = true;
827                         while (read != -1)
828                         {
829                             if (out != null)
830                             {
831                                 out.write(buffer, 0, read);
832                             }
833                             
834                             if (trace)
835                             {
836                                 if (ct != null && (ct.startsWith("text/") || ct.startsWith("application/json")))
837                                 {
838                                     traceBuf.append(new String(buffer, 0, read));
839                                 }
840                             }
841                             
842                             read = input.read(buffer);
843                         }
844                     }
845         		    finally
846         		    {
847                         if (trace && traceBuf.length() != 0)
848                         {
849                             logger.trace("Output (" + (traceBuf.length()) + " bytes) from: " + url.toString());
850                             logger.trace(traceBuf.toString());
851                         }
852                         try
853                         {
854                             try
855                             {
856                                 input.close();
857                             }
858                             finally
859                             {
860                                 if (responseCommit)
861                                 {
862                                     if (out != null)
863                                     {
864                                         out.close();
865                                     }
866                                 }
867                             }
868                         }
869                         catch (IOException e)
870                         {
871                             if (logger.isWarnEnabled())
872                                 logger.warn("Exception during close() of HTTP API connection", e);
873                         }
874                     }
875                 }
876             }
877             
878             // apply error response message if required
879             if (res != null && responseCode != HttpServletResponse.SC_OK && !responseCommit)
880             {
881                 res.sendError(responseCode, method.getStatusText());
882             }
883             
884             // if we get here call was successful
885             return encoding;
886         }
887         catch (ConnectTimeoutException timeErr)
888         {
889             // caught a socket timeout IO exception - apply internal error code
890             status.setCode(SC_REMOTE_CONN_TIMEOUT);
891             status.setException(timeErr);
892             status.setMessage(timeErr.getMessage());
893             if (res != null)
894             {
895                 // externally just return a generic 500 server error
896                 res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, timeErr.getMessage());
897             }            
898             
899             throw timeErr;
900         }
901         catch (SocketTimeoutException socketErr)
902         {
903             // caught a socket timeout IO exception - apply internal error code
904             status.setCode(SC_REMOTE_CONN_TIMEOUT);
905             status.setException(socketErr);
906             status.setMessage(socketErr.getMessage());
907             if (res != null)
908             {
909                 // externally just return a generic 500 server error
910                 res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, socketErr.getMessage());
911             }            
912             
913             throw socketErr;
914         }
915         catch (UnknownHostException hostErr)
916         {
917             // caught an unknown host IO exception - apply internal error code
918             status.setCode(SC_REMOTE_CONN_NOHOST);
919             status.setException(hostErr);
920             status.setMessage(hostErr.getMessage());
921             if (res != null)
922             {
923                 // externally just return a generic 500 server error
924                 res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, hostErr.getMessage());
925             }            
926             
927             throw hostErr;
928         }
929         catch (ConnectException connErr)
930         {
931             // caught a no host IO exception - apply internal error code
932             status.setCode(SC_REMOTE_CONN_NOHOST);
933             status.setException(connErr);
934             status.setMessage(connErr.getMessage());
935             if (res != null)
936             {
937                 // externally just return a generic 500 server error
938                 res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, connErr.getMessage());
939             }            
940             
941             throw connErr;
942         }
943         catch (IOException ioErr)
944         {
945             // caught a general IO exception - apply generic error code so one gets returned
946             status.setCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
947             status.setException(ioErr);
948             status.setMessage(ioErr.getMessage());
949             if (res != null)
950             {
951                 res.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, ioErr.getMessage());
952             }            
953             
954             throw ioErr;
955         }
956         finally
957         {
958             // reset state values
959             method.releaseConnection();
960             this.requestContentType = "application/octet-stream";
961             this.requestMethod = HttpMethod.GET;
962         }
963     }    
964 }