HttpResponseUtils.java

/* 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.esigate.http;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.List;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.entity.DeflateDecompressingEntity;
import org.apache.http.client.entity.GzipDecompressingEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.cookie.Cookie;
import org.apache.http.cookie.CookieOrigin;
import org.apache.http.cookie.CookieSpec;
import org.apache.http.cookie.MalformedCookieException;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.cookie.DefaultCookieSpec;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.Args;
import org.apache.http.util.EntityUtils;
import org.esigate.HttpErrorPage;
import org.esigate.events.EventManager;
import org.esigate.events.impl.ReadEntityEvent;
import org.esigate.util.UriUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Utility methods for HttpClient's Request and Response objects.
 * 
 * @author Francois-Xavier Bonnet
 * @author Nicolas Richeton
 * 
 */
public final class HttpResponseUtils {
    private static final Logger LOG = LoggerFactory.getLogger(HttpResponseUtils.class);
    private static final int OUTPUT_BUFFER_SIZE = 4096;

    private HttpResponseUtils() {

    }

    /**
     * Check if httpResponse has an error status.
     * 
     * @param httpResponse
     *            tge {@link HttpResponse}
     * @return true if status code >= 400
     */
    public static boolean isError(HttpResponse httpResponse) {
        return httpResponse.getStatusLine().getStatusCode() >= HttpStatus.SC_BAD_REQUEST;
    }

    /**
     * Get the value of the first header matching "headerName".
     * 
     * @param headerName
     * @param httpResponse
     * @return value of the first header or null if it doesn't exist.
     */
    public static String getFirstHeader(String headerName, HttpResponse httpResponse) {
        Header header = httpResponse.getFirstHeader(headerName);
        if (header != null) {
            return header.getValue();
        }
        return null;
    }

    /**
     * Removes ";jsessionid=<id>" from the url, if the session id is also set in "httpResponse".
     * <p>
     * This methods first looks for the following header :
     * 
     * <pre>
     * Set-Cookie: JSESSIONID=
     * </pre>
     * 
     * If found and perfectly matches the jsessionid value in url, the complete jsessionid definition is removed from
     * the url.
     * 
     * @param uri
     *            original uri, may contains a jsessionid.
     * @param httpResponse
     *            the response which set the jsessionId
     * @return uri, without jsession
     */
    public static String removeSessionId(String uri, HttpResponse httpResponse) {
        CookieSpec cookieSpec = new DefaultCookieSpec();
        // Dummy origin, used only by CookieSpec for setting the domain for the
        // cookie but we don't need it
        CookieOrigin cookieOrigin = new CookieOrigin("dummy", Http.DEFAULT_HTTP_PORT, "/", false);
        Header[] responseHeaders = httpResponse.getHeaders("Set-cookie");
        String jsessionid = null;
        for (Header header : responseHeaders) {
            try {
                List<Cookie> cookies = cookieSpec.parse(header, cookieOrigin);
                for (Cookie cookie : cookies) {
                    if ("JSESSIONID".equalsIgnoreCase(cookie.getName())) {
                        jsessionid = cookie.getValue();
                    }
                    break;
                }
            } catch (MalformedCookieException ex) {
                LOG.warn("Malformed header: " + header.getName() + ": " + header.getValue());
            }
            if (jsessionid != null) {
                break;
            }
        }
        if (jsessionid == null) {
            return uri;
        }

        return UriUtils.removeSessionId(jsessionid, uri);

    }

