Avoid Java bottleneck by using explicit Charset for byte[] <-> String conversions.

Repost of Rietveld issue 158803.
Patch By: Jason Terk

Review at http://gwt-code-reviews.appspot.com/1632803

Review by: skybrian@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@10933 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/user/server/rpc/HybridServiceServlet.java b/user/src/com/google/gwt/user/server/rpc/HybridServiceServlet.java
index 9d7c619..16fbe7a 100644
--- a/user/src/com/google/gwt/user/server/rpc/HybridServiceServlet.java
+++ b/user/src/com/google/gwt/user/server/rpc/HybridServiceServlet.java
@@ -114,7 +114,7 @@
       onAfterResponseSerialized(toReturn);
 
       try {
-        stream.write(toReturn.getBytes("UTF-8"));
+        stream.write(toReturn.getBytes(RPCServletUtils.CHARSET_UTF8));
       } catch (IOException e) {
         throw new SerializationException("Unable to commit bytes", e);
       }
diff --git a/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
index 05bf40d..d9d2178 100644
--- a/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
+++ b/user/src/com/google/gwt/user/server/rpc/RPCServletUtils.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -19,6 +19,8 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.lang.reflect.Method;
+import java.nio.charset.Charset;
+import java.util.concurrent.ConcurrentHashMap;
 import java.util.zip.GZIPOutputStream;
 
 import javax.servlet.ServletContext;
