View Javadoc
1   /* 
2    * Licensed under the Apache License, Version 2.0 (the "License");
3    * you may not use this file except in compliance with the License.
4    * You may obtain a copy of the License at
5    *
6    * http://www.apache.org/licenses/LICENSE-2.0
7    *
8    * Unless required by applicable law or agreed to in writing, software
9    * distributed under the License is distributed on an "AS IS" BASIS,
10   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11   * See the License for the specific language governing permissions and
12   * limitations under the License.
13   *
14   */
15  
16  package org.esigate.http;
17  
18  import java.io.IOException;
19  import java.io.InputStream;
20  import java.io.OutputStream;
21  import java.io.UnsupportedEncodingException;
22  import java.nio.charset.Charset;
23  import java.nio.charset.UnsupportedCharsetException;
24  import java.util.List;
25  
26  import org.apache.http.Header;
27  import org.apache.http.HttpEntity;
28  import org.apache.http.HttpResponse;
29  import org.apache.http.HttpStatus;
30  import org.apache.http.client.entity.DeflateDecompressingEntity;
31  import org.apache.http.client.entity.GzipDecompressingEntity;
32  import org.apache.http.client.methods.CloseableHttpResponse;
33  import org.apache.http.cookie.Cookie;
34  import org.apache.http.cookie.CookieOrigin;
35  import org.apache.http.cookie.CookieSpec;
36  import org.apache.http.cookie.MalformedCookieException;
37  import org.apache.http.entity.ContentType;
38  import org.apache.http.impl.cookie.DefaultCookieSpec;
39  import org.apache.http.protocol.HTTP;
40  import org.apache.http.util.Args;
41  import org.apache.http.util.EntityUtils;
42  import org.esigate.HttpErrorPage;
43  import org.esigate.events.EventManager;
44  import org.esigate.events.impl.ReadEntityEvent;
45  import org.esigate.util.UriUtils;
46  import org.slf4j.Logger;
47  import org.slf4j.LoggerFactory;
48  
49  /**
50   * Utility methods for HttpClient's Request and Response objects.
51   * 
52   * @author Francois-Xavier Bonnet
53   * @author Nicolas Richeton
54   * 
55   */
56  public final class HttpResponseUtils {
57      private static final Logger LOG = LoggerFactory.getLogger(HttpResponseUtils.class);
58      private static final int OUTPUT_BUFFER_SIZE = 4096;
59  
60      private HttpResponseUtils() {
61  
62      }
63  
64      /**
65       * Check if httpResponse has an error status.
66       * 
67       * @param httpResponse
68       *            tge {@link HttpResponse}
69       * @return true if status code >= 400
70       */
71      public static boolean isError(HttpResponse httpResponse) {
72          return httpResponse.getStatusLine().getStatusCode() >= HttpStatus.SC_BAD_REQUEST;
73      }
74  
75      /**
76       * Get the value of the first header matching "headerName".
77       * 
78       * @param headerName
79       * @param httpResponse
80       * @return value of the first header or null if it doesn't exist.
81       */
82      public static String getFirstHeader(String headerName, HttpResponse httpResponse) {
83          Header header = httpResponse.getFirstHeader(headerName);
84          if (header != null) {
85              return header.getValue();
86          }
87          return null;
88      }
89  
90      /**
91       * Removes ";jsessionid=<id>" from the url, if the session id is also set in "httpResponse".
92       * <p>
93       * This methods first looks for the following header :
94       * 
95       * <pre>
96       * Set-Cookie: JSESSIONID=
97       * </pre>
98       * 
99       * If found and perfectly matches the jsessionid value in url, the complete jsessionid definition is removed from
100      * the url.
101      * 
102      * @param uri
103      *            original uri, may contains a jsessionid.
104      * @param httpResponse
105      *            the response which set the jsessionId
106      * @return uri, without jsession
107      */
108     public static String removeSessionId(String uri, HttpResponse httpResponse) {
109         CookieSpec cookieSpec = new DefaultCookieSpec();
110         // Dummy origin, used only by CookieSpec for setting the domain for the
111         // cookie but we don't need it
112         CookieOrigin cookieOrigin = new CookieOrigin("dummy", Http.DEFAULT_HTTP_PORT, "/", false);
113         Header[] responseHeaders = httpResponse.getHeaders("Set-cookie");
114         String jsessionid = null;
115         for (Header header : responseHeaders) {
116             try {
117                 List<Cookie> cookies = cookieSpec.parse(header, cookieOrigin);
118                 for (Cookie cookie : cookies) {
119                     if ("JSESSIONID".equalsIgnoreCase(cookie.getName())) {
120                         jsessionid = cookie.getValue();
121                     }
122                     break;
123                 }
124             } catch (MalformedCookieException ex) {
125                 LOG.warn("Malformed header: " + header.getName() + ": " + header.getValue());
126             }
127             if (jsessionid != null) {
128                 break;
129             }
130         }
131         if (jsessionid == null) {
132             return uri;
133         }
134 
135         return UriUtils.removeSessionId(jsessionid, uri);
136 
137     }
138 
139     /**
140      * Returns the response body as a string or the reason phrase if body is empty.
141      * <p>
142      * This methods is similar to EntityUtils#toString() internally, but uncompress the entity first if necessary.
143      * <p>
144      * This methods also holds an extension point, which can be used to guess the real encoding of the entity, if the
145      * HTTP headers set a wrong encoding declaration.
146      * 
147      * @since 3.0
148      * @since 4.1 - Event EventManager.EVENT_READ_ENTITY is fired when calling this method.
149      * 
150      * @param httpResponse
151      * @param eventManager
152      * @return The body as string or the reason phrase if body was empty.
153      * @throws HttpErrorPage
154      */
155     public static String toString(HttpResponse httpResponse, EventManager eventManager) throws HttpErrorPage {
156         HttpEntity httpEntity = httpResponse.getEntity();
157         String result;
158         if (httpEntity == null) {
159             result = httpResponse.getStatusLine().getReasonPhrase();
160         } else {
161             // Unzip the stream if necessary
162             Header contentEncoding = httpEntity.getContentEncoding();
163             if (contentEncoding != null) {
164                 String contentEncodingValue = contentEncoding.getValue();
165                 if ("gzip".equalsIgnoreCase(contentEncodingValue) || "x-gzip".equalsIgnoreCase(contentEncodingValue)) {
166                     httpEntity = new GzipDecompressingEntity(httpEntity);
167                 } else if ("deflate".equalsIgnoreCase(contentEncodingValue)) {
168                     httpEntity = new DeflateDecompressingEntity(httpEntity);
169                 } else {
170                     throw new UnsupportedContentEncodingException("Content-encoding \"" + contentEncoding
171                             + "\" is not supported");
172                 }
173             }
174 
175             try {
176                 byte[] rawEntityContent = EntityUtils.toByteArray(httpEntity);
177                 ContentType contentType;
178                 Charset charset;
179                 String mimeType;
180                 try {
181                     contentType = ContentType.getOrDefault(httpEntity);
182                     mimeType = contentType.getMimeType();
183                     charset = contentType.getCharset();
184                 } catch (UnsupportedCharsetException ex) {
185                     throw new UnsupportedEncodingException(ex.getMessage());
186                 }
187 
188                 // Use default charset is no valid information found from HTTP
189                 // headers
190                 if (charset == null) {
191                     charset = HTTP.DEF_CONTENT_CHARSET;
192                 }
193 
194                 ReadEntityEvent event = new ReadEntityEvent(mimeType, charset, rawEntityContent);
195 
196                 // Read using charset based on HTTP headers
197                 event.setEntityContent(new String(rawEntityContent, charset));
198 
199                 // Allow extensions to detect document encoding
200                 if (eventManager != null) {
201                     eventManager.fire(EventManager.EVENT_READ_ENTITY, event);
202                 }
203 
204                 return event.getEntityContent();
205 
206             } catch (IOException e) {
207                 throw new HttpErrorPage(HttpErrorPage.generateHttpResponse(e));
208             }
209         }
210 
211         return removeSessionId(result, httpResponse);
212     }
213 
214     public static ContentType getContentType(CloseableHttpResponse response) {
215         HttpEntity entity = response.getEntity();
216         if (entity == null) {
217             return null;
218         }
219         return ContentType.get(entity);
220     }
221 
222     public static String toString(CloseableHttpResponse response) throws HttpErrorPage {
223         return toString(response, null);
224     }
225 
226     /**
227      * Copied from org.apache.http.entity.InputStreamEntity.writeTo(OutputStream) method but flushes the buffer after
228      * each read in order to allow streaming and web sockets.
229      * 
230      * @param httpEntity
231      *            The entity to copy to the OutputStream
232      * @param outstream
233      *            The OutputStream
234      * @throws IOException
235      *             If a problem occurs
236      */
237     public static void writeTo(final HttpEntity httpEntity, final OutputStream outstream) throws IOException {
238         Args.notNull(outstream, "Output stream");
239         try (InputStream instream = httpEntity.getContent()) {
240             final byte[] buffer = new byte[OUTPUT_BUFFER_SIZE];
241             int l;
242             if (httpEntity.getContentLength() < 0) {
243                 // consume until EOF
244                 while ((l = instream.read(buffer)) != -1) {
245                     outstream.write(buffer, 0, l);
246                     outstream.flush();
247                     LOG.debug("Flushed {} bytes of data");
248                 }
249             } else {
250                 // consume no more than length
251                 long remaining = httpEntity.getContentLength();
252                 while (remaining > 0) {
253                     l = instream.read(buffer, 0, (int) Math.min(OUTPUT_BUFFER_SIZE, remaining));
254                     if (l == -1) {
255                         break;
256                     }
257                     outstream.write(buffer, 0, l);
258                     outstream.flush();
259                     LOG.debug("Flushed {} bytes of data");
260                     remaining -= l;
261                 }
262             }
263         }
264     }
265 
266 }