    /**
     * Returns the response body as a string or the reason phrase if body is empty.
     * <p>
     * This methods is similar to EntityUtils#toString() internally, but uncompress the entity first if necessary.
     * <p>
     * This methods also holds an extension point, which can be used to guess the real encoding of the entity, if the
     * HTTP headers set a wrong encoding declaration.
     * 
     * @since 3.0
     * @since 4.1 - Event EventManager.EVENT_READ_ENTITY is fired when calling this method.
     * 
     * @param httpResponse
     * @param eventManager
     * @return The body as string or the reason phrase if body was empty.
     * @throws HttpErrorPage
     */
    public static String toString(HttpResponse httpResponse, EventManager eventManager) throws HttpErrorPage {
        HttpEntity httpEntity = httpResponse.getEntity();
        String result;
        if (httpEntity == null) {
            result = httpResponse.getStatusLine().getReasonPhrase();
        } else {
            // Unzip the stream if necessary
            Header contentEncoding = httpEntity.getContentEncoding();
            if (contentEncoding != null) {
                String contentEncodingValue = contentEncoding.getValue();
                if ("gzip".equalsIgnoreCase(contentEncodingValue) || "x-gzip".equalsIgnoreCase(contentEncodingValue)) {
                    httpEntity = new GzipDecompressingEntity(httpEntity);
                } else if ("deflate".equalsIgnoreCase(contentEncodingValue)) {
                    httpEntity = new DeflateDecompressingEntity(httpEntity);
                } else {
                    throw new UnsupportedContentEncodingException("Content-encoding \"" + contentEncoding
                            + "\" is not supported");
                }
            }

            try {
                byte[] rawEntityContent = EntityUtils.toByteArray(httpEntity);
                ContentType contentType;
                Charset charset;
                String mimeType;
                try {
                    contentType = ContentType.getOrDefault(httpEntity);
                    mimeType = contentType.getMimeType();
                    charset = contentType.getCharset();
                } catch (UnsupportedCharsetException ex) {
                    throw new UnsupportedEncodingException(ex.getMessage());
                }

                // Use default charset is no valid information found from HTTP
                // headers
                if (charset == null) {
                    charset = HTTP.DEF_CONTENT_CHARSET;
                }

                ReadEntityEvent event = new ReadEntityEvent(mimeType, charset, rawEntityContent);

                // Read using charset based on HTTP headers
                event.setEntityContent(new String(rawEntityContent, charset));

                // Allow extensions to detect document encoding
                if (eventManager != null) {
                    eventManager.fire(EventManager.EVENT_READ_ENTITY, event);
                }

                return event.getEntityContent();

            } catch (IOException e) {
                throw new HttpErrorPage(HttpErrorPage.generateHttpResponse(e));
            }
        }

        return removeSessionId(result, httpResponse);
    }

    public static ContentType getContentType(CloseableHttpResponse response) {
        HttpEntity entity = response.getEntity();
        if (entity == null) {
            return null;
        }
        return ContentType.get(entity);
    }

    public static String toString(CloseableHttpResponse response) throws HttpErrorPage {
        return toString(response, null);
    }

    /**
     * Copied from org.apache.http.entity.InputStreamEntity.writeTo(OutputStream) method but flushes the buffer after
     * each read in order to allow streaming and web sockets.
     * 
     * @param httpEntity
     *            The entity to copy to the OutputStream
     * @param outstream
     *            The OutputStream
     * @throws IOException
     *             If a problem occurs
     */
    public static void writeTo(final HttpEntity httpEntity, final OutputStream outstream) throws IOException {
        Args.notNull(outstream, "Output stream");
        try (InputStream instream = httpEntity.getContent()) {
            final byte[] buffer = new byte[OUTPUT_BUFFER_SIZE];
            int l;
            if (httpEntity.getContentLength() < 0) {
                // consume until EOF
                while ((l = instream.read(buffer)) != -1) {
                    outstream.write(buffer, 0, l);
                    outstream.flush();
                    LOG.debug("Flushed {} bytes of data");
                }
            } else {
                // consume no more than length
                long remaining = httpEntity.getContentLength();
                while (remaining > 0) {
                    l = instream.read(buffer, 0, (int) Math.min(OUTPUT_BUFFER_SIZE, remaining));
                    if (l == -1) {
                        break;
                    }
                    outstream.write(buffer, 0, l);
                    outstream.flush();
                    LOG.debug("Flushed {} bytes of data");
                    remaining -= l;
                }
            }
        }
    }

}