@@ -31,6 +33,16 @@
  * the RPC system.
  */
 public class RPCServletUtils {
+  
+  public static final String CHARSET_UTF8_NAME = "UTF-8";
+  
+  /**
+   * The UTF-8 Charset. Use this to avoid concurrency bottlenecks when
+   * converting between byte arrays and Strings.
+   * See http://code.google.com/p/google-web-toolkit/issues/detail?id=6398
+   */
+  public static final Charset CHARSET_UTF8 = Charset.forName(CHARSET_UTF8_NAME);
+
   /**
    * Package protected for use in tests.
    */
@@ -40,11 +52,6 @@
 
   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";
@@ -64,10 +71,24 @@
   private static final int UNCOMPRESSED_BYTE_SIZE_LIMIT = 256;
 
   /**
+   * Contains cached mappings from character set name to Charset. The
+   * null key maps to the default UTF-8 character set.
+   */
+  private static final ConcurrentHashMap<String, Charset> CHARSET_CACHE =
+      new ConcurrentHashMap<String, Charset>();
+
+  /**
+   * Pre-populate the character set cache with UTF-8.
+   */
+  static {
+    CHARSET_CACHE.put(CHARSET_UTF8_NAME, CHARSET_UTF8);
+  }
+
+  /**
    * 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
@@ -86,7 +107,7 @@
   /**
    * 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
@@ -96,12 +117,38 @@
   }
 
   /**
+   * Get the Charset for a named character set. Caches Charsets to work around
+   * a concurrency bottleneck in FastCharsetProvider.
+   * See http://code.google.com/p/google-web-toolkit/issues/detail?id=6398
+   * @see {@link Charset#forName(String)}
+   *
+   * @param encoding the name of the Charset to get. If this is null
+   *                 the default UTF-8 character set will be returned.
+   * @return the named Charset.
+   */
+  public static Charset getCharset(String encoding) {
+        
+    if (encoding == null) {
+      return CHARSET_UTF8;
+    }
+    
+    Charset charset = CHARSET_CACHE.get(encoding);
+
+    if (charset == null) {
+      charset = Charset.forName(encoding);
+      CHARSET_CACHE.put(encoding, charset);
+    }
+
+    return charset;
+  }
+
+  /**
    * 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
@@ -135,7 +182,7 @@
    * 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
@@ -178,20 +225,18 @@
         }
         out.write(buffer, 0, byteCount);
       }
-      String contentCharSet = expectedCharSet != null
-          ? expectedCharSet : CHARSET_UTF8;
-      return out.toString(contentCharSet);
+      return new String(out.toByteArray(), getCharset(expectedCharSet));
     } 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>
@@ -202,13 +247,13 @@
    */
   public static String readContentAsGwtRpc(HttpServletRequest request)
       throws IOException, ServletException {
-      return readContent(request, GWT_RPC_CONTENT_TYPE, CHARSET_UTF8);
+      return readContent(request, GWT_RPC_CONTENT_TYPE, CHARSET_UTF8_NAME);
   }
 
  /**
    * 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
@@ -228,7 +273,7 @@
   /**
    * 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
@@ -246,7 +291,7 @@
   @Deprecated
   public static String readContentAsUtf8(HttpServletRequest request,
       boolean checkHeaders) throws IOException, ServletException {
-    return readContent(request, GWT_RPC_CONTENT_TYPE, CHARSET_UTF8);
+    return readContent(request, GWT_RPC_CONTENT_TYPE, CHARSET_UTF8_NAME);
   }
 
   /**
@@ -259,7 +304,7 @@
   /**
    * 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
@@ -275,7 +320,7 @@
    * 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
@@ -333,7 +378,7 @@
   /**
    * 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
@@ -349,7 +394,7 @@
       response.setContentType("text/plain");
       response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
       try {
-        response.getOutputStream().write(GENERIC_FAILURE_MSG.getBytes("UTF-8"));
+        response.getOutputStream().write(GENERIC_FAILURE_MSG.getBytes(CHARSET_UTF8));
       } catch (IllegalStateException e) {
         // Handle the (unexpected) case where getWriter() was previously used
         response.getWriter().write(GENERIC_FAILURE_MSG);
@@ -363,7 +408,7 @@
 
   /**
    * 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
@@ -372,6 +417,8 @@
   private static void checkCharacterEncodingIgnoreCase(
       HttpServletRequest request, String expectedCharSet)
       throws ServletException {
+    
+    assert (expectedCharSet != null);
     boolean encodingOkay = false;
     String characterEncoding = request.getCharacterEncoding();
     if (characterEncoding != null) {
@@ -409,6 +456,8 @@
   private static void checkContentTypeIgnoreCase(
       HttpServletRequest request, String expectedContentType)
       throws ServletException {
+    
+    assert (expectedContentType != null);
     String contentType = request.getContentType();
     boolean contentTypeIsOkay = false;
 
diff --git a/user/src/com/google/gwt/user/server/rpc/impl/SerializabilityUtil.java b/user/src/com/google/gwt/user/server/rpc/impl/SerializabilityUtil.java
index 3b81cc9..7aa02db 100644
--- a/user/src/com/google/gwt/user/server/rpc/impl/SerializabilityUtil.java
+++ b/user/src/com/google/gwt/user/server/rpc/impl/SerializabilityUtil.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -21,6 +21,7 @@
 import com.google.gwt.user.client.rpc.SerializationException;
 import com.google.gwt.user.client.rpc.SerializationStreamReader;
 import com.google.gwt.user.client.rpc.SerializationStreamWriter;
+import com.google.gwt.user.server.rpc.RPCServletUtils;
 import com.google.gwt.user.server.rpc.SerializationPolicy;
 import com.google.gwt.user.server.rpc.ServerCustomFieldSerializer;
 
@@ -47,9 +48,6 @@
  * Serialization utility class used by the server-side RPC code.
  */
 public class SerializabilityUtil {
-
-  public static final String DEFAULT_ENCODING = "UTF-8";
-
   /**
    * Comparator used to sort fields.
    */
@@ -222,7 +220,7 @@
 
   /**
    * Return the concrete type that a generic type maps to, if known.
-   * 
+   *
    * @param genericType The generic type to resolve.
    * @param resolvedTypes A map of generic types to actual types.
    * @return The actual type, which may be of any subclass of Type.
@@ -244,11 +242,11 @@
 
   /**
    * Determine the expected types for any instance type parameters.
-   * 
+   *
    * This method also determines whether or not the instance can be assigned to
    * the expected type. We combine the tasks because they require traversing the
    * same data structures.
-   * 
+   *
    * @param instanceClass The instance for which we want generic parameter types
    * @param expectedType The type we are expecting this instance to be
    * @param resolvedTypes The types that have been resolved to actual values
@@ -269,14 +267,14 @@
       if (resolvedTypes != null) {
         findInstanceParameters(instanceClass, resolvedTypes, expectedParameterTypes);
       }
-      
+
       // With no expected type, the instance is assignable and we fall through
     } else {
       // First determine what type we are really expecting. The type may still
       // be a TypeVariable<?> at this time when we are deserializing class
       // fields or components of another data structure.
       Type actualType = findActualType(expectedType, resolvedTypes);
-      
+
       // Try to match the instanceClass to the expected type, updating the
       // expectedParameterTypes so that we can track them back to the resolved
       // types once we determine what class to treat the instance as. In
@@ -294,7 +292,7 @@
         // and we are done.
         return null;
       }
-      
+
       // Now that we know what class the instance should be,
       // get any remaining parameters using resolved types.
       if (resolvedTypes != null) {
@@ -309,7 +307,7 @@
 
   /**
    * Find the Class that a given type refers to.
-   * 
+   *
    * @param type The type of interest
    * @return The Class that type represents
    */
@@ -353,7 +351,7 @@
   /**
    * Returns the {@link Class} which can serialize the given instance type, or
    * <code>null</code> if this class has no custom field serializer.
-   * 
+   *
    * Note that arrays never have custom field serializers.
    */
   public static Class<?> hasCustomFieldSerializer(Class<?> instanceType) {
@@ -384,7 +382,7 @@
    * Returns the server-side {@link Class} which can serialize the given
    * instance type, or <code>null</code> if this class has no type-checking
    * custom field serializer.
-   * 
+   *
    * Note that arrays never have custom field serializers.
    */
   public static Class<?> hasServerCustomFieldSerializer(Class<?> instanceType) {
@@ -413,10 +411,10 @@
 
   /**
    * Remove all of the actual types that arose from the the given type.
-   * 
+   *
    * This method should always be called after a corresponding call to
    * resolveTypes.
-   * 
+   *
    * @param methodType The type we wish to assign this instance to
    * @param resolvedTypes The types that have been resolved to actual values
    */
@@ -428,10 +426,10 @@
    * Find all the actual types we can from the information in the given type,
    * and put the mapping from TypeVariable objects to actual types into the
    * resolved types map.
-   * 
+   *
    * The method releaseTypes should always be called after a call to this
    * method, unless the resolved types map is about to be discarded.
-   * 
+   *
    * @param methodType The type we wish to assign this instance to
    * @param resolvedTypes The types that have been resolved to actual values
    */
@@ -464,12 +462,12 @@
   /**
    * Loads a {@link CustomFieldSerializer} from a class that may implement that
    * interface.
-   * 
+   *
    * @param customSerializerClass the Custom Field Serializer class
-   * 
+   *
    * @return an instance the class provided if it implements
    *         {@link CustomFieldSerializer} or {@code null} if it does not
-   * 
+   *
    * @throws SerializationException if the load process encounters an unexpected
    *           problem
    */
@@ -567,7 +565,7 @@
     if (Throwable.class == field.getDeclaringClass()) {
       /**
        * Only serialize Throwable's detailMessage field; all others are ignored.
-       * 
+       *
        * NOTE: Changing the set of fields that we serialize for Throwable will
        * necessitate a change to our JRE emulation's version of Throwable.
        */
@@ -612,7 +610,7 @@
       }
     } else if (expectedType instanceof WildcardType) {
       WildcardType wildcardType = (WildcardType) expectedType;
-      
+
       Type[] lowerBounds = wildcardType.getLowerBounds();
       for (Type type : lowerBounds) {
         /* Require instance to be a superclass of type, or type itself. */
@@ -625,7 +623,7 @@
           }
           boundClass = boundClass.getSuperclass();
         }
-        
+
         // We fail if the class does not meet any bound, as we should.
         if (boundClass == null) {
           return false;
@@ -672,7 +670,7 @@
         return true;
       }
     }
-    
+
     Type[] interfaces = instanceClass.getGenericInterfaces();
     for (Type interfaceType : interfaces) {
       Type[] localTypes = expectedParameterTypes.clone();
@@ -691,7 +689,7 @@
   private static boolean findExpectedInstanceClassFromSuper(
       Type superType, Type expectedType, DequeMap<TypeVariable<?>, Type> resolvedTypes,
       Set<Class<?>> expectedInstanceClasses, Type[] expectedParameterTypes) {
-    
+
     if (superType instanceof GenericArrayType) {
       // Can't use array types as supertypes or interfaces
       return false;
@@ -723,18 +721,18 @@
       Type[] upperBounds = wildcardType.getUpperBounds();
       for (Type boundType : upperBounds) {
         if (findExpectedInstanceClassFromSuper(boundType, expectedType,
-            resolvedTypes, expectedInstanceClasses, expectedParameterTypes)) { 
+            resolvedTypes, expectedInstanceClasses, expectedParameterTypes)) {
           return true;
         }
       }
     }
-    
+
     return false;
   }
 
     /**
    * Attempt to find known type for TypeVariable type from an instance.
-   * 
+   *
    * @param foundParameter The currently known parameter, which must be of type
    *          TypeVariable
    * @param instanceType The instance that we need to check for information
@@ -802,7 +800,7 @@
    * Attempt to find the actual types for the generic parameters of an instance,
    * given the types we have resolved from the method signature or class field
    * declaration.
-   * 
+   *
    * @param instanceClass The instance for which we want actual generic
    *          parameter types.
    * @param resolvedTypes The types that have been resolved to actual values
@@ -861,7 +859,7 @@
 
   private static void generateSerializationSignature(Class<?> instanceType, CRC32 crc,
       SerializationPolicy policy) throws UnsupportedEncodingException {
-    crc.update(getSerializedTypeName(instanceType).getBytes(DEFAULT_ENCODING));
+    crc.update(getSerializedTypeName(instanceType).getBytes(RPCServletUtils.CHARSET_UTF8));
 
     if (excludeImplementationFromSerializationSignature(instanceType)) {
       return;
@@ -882,8 +880,8 @@
          * generate the signature. Otherwise, use all known fields.
          */
         if ((clientFieldNames == null) || clientFieldNames.contains(field.getName())) {
-          crc.update(field.getName().getBytes(DEFAULT_ENCODING));
-          crc.update(getSerializedTypeName(field.getType()).getBytes(DEFAULT_ENCODING));
+          crc.update(field.getName().getBytes(RPCServletUtils.CHARSET_UTF8));
+          crc.update(getSerializedTypeName(field.getType()).getBytes(RPCServletUtils.CHARSET_UTF8));
         }
       }
 
@@ -954,7 +952,7 @@
       }
     } else if (methodType instanceof Class) {
       Class<?> classType = (Class<?>) methodType;
-      
+
       // A type that is of instance Class, with TypeParameters, must be a raw
       // class, so strip off any parameters in the map.
       TypeVariable<?>[] classParams = classType.getTypeParameters();
diff --git a/user/test/com/google/gwt/user/server/rpc/RPCServletUtilsTest.java b/user/test/com/google/gwt/user/server/rpc/RPCServletUtilsTest.java
index 27c6932..18928f2 100644
--- a/user/test/com/google/gwt/user/server/rpc/RPCServletUtilsTest.java
+++ b/user/test/com/google/gwt/user/server/rpc/RPCServletUtilsTest.java
@@ -1,12 +1,12 @@
 /*
  * 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
@@ -23,6 +23,7 @@
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
 
 import javax.servlet.ServletException;
 import javax.servlet.ServletInputStream;
@@ -30,7 +31,6 @@
 
 /**
  * Tests some of the methods in {@link RPCServletUtils}.
- * 
  */
 public class RPCServletUtilsTest extends TestCase {
 
@@ -69,7 +69,7 @@
     public String getHeader(String name) {
       return null;
     }
-      
+
     @SuppressWarnings("unused")
     @Override
     public ServletInputStream getInputStream() throws IOException {
@@ -165,6 +165,31 @@
   }
 
   /**
+   * RPCServletUtils#getCharset() should return the same instance for
+   * every invocation of a given encoding.
+   */
+  public void testGetCharsetInstances() {
+    // Default UTF-8 character set.
+    assertSame(RPCServletUtils.getCharset(null),
+        RPCServletUtils.getCharset(null));
+    assertSame(RPCServletUtils.getCharset("UTF-8"),
+        RPCServletUtils.getCharset("UTF-8"));
+    assertSame(RPCServletUtils.getCharset("US-ASCII"),
+        RPCServletUtils.getCharset("US-ASCII"));
+    assertSame(RPCServletUtils.getCharset("ISO-8859-1"),
+        RPCServletUtils.getCharset("ISO-8859-1"));
+  }
+
+  /**
+   * Test that RPCServletUtils#getCharset() returns the correct
+   * default UTF-8 charachter set when passed a null encoding value.
+   */
+  public void testGetDefaultCharset() {
+    assertEquals(Charset.forName("UTF-8"), RPCServletUtils.CHARSET_UTF8);
+    assertSame(RPCServletUtils.CHARSET_UTF8, RPCServletUtils.getCharset(null));
+  }
+
+  /**
    * Character type doesn't match UTF-8, but ignore it.
    */
   public void testIgnoreCharacterEncoding() throws IOException,