com.google.gwt.user.server.rpc.RPCServletUtils.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gwt.user.server.rpc.RPCServletUtils.java

Source

/*
 * Copyright 2008 Google Inc.
 * 
 * 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 com.google.gwt.user.server.rpc;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.zip.GZIPOutputStream;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Utility class containing helper methods used by servlets that integrate with
 * the RPC system.
 */
public class RPCServletUtils {
    /**
     * Package protected for use in tests.
     */
    static final int BUFFER_SIZE = 4096;

    private static final String ACCEPT_ENCODING = "Accept-Encoding";

    private static final String ATTACHMENT = "attachment";

    /**
     * Used both as expected request charset and encoded response charset.
     */
    private static final String CHARSET_UTF8 = "UTF-8";

    private static final String CONTENT_DISPOSITION = "Content-Disposition";

    private static final String CONTENT_ENCODING = "Content-Encoding";

    private static final String CONTENT_ENCODING_GZIP = "gzip";

    private static final String CONTENT_TYPE_APPLICATION_JSON_UTF8 = "application/json; charset=utf-8";

    private static final String GENERIC_FAILURE_MSG = "The call failed on the server; see server log for details";

    private static final String GWT_RPC_CONTENT_TYPE = "text/x-gwt-rpc";

    /**
     * Controls the compression threshold at and below which no compression will
     * take place.
     */
    private static final int UNCOMPRESSED_BYTE_SIZE_LIMIT = 256;

    /**
     * Returns <code>true</code> if the {@link HttpServletRequest} accepts Gzip
     * encoding. This is done by checking that the accept-encoding header
     * specifies gzip as a supported encoding.
     * 
     * @param request the request instance to test for gzip encoding acceptance
     * @return <code>true</code> if the {@link HttpServletRequest} accepts Gzip
     *         encoding
     */
    public static boolean acceptsGzipEncoding(HttpServletRequest request) {
        assert (request != null);

        String acceptEncoding = request.getHeader(ACCEPT_ENCODING);
        if (null == acceptEncoding) {
            return false;
        }

        return (acceptEncoding.indexOf(CONTENT_ENCODING_GZIP) != -1);
    }

    /**
     * Returns <code>true</code> if the response content's estimated UTF-8 byte
     * length exceeds 256 bytes.
     * 
     * @param content the contents of the response
     * @return <code>true</code> if the response content's estimated UTF-8 byte
     *         length exceeds 256 bytes
     */
    public static boolean exceedsUncompressedContentLengthLimit(String content) {
        return (content.length() * 2) > UNCOMPRESSED_BYTE_SIZE_LIMIT;
    }

