Replace anonymous Account.Id serializer with named class

CacheSerializer.convert constructs a CacheSerializer class which, being
created as return value, does not have a well-defined class name, but
rather an anonymous class name.

This can break some cache implementations that try to parse the
serializer and expect a well-defined class name.

Specifically this issue has been observed in an implementation of
chronicle-map cache. More information can be found in the relevant issue
ticket.

Bug: Issue 13410
Change-Id: I15cdfb4bcb0d3f2bb92bacf5d9ce9a5e270f5680
diff --git a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
index 3bb88e5..03ecd91 100644
--- a/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
+++ b/java/com/google/gerrit/server/auth/oauth/OAuthTokenCache.java
@@ -40,15 +40,32 @@
 
   private final DynamicItem<OAuthTokenEncrypter> encrypter;
 
+  public enum AccountIdSerializer implements CacheSerializer<Account.Id> {
+    INSTANCE;
+
+    private final Converter<Account.Id, Integer> converter =
+        Converter.from(Account.Id::get, Account::id);
+
+    private final Converter<Integer, Account.Id> reverse = converter.reverse();
+
+    @Override
+    public byte[] serialize(Account.Id object) {
+      return IntegerCacheSerializer.INSTANCE.serialize(converter.convert(object));
+    }
+
+    @Override
+    public Account.Id deserialize(byte[] in) {
+      return reverse.convert(IntegerCacheSerializer.INSTANCE.deserialize(in));
+    }
+  }
+
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
         persist(OAUTH_TOKENS, Account.Id.class, OAuthToken.class)
             .version(1)
-            .keySerializer(
-                CacheSerializer.convert(
-                    IntegerCacheSerializer.INSTANCE, Converter.from(Account.Id::get, Account::id)))
+            .keySerializer(AccountIdSerializer.INSTANCE)
             .valueSerializer(new Serializer());
       }
     };
diff --git a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
index 586f1bc..64fa74f 100644
--- a/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
+++ b/javatests/com/google/gerrit/server/auth/oauth/OAuthTokenCacheTest.java
@@ -19,6 +19,7 @@
 import static com.google.gerrit.proto.testing.SerializedClassSubject.assertThatSerializedClass;
 
 import com.google.common.collect.ImmutableMap;
+import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.auth.oauth.OAuthToken;
 import com.google.gerrit.proto.testing.SerializedClassSubject;
 import com.google.gerrit.server.cache.proto.Cache.OAuthTokenProto;
@@ -71,6 +72,23 @@
     assertThat(s.deserialize(serializedWithEmptyString)).isEqualTo(tokenWithNull);
   }
 
+  @Test
+  public void serializeAndDeserializeBackAccountId() {
+    OAuthTokenCache.AccountIdSerializer serializer = OAuthTokenCache.AccountIdSerializer.INSTANCE;
+
+    Account.Id id = Account.id(1234);
+    assertThat(serializer.deserialize(serializer.serialize(id))).isEqualTo(id);
+  }
+
+  // Anonymous classes can break some cache implementations that try to parse the
+  // serializer class name and expect a well-defined class name: test that
+  // OAuthTokenCache.AccountIdSerializer is not an anonymous class.
+  @Test
+  public void accountIdSerializerIsNotAnAnonymousClass() {
+    assertThat(OAuthTokenCache.AccountIdSerializer.INSTANCE.getDeclaringClass().getSimpleName())
+        .isNotEmpty();
+  }
+
   /** See {@link SerializedClassSubject} for background and what to do if this test fails. */
   @Test
   public void oAuthTokenFields() throws Exception {