    /**
     * Returns true if the {@link java.lang.reflect.Method Method} definition on
     * the service is specified to throw the exception contained in the
     * InvocationTargetException or false otherwise. NOTE we do not check that the
     * type is serializable here. We assume that it must be otherwise the
     * application would never have been allowed to run.
     * 
     * @param serviceIntfMethod the method from the RPC request
     * @param cause the exception that the method threw
     * @return true if the exception's type is in the method's signature
     */
    public static boolean isExpectedException(Method serviceIntfMethod, Throwable cause) {
        assert (serviceIntfMethod != null);
        assert (cause != null);

        Class<?>[] exceptionsThrown = serviceIntfMethod.getExceptionTypes();
        if (exceptionsThrown.length <= 0) {
            // The method is not specified to throw any exceptions
            //
            return false;
        }

        Class<? extends Throwable> causeType = cause.getClass();

        for (Class<?> exceptionThrown : exceptionsThrown) {
            assert (exceptionThrown != null);

            if (exceptionThrown.isAssignableFrom(causeType)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Returns the content of an {@link HttpServletRequest} by decoding it using
     * <code>expectedCharSet</code>, or <code>UTF-8</code> if
     * <code>expectedCharSet</code> is <code>null</null>.
     * 
     * @param request the servlet request whose content we want to read
     * @param expectedContentType the expected content (i.e. 'type/subtype' only)
     *          in the Content-Type request header, or <code>null</code> if no
     *          validation is to be performed, and you are willing to allow for
     *          some types of cross type security attacks
     * @param expectedCharSet the expected request charset, or <code>null</code>
     *          if no charset validation is to be performed and <code>UTF-8</code>
     *          should be assumed
     * @return the content of an {@link HttpServletRequest} by decoding it using
     *         <code>expectedCharSet</code>, or <code>UTF-8</code> if
     *         <code>expectedCharSet</code> is <code>null</code>
     * @throws IOException if the request's input stream cannot be accessed, read
     *         from or closed
     * @throws ServletException if the request's content type does not
     *         equal the supplied <code>expectedContentType</code> or
     *         <code>expectedCharSet</code>
     */
    public static String readContent(HttpServletRequest request, String expectedContentType, String expectedCharSet)
            throws IOException, ServletException {
        if (expectedContentType != null) {
            checkContentTypeIgnoreCase(request, expectedContentType);
        }
        if (expectedCharSet != null) {
            checkCharacterEncodingIgnoreCase(request, expectedCharSet);
        }

        /*
         * Need to support 'Transfer-Encoding: chunked', so do not rely on
         * presence of a 'Content-Length' request header.
         */
        InputStream in = request.getInputStream();
        byte[] buffer = new byte[BUFFER_SIZE];
        ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE);
        try {
            while (true) {
                int byteCount = in.read(buffer);
                if (byteCount == -1) {
                    break;
                }
                out.write(buffer, 0, byteCount);
            }
            String contentCharSet = expectedCharSet != null ? expectedCharSet : CHARSET_UTF8;
            return out.toString(contentCharSet);
        } finally {
            if (in != null) {
                in.close();
            }
        }
    }

    /**
     * Returns the content of an {@link HttpServletRequest}, after verifying a
     * <code>gwt/x-gwt-rpc; charset=utf-8</code> content type.
     * 
     * @param request the servlet request whose content we want to read
     * @return the content of an {@link HttpServletRequest} by decoding it using
     *         <code>UTF-8</code>
     * @throws IOException if the request's input stream cannot be accessed, read
     *         from or closed
     * @throws ServletException if the request's content type is not
     *         <code>gwt/x-gwt-rpc; charset=utf-8</code>, ignoring case
     */
    public static String readContentAsGwtRpc(HttpServletRequest request) throws IOException, ServletException {
        return readContent(request, GWT_RPC_CONTENT_TYPE, CHARSET_UTF8);
    }

    /**
      * Returns the content of an {@link HttpServletRequest} by decoding it using
      * the UTF-8 charset.
      * 
      * @param request the servlet request whose content we want to read
      * @return the content of an {@link HttpServletRequest} by decoding it using
      *         the UTF-8 charset
      * @throws IOException if the requests input stream cannot be accessed, read
      *           from or closed
      * @throws ServletException if the content length of the request is not
      *           specified of if the request's content type is not
      *           'text/x-gwt-rpc' and 'charset=utf-8'
      * @deprecated Use {@link #readContent} instead.
      */
    @Deprecated
    public static String readContentAsUtf8(HttpServletRequest request) throws IOException, ServletException {
        return readContent(request, null, null);
    }

    /**
     * Returns the content of an {@link HttpServletRequest} by decoding it using
     * the UTF-8 charset.
     * 
     * @param request the servlet request whose content we want to read
     * @param checkHeaders Specify 'true' to check the Content-Type header to see
     *          that it matches the expected value 'text/x-gwt-rpc' and the
     *          content encoding is UTF-8. Disabling this check may allow some
     *          types of cross type security attacks.
     * @return the content of an {@link HttpServletRequest} by decoding it using
     *         the UTF-8 charset
     * @throws IOException if the requests input stream cannot be accessed, read
     *           from or closed
     * @throws ServletException if the content length of the request is not
     *           specified of if the request's content type is not
     *           'text/x-gwt-rpc' and 'charset=utf-8'
     * @deprecated Use {@link #readContent} instead.
     */
    @Deprecated
    public static String readContentAsUtf8(HttpServletRequest request, boolean checkHeaders)
            throws IOException, ServletException {
        return readContent(request, GWT_RPC_CONTENT_TYPE, CHARSET_UTF8);
    }

    /**
     * Sets the correct header to indicate that a response is gzipped.
     */
    public static void setGzipEncodingHeader(HttpServletResponse response) {
        response.setHeader(CONTENT_ENCODING, CONTENT_ENCODING_GZIP);
    }

    /**
     * Returns <code>true</code> if the request accepts gzip encoding and the the
     * response content's estimated UTF-8 byte length exceeds 256 bytes.
     * 
     * @param request the request associated with the response content
     * @param responseContent a string that will be
     * @return <code>true</code> if the request accepts gzip encoding and the the
     *         response content's estimated UTF-8 byte length exceeds 256 bytes
     */
    public static boolean shouldGzipResponseContent(HttpServletRequest request, String responseContent) {
        return acceptsGzipEncoding(request) && exceedsUncompressedContentLengthLimit(responseContent);
    }

    /**
     * Write the response content into the {@link HttpServletResponse}. If
     * <code>gzipResponse</code> is <code>true</code>, the response content will
     * be gzipped prior to being written into the response.
     * 
     * @param servletContext servlet context for this response
     * @param response response instance
     * @param responseContent a string containing the response content
     * @param gzipResponse if <code>true</code> the response content will be gzip
     *          encoded before being written into the response
     * @throws IOException if reading, writing, or closing the response's output
     *           stream fails
     */
    public static void writeResponse(ServletContext servletContext, HttpServletResponse response,
            String responseContent, boolean gzipResponse) throws IOException {

        byte[] responseBytes = responseContent.getBytes(CHARSET_UTF8);
        if (gzipResponse) {
            // Compress the reply and adjust headers.
            //
            ByteArrayOutputStream output = null;
            GZIPOutputStream gzipOutputStream = null;
            Throwable caught = null;
            try {
                output = new ByteArrayOutputStream(responseBytes.length);
                gzipOutputStream = new GZIPOutputStream(output);
                gzipOutputStream.write(responseBytes);
                gzipOutputStream.finish();
                gzipOutputStream.flush();
                setGzipEncodingHeader(response);
                responseBytes = output.toByteArray();
            } catch (IOException e) {
                caught = e;
            } finally {
                if (null != gzipOutputStream) {
                    gzipOutputStream.close();
                }
                if (null != output) {
                    output.close();
                }
            }

            if (caught != null) {
                servletContext.log("Unable to compress response", caught);
                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
                return;
            }
        }

        // Send the reply.
        //
        response.setContentLength(responseBytes.length);
        response.setContentType(CONTENT_TYPE_APPLICATION_JSON_UTF8);
        response.setStatus(HttpServletResponse.SC_OK);
        response.setHeader(CONTENT_DISPOSITION, ATTACHMENT);
        response.getOutputStream().write(responseBytes);
    }

    /**
     * Called when the servlet itself has a problem, rather than the invoked
     * third-party method. It writes a simple 500 message back to the client.
     * 
     * @param servletContext
     * @param response
     * @param failure
     */
    public static void writeResponseForUnexpectedFailure(ServletContext servletContext,
            HttpServletResponse response, Throwable failure) {
        servletContext.log("Exception while dispatching incoming RPC call", failure);

        // Send GENERIC_FAILURE_MSG with 500 status.
        //
        try {
            response.setContentType("text/plain");
            response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
            try {
                response.getOutputStream().write(GENERIC_FAILURE_MSG.getBytes("UTF-8"));
            } catch (IllegalStateException e) {
                // Handle the (unexpected) case where getWriter() was previously used
                response.getWriter().write(GENERIC_FAILURE_MSG);
            }
        } catch (IOException ex) {
            servletContext.log(
                    "respondWithUnexpectedFailure failed while sending the previous failure to the client", ex);
        }
    }

    /**
     * Performs validation of the character encoding, ignoring case.
     * 
     * @param request the incoming request
     * @param expectedCharSet the expected charset of the request
     * @throws ServletException if requests encoding is not <code>null</code> and
     *         does not equal, ignoring case, <code>expectedCharSet</code>
     */
    private static void checkCharacterEncodingIgnoreCase(HttpServletRequest request, String expectedCharSet)
            throws ServletException {
        boolean encodingOkay = false;
        String characterEncoding = request.getCharacterEncoding();
        if (characterEncoding != null) {
            /*
             * TODO: It would seem that we should be able to use equalsIgnoreCase here
             * instead of indexOf. Need to be sure that servlet engines return a
             * properly parsed character encoding string if we decide to make this
             * change.
             */
            if (characterEncoding.toLowerCase().indexOf(expectedCharSet.toLowerCase()) != -1) {
                encodingOkay = true;
            }
        }

        if (!encodingOkay) {
            throw new ServletException(
                    "Character Encoding is '" + (characterEncoding == null ? "(null)" : characterEncoding)
                            + "'.  Expected '" + expectedCharSet + "'");
        }
    }

    /**
     * Performs Content-Type validation of the incoming request, ignoring case
     * and any <code>charset</code> parameter.
     *
     * @see   #checkCharacterEncodingIgnoreCase(HttpServletRequest, String)
     * @param request the incoming request
     * @param expectedContentType the expected Content-Type for the incoming
     *        request
     * @throws ServletException if the request's content type is not
     *         <code>null</code> and does not, ignoring case, equal
     *         <code>expectedContentType</code>,
     */
    private static void checkContentTypeIgnoreCase(HttpServletRequest request, String expectedContentType)
            throws ServletException {
        String contentType = request.getContentType();
        boolean contentTypeIsOkay = false;

        if (contentType != null) {
            contentType = contentType.toLowerCase();
            /*
             * NOTE:We use startsWith because some servlet engines, i.e. Tomcat, do
             * not remove the charset component but others do.
             */
            if (contentType.startsWith(expectedContentType.toLowerCase())) {
                contentTypeIsOkay = true;
            }
        }

        if (!contentTypeIsOkay) {
            throw new ServletException("Content-Type was '" + (contentType == null ? "(null)" : contentType)
                    + "'. Expected '" + expectedContentType + "'.");
        }
    }

    private RPCServletUtils() {
        // Not instantiable
    }
}