You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ol...@apache.org on 2017/10/19 14:04:55 UTC

[2/4] httpcomponents-client git commit: Factored out logic shared by Memcached and Ehcache implementations into an abstract base class

Factored out logic shared by Memcached and Ehcache implementations into an abstract base class


Project: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/repo
Commit: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/commit/f215fdcd
Tree: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/tree/f215fdcd
Diff: http://git-wip-us.apache.org/repos/asf/httpcomponents-client/diff/f215fdcd

Branch: refs/heads/master
Commit: f215fdcd32f714c2ec9b390300debac7fa8e9ec7
Parents: f70c974
Author: Oleg Kalnichevski <ol...@apache.org>
Authored: Tue Oct 17 16:23:51 2017 +0200
Committer: Oleg Kalnichevski <ol...@apache.org>
Committed: Thu Oct 19 15:36:11 2017 +0200

----------------------------------------------------------------------
 .../http/cache/HttpCacheEntrySerializer.java    |  26 +-
 .../http/cache/HttpCacheStorageEntry.java       |  56 ++
 .../http/cache/HttpCacheUpdateException.java    |   4 +-
 .../impl/cache/AbstractBinaryCacheStorage.java  |  46 ++
 .../cache/AbstractSerializingCacheStorage.java  | 127 ++++
 .../cache/ByteArrayCacheEntrySerializer.java    |  80 +++
 .../cache/DefaultHttpCacheEntrySerializer.java  |  70 ---
 .../cache/ehcache/EhcacheHttpCacheStorage.java  | 114 ++--
 .../cache/memcached/MemcachedCacheEntry.java    |  77 ---
 .../memcached/MemcachedCacheEntryFactory.java   |  62 --
 .../MemcachedCacheEntryFactoryImpl.java         |  45 --
 .../memcached/MemcachedCacheEntryImpl.java      | 111 ----
 .../memcached/MemcachedHttpCacheStorage.java    | 169 ++----
 .../MemcachedSerializationException.java        |  41 --
 .../cache/memcached/PrefixKeyHashingScheme.java |   2 +-
 .../cache/memcached/SHA256KeyHashingScheme.java |   4 +-
 .../http/impl/cache/HttpCacheEntryMatcher.java  | 100 +++
 .../TestAbstractSerializingCacheStorage.java    | 256 ++++++++
 .../TestByteArrayCacheEntrySerializer.java      |  89 +++
 .../cache/TestHttpCacheEntrySerializers.java    | 149 -----
 .../ehcache/TestEhcacheHttpCacheStorage.java    | 247 --------
 .../TestEhcacheProtocolRequirements.java        |  88 ---
 .../TestMemcachedCacheEntryFactoryImpl.java     |  48 --
 .../memcached/TestMemcachedCacheEntryImpl.java  | 119 ----
 .../TestMemcachedHttpCacheStorage.java          | 603 -------------------
 25 files changed, 865 insertions(+), 1868 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheEntrySerializer.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheEntrySerializer.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheEntrySerializer.java
index dcf9fb2..70f57c6 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheEntrySerializer.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheEntrySerializer.java
@@ -26,28 +26,28 @@
  */
 package org.apache.hc.client5.http.cache;
 
-import java.io.InputStream;
-import java.io.OutputStream;
-
 /**
- * Used by some {@link HttpCacheStorage} implementations to serialize
- * {@link HttpCacheEntry} instances to a byte representation before
- * storage.
+ * Serializer / deserializer for {@link HttpCacheStorageEntry} entries.
+ *
+ * @since 5.0
  */
-public interface HttpCacheEntrySerializer {
+public interface HttpCacheEntrySerializer<T> {
 
     /**
-     * Serializes the given entry to a byte representation on the
-     * given {@link OutputStream}.
+     * Serializes the given entry.
+     *
+     * @param entry cache entry
+     * @return serialized representation of the cache entry
      * @throws ResourceIOException
      */
-    void writeTo(HttpCacheEntry entry, OutputStream os) throws ResourceIOException;
+    T serialize(HttpCacheStorageEntry entry) throws ResourceIOException;
 
     /**
-     * Deserializes a byte representation of a cache entry by reading
-     * from the given {@link InputStream}.
+     * Deserializes a cache entry from its serialized representation.
+     * @param serializedObject serialized representation of the cache entry
+     * @return cache entry
      * @throws ResourceIOException
      */
-    HttpCacheEntry readFrom(InputStream is) throws ResourceIOException;
+    HttpCacheStorageEntry deserialize(T serializedObject) throws ResourceIOException;
 
 }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheStorageEntry.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheStorageEntry.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheStorageEntry.java
new file mode 100644
index 0000000..d806069
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheStorageEntry.java
@@ -0,0 +1,56 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.cache;
+
+import java.io.Serializable;
+
+import org.apache.hc.core5.util.Args;
+
+public final class HttpCacheStorageEntry implements Serializable {
+
+    private final String key;
+    private final HttpCacheEntry content;
+
+    public HttpCacheStorageEntry(final String key, final HttpCacheEntry content) {
+        this.key = key;
+        this.content = Args.notNull(content, "Cache entry");
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public HttpCacheEntry getContent() {
+        return content;
+    }
+
+    @Override
+    public String toString() {
+        return "[key=" + key + "; content=" + content + "]";
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheUpdateException.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheUpdateException.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheUpdateException.java
index 317593a..2e7617d 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheUpdateException.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/cache/HttpCacheUpdateException.java
@@ -27,8 +27,8 @@
 package org.apache.hc.client5.http.cache;
 
 /**
- * Signals that {@link HttpCacheStorage} encountered an error performing an
- * processChallenge operation.
+ * Signals that {@link HttpCacheStorage} encountered an error performing
+ * an update operation.
  *
  * @since 4.1
  */

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AbstractBinaryCacheStorage.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AbstractBinaryCacheStorage.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AbstractBinaryCacheStorage.java
new file mode 100644
index 0000000..e959216
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AbstractBinaryCacheStorage.java
@@ -0,0 +1,46 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.cache;
+
+import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
+
+/**
+ * Abstract cache backend for serialized binary objects capable of CAS (compare-and-swap) updates.
+ *
+ * @since 5.0
+ */
+public abstract class AbstractBinaryCacheStorage<CAS> extends AbstractSerializingCacheStorage<byte[], CAS> {
+
+    public AbstractBinaryCacheStorage(final int maxUpdateRetries, final HttpCacheEntrySerializer<byte[]> serializer) {
+        super(maxUpdateRetries, serializer);
+    }
+
+    public AbstractBinaryCacheStorage(final int maxUpdateRetries) {
+        super(maxUpdateRetries, ByteArrayCacheEntrySerializer.INSTANCE);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AbstractSerializingCacheStorage.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AbstractSerializingCacheStorage.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AbstractSerializingCacheStorage.java
new file mode 100644
index 0000000..084cbd7
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/AbstractSerializingCacheStorage.java
@@ -0,0 +1,127 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.cache;
+
+import org.apache.hc.client5.http.cache.HttpCacheEntry;
+import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
+import org.apache.hc.client5.http.cache.HttpCacheStorage;
+import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
+import org.apache.hc.client5.http.cache.HttpCacheUpdateCallback;
+import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
+import org.apache.hc.client5.http.cache.ResourceIOException;
+import org.apache.hc.core5.util.Args;
+
+/**
+ * Abstract cache backend for serialized objects capable of CAS (compare-and-swap) updates.
+ *
+ * @since 5.0
+ */
+public abstract class AbstractSerializingCacheStorage<T, CAS> implements HttpCacheStorage {
+
+    private final int maxUpdateRetries;
+    private final HttpCacheEntrySerializer<T> serializer;
+
+    public AbstractSerializingCacheStorage(final int maxUpdateRetries, final HttpCacheEntrySerializer<T> serializer) {
+        this.maxUpdateRetries = Args.notNegative(maxUpdateRetries, "Max retries");
+        this.serializer = Args.notNull(serializer, "Cache entry serializer");
+    }
+
+    protected abstract String digestToStorageKey(String key);
+
+    protected abstract void store(String storageKey, T storageObject) throws ResourceIOException;
+
+    protected abstract T restore(String storageKey) throws ResourceIOException;
+
+    protected abstract CAS getForUpdateCAS(String storageKey) throws ResourceIOException;
+
+    protected abstract T getStorageObject(CAS cas) throws ResourceIOException;
+
+    protected abstract boolean updateCAS(String storageKey, CAS cas, T storageObject) throws ResourceIOException;
+
+    protected abstract void delete(String storageKey) throws ResourceIOException;
+
+    @Override
+    public final void putEntry(final String key, final HttpCacheEntry entry) throws ResourceIOException {
+        final String storageKey = digestToStorageKey(key);
+        final T storageObject = serializer.serialize(new HttpCacheStorageEntry(key, entry));
+        store(storageKey, storageObject);
+    }
+
+    @Override
+    public final HttpCacheEntry getEntry(final String key) throws ResourceIOException {
+        final String storageKey = digestToStorageKey(key);
+        final T storageObject = restore(storageKey);
+        if (storageObject == null) {
+            return null;
+        }
+        final HttpCacheStorageEntry entry = serializer.deserialize(storageObject);
+        if (key.equals(entry.getKey())) {
+            return entry.getContent();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public final void removeEntry(final String key) throws ResourceIOException {
+        final String storageKey = digestToStorageKey(key);
+        delete(storageKey);
+    }
+
+    @Override
+    public final void updateEntry(
+            final String key,
+            final HttpCacheUpdateCallback callback) throws HttpCacheUpdateException, ResourceIOException {
+        int numRetries = 0;
+        final String storageKey = digestToStorageKey(key);
+        for (;;) {
+            final CAS cas = getForUpdateCAS(storageKey);
+            HttpCacheStorageEntry storageEntry = cas != null ? serializer.deserialize(getStorageObject(cas)) : null;
+            if (storageEntry != null && !key.equals(storageEntry.getKey())) {
+                storageEntry = null;
+            }
+            final HttpCacheEntry existingEntry = storageEntry != null ? storageEntry.getContent() : null;
+            final HttpCacheEntry updatedEntry = callback.update(existingEntry);
+
+            if (existingEntry == null) {
+                putEntry(key, updatedEntry);
+                return;
+
+            }
+            final T storageObject = serializer.serialize(new HttpCacheStorageEntry(key, updatedEntry));
+            if (!updateCAS(storageKey, cas, storageObject)) {
+                numRetries++;
+                if (numRetries >= maxUpdateRetries) {
+                    throw new HttpCacheUpdateException("Cache update failed after " + numRetries + " retries");
+                }
+            } else {
+                return;
+            }
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ByteArrayCacheEntrySerializer.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ByteArrayCacheEntrySerializer.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ByteArrayCacheEntrySerializer.java
new file mode 100644
index 0000000..e5f653c
--- /dev/null
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ByteArrayCacheEntrySerializer.java
@@ -0,0 +1,80 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.cache;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
+import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
+import org.apache.hc.client5.http.cache.ResourceIOException;
+import org.apache.hc.core5.annotation.Contract;
+import org.apache.hc.core5.annotation.ThreadingBehavior;
+
+/**
+ * {@link HttpCacheEntrySerializer} implementation that uses the default (native)
+ * serialization.
+ *
+ * @see java.io.Serializable
+ *
+ * @since 4.1
+ */
+@Contract(threading = ThreadingBehavior.IMMUTABLE)
+public final class ByteArrayCacheEntrySerializer implements HttpCacheEntrySerializer<byte[]> {
+
+    public static final ByteArrayCacheEntrySerializer INSTANCE = new ByteArrayCacheEntrySerializer();
+
+    @Override
+    public byte[] serialize(final HttpCacheStorageEntry cacheEntry) throws ResourceIOException {
+        if (cacheEntry == null) {
+            return null;
+        }
+        final ByteArrayOutputStream buf = new ByteArrayOutputStream();
+        try (final ObjectOutputStream oos = new ObjectOutputStream(buf)) {
+            oos.writeObject(cacheEntry);
+        } catch (final IOException ex) {
+            throw new ResourceIOException(ex.getMessage(), ex);
+        }
+        return buf.toByteArray();
+    }
+
+    @Override
+    public HttpCacheStorageEntry deserialize(final byte[] serializedObject) throws ResourceIOException {
+        if (serializedObject == null) {
+            return null;
+        }
+        try (final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serializedObject))) {
+            return (HttpCacheStorageEntry) ois.readObject();
+        } catch (final IOException | ClassNotFoundException ex) {
+            throw new ResourceIOException(ex.getMessage(), ex);
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultHttpCacheEntrySerializer.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultHttpCacheEntrySerializer.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultHttpCacheEntrySerializer.java
deleted file mode 100644
index ca39e46..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/DefaultHttpCacheEntrySerializer.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-import java.io.OutputStream;
-
-import org.apache.hc.client5.http.cache.HttpCacheEntry;
-import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
-import org.apache.hc.client5.http.cache.ResourceIOException;
-import org.apache.hc.core5.annotation.Contract;
-import org.apache.hc.core5.annotation.ThreadingBehavior;
-
-/**
- * {@link HttpCacheEntrySerializer} implementation that uses the default (native)
- * serialization.
- *
- * @see java.io.Serializable
- *
- * @since 4.1
- */
-@Contract(threading = ThreadingBehavior.IMMUTABLE)
-public class DefaultHttpCacheEntrySerializer implements HttpCacheEntrySerializer {
-
-    @Override
-    public void writeTo(final HttpCacheEntry cacheEntry, final OutputStream os) throws ResourceIOException {
-        try (final ObjectOutputStream oos = new ObjectOutputStream(os)) {
-            oos.writeObject(cacheEntry);
-        } catch (final IOException ex) {
-            throw new ResourceIOException(ex.getMessage(), ex);
-        }
-    }
-
-    @Override
-    public HttpCacheEntry readFrom(final InputStream is) throws ResourceIOException {
-        try (final ObjectInputStream ois = new ObjectInputStream(is)) {
-            return (HttpCacheEntry) ois.readObject();
-        } catch (final IOException | ClassNotFoundException ex) {
-            throw new ResourceIOException(ex.getMessage(), ex);
-        }
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ehcache/EhcacheHttpCacheStorage.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ehcache/EhcacheHttpCacheStorage.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ehcache/EhcacheHttpCacheStorage.java
index ad31ace..36cc4c2 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ehcache/EhcacheHttpCacheStorage.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/ehcache/EhcacheHttpCacheStorage.java
@@ -26,17 +26,12 @@
  */
 package org.apache.hc.client5.http.impl.cache.ehcache;
 
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-
-import org.apache.hc.client5.http.cache.HttpCacheEntry;
 import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
-import org.apache.hc.client5.http.cache.HttpCacheStorage;
-import org.apache.hc.client5.http.cache.HttpCacheUpdateCallback;
-import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
 import org.apache.hc.client5.http.cache.ResourceIOException;
+import org.apache.hc.client5.http.impl.cache.AbstractBinaryCacheStorage;
 import org.apache.hc.client5.http.impl.cache.CacheConfig;
-import org.apache.hc.client5.http.impl.cache.DefaultHttpCacheEntrySerializer;
+import org.apache.hc.client5.http.impl.cache.ByteArrayCacheEntrySerializer;
+import org.apache.hc.core5.util.Args;
 
 import net.sf.ehcache.Ehcache;
 import net.sf.ehcache.Element;
@@ -58,31 +53,17 @@ import net.sf.ehcache.Element;
  * itself.</p>
  * @since 4.1
  */
-public class EhcacheHttpCacheStorage implements HttpCacheStorage {
+public class EhcacheHttpCacheStorage extends AbstractBinaryCacheStorage<Element> {
 
     private final Ehcache cache;
-    private final HttpCacheEntrySerializer serializer;
-    private final int maxUpdateRetries;
 
     /**
      * Constructs a storage backend using the provided Ehcache
      * with default configuration options.
      * @param cache where to store cached origin responses
      */
-    public EhcacheHttpCacheStorage(final Ehcache cache) {
-        this(cache, CacheConfig.DEFAULT, new DefaultHttpCacheEntrySerializer());
-    }
-
-    /**
-     * Constructs a storage backend using the provided Ehcache
-     * with the given configuration options.
-     * @param cache where to store cached origin responses
-     * @param config cache storage configuration options - note that
-     *   the setting for max object size <b>will be ignored</b> and
-     *   should be configured in the Ehcache instead.
-     */
-    public EhcacheHttpCacheStorage(final Ehcache cache, final CacheConfig config){
-        this(cache, config, new DefaultHttpCacheEntrySerializer());
+    public EhcacheHttpCacheStorage(final Ehcache cache){
+        this(cache, CacheConfig.DEFAULT, ByteArrayCacheEntrySerializer.INSTANCE);
     }
 
     /**
@@ -95,67 +76,60 @@ public class EhcacheHttpCacheStorage implements HttpCacheStorage {
      *   should be configured in the Ehcache instead.
      * @param serializer alternative serialization mechanism
      */
-    public EhcacheHttpCacheStorage(final Ehcache cache, final CacheConfig config, final HttpCacheEntrySerializer serializer){
-        this.cache = cache;
-        this.maxUpdateRetries = config.getMaxUpdateRetries();
-        this.serializer = serializer;
+    public EhcacheHttpCacheStorage(
+            final Ehcache cache,
+            final CacheConfig config,
+            final HttpCacheEntrySerializer<byte[]> serializer) {
+        super((config != null ? config : CacheConfig.DEFAULT).getMaxUpdateRetries(), serializer);
+        this.cache = Args.notNull(cache, "Ehcache");
     }
 
     @Override
-    public synchronized void putEntry(final String key, final HttpCacheEntry entry) throws ResourceIOException {
-        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
-        serializer.writeTo(entry, bos);
-        cache.put(new Element(key, bos.toByteArray()));
+    protected String digestToStorageKey(final String key) {
+        return key;
     }
 
     @Override
-    public synchronized HttpCacheEntry getEntry(final String key) throws ResourceIOException {
-        final Element e = cache.get(key);
-        if(e == null){
+    protected void store(final String storageKey, final byte[] storageObject) throws ResourceIOException {
+        cache.put(new Element(storageKey, storageKey));
+    }
+
+    private byte[] castAsByteArray(final Object storageObject) throws ResourceIOException {
+        if (storageObject == null) {
             return null;
         }
-
-        final byte[] data = (byte[])e.getObjectValue();
-        return serializer.readFrom(new ByteArrayInputStream(data));
+        if (storageObject instanceof byte[]) {
+            return (byte[]) storageObject;
+        } else {
+            throw new ResourceIOException("Unexpected cache content: " + storageObject.getClass());
+        }
     }
 
     @Override
-    public synchronized void removeEntry(final String key) {
-        cache.remove(key);
+    protected byte[] restore(final String storageKey) throws ResourceIOException {
+        final Element element = cache.get(storageKey);
+        return element != null ? castAsByteArray(element.getObjectValue()) : null;
     }
 
     @Override
-    public synchronized void updateEntry(
-            final String key, final HttpCacheUpdateCallback callback) throws ResourceIOException, HttpCacheUpdateException {
-        int numRetries = 0;
-        do{
-            final Element oldElement = cache.get(key);
+    protected Element getForUpdateCAS(final String storageKey) throws ResourceIOException {
+        return cache.get(storageKey);
+    }
 
-            HttpCacheEntry existingEntry = null;
-            if(oldElement != null){
-                final byte[] data = (byte[])oldElement.getObjectValue();
-                existingEntry = serializer.readFrom(new ByteArrayInputStream(data));
-            }
+    @Override
+    protected byte[] getStorageObject(final Element element) throws ResourceIOException {
+        return castAsByteArray(element.getObjectValue());
+    }
 
-            final HttpCacheEntry updatedEntry = callback.update(existingEntry);
+    @Override
+    protected boolean updateCAS(final String storageKey, final Element element, final byte[] storageObject) throws ResourceIOException {
+        final Element newElement = new Element(storageKey, storageObject);
+        return cache.replace(element, newElement);
+    }
 
-            if (existingEntry == null) {
-                putEntry(key, updatedEntry);
-                return;
-            } else {
-                // Attempt to do a CAS replace, if we fail then retry
-                // While this operation should work fine within this instance, multiple instances
-                //  could trample each others' data
-                final ByteArrayOutputStream bos = new ByteArrayOutputStream();
-                serializer.writeTo(updatedEntry, bos);
-                final Element newElement = new Element(key, bos.toByteArray());
-                if (cache.replace(oldElement, newElement)) {
-                    return;
-                }else{
-                    numRetries++;
-                }
-            }
-        }while(numRetries <= maxUpdateRetries);
-        throw new HttpCacheUpdateException("Failed to processChallenge");
+    @Override
+    protected void delete(final String storageKey) throws ResourceIOException {
+        cache.remove(storageKey);
     }
+
 }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntry.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntry.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntry.java
deleted file mode 100644
index 769841d..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntry.java
+++ /dev/null
@@ -1,77 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache.memcached;
-
-import org.apache.hc.client5.http.cache.HttpCacheEntry;
-
-/**
- * Provides for serialization and deserialization of higher-level
- * {@link HttpCacheEntry} objects into byte arrays suitable for
- * storage in memcached. Clients wishing to change the serialization
- * mechanism from the provided defaults should implement this
- * interface as well as {@link MemcachedCacheEntryFactory}.
- */
-public interface MemcachedCacheEntry {
-
-    /**
-     * Returns a serialized representation of the current cache entry.
-     * @throws MemcachedSerializationException if serialization fails.
-     *   */
-    byte[] toByteArray() throws MemcachedSerializationException;
-
-    /**
-     * Returns the storage key associated with this entry. May return
-     * {@code null} if this is an "unset" instance waiting to be
-     * {@link #set(byte[])} with a serialized representation.
-     */
-    String getStorageKey();
-
-    /**
-     * Returns the {@link HttpCacheEntry} associated with this entry.
-     * May return {@code null} if this is an "unset" instance
-     * waiting to be {@link #set(byte[])} with a serialized
-     * representation.
-     */
-    HttpCacheEntry getHttpCacheEntry();
-
-    /**
-     * Given a serialized representation of a {@link MemcachedCacheEntry},
-     * attempt to reconstitute the storage key and {@link HttpCacheEntry}
-     * represented therein. After a successful call to this method, this
-     * object should return updated (as appropriate) values for
-     * {@link #getStorageKey()} and {@link #getHttpCacheEntry()}. This
-     * should be viewed as an atomic operation on the
-     * {@code MemcachedCacheEntry}.
-     *
-     * @param bytes serialized representation
-     * @throws MemcachedSerializationException if deserialization
-     *   fails. In this case, the prior values for {{@link #getStorageKey()}
-     *   and {@link #getHttpCacheEntry()} should remain unchanged.
-     */
-    void set(byte[] bytes) throws MemcachedSerializationException ;
-
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntryFactory.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntryFactory.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntryFactory.java
deleted file mode 100644
index 0da3d65..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntryFactory.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache.memcached;
-
-import org.apache.hc.client5.http.cache.HttpCacheEntry;
-
-/**
- * Creates {@link MemcachedCacheEntry} instances that can be used for
- * serializing and deserializing {@link HttpCacheEntry} instances for
- * storage in memcached.
- */
-public interface MemcachedCacheEntryFactory {
-
-    /**
-     * Creates a new {@link MemcachedCacheEntry} for storing the
-     * given {@link HttpCacheEntry} under the given storage key. Since
-     * we are hashing storage keys into cache keys to accommodate
-     * limitations in memcached's key space, it is possible to have
-     * cache collisions. Therefore, we store the storage key along
-     * with the {@code HttpCacheEntry} so it can be compared
-     * on retrieval and thus detect collisions.
-     * @param storageKey storage key under which the entry will
-     *   be logically stored
-     * @param entry the cache entry to store
-     * @return a {@link MemcachedCacheEntry} ready to provide
-     *   a serialized representation
-     */
-    MemcachedCacheEntry getMemcachedCacheEntry(String storageKey, HttpCacheEntry entry);
-
-    /**
-     * Creates an "unset" {@link MemcachedCacheEntry} ready to accept
-     * a serialized representation via {@link MemcachedCacheEntry#set(byte[])}
-     * and deserialize it into a storage key and a {@link HttpCacheEntry}.
-     * @return {@code MemcachedCacheEntry}
-     */
-    MemcachedCacheEntry getUnsetCacheEntry();
-
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntryFactoryImpl.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntryFactoryImpl.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntryFactoryImpl.java
deleted file mode 100644
index 872cb2e..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntryFactoryImpl.java
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache.memcached;
-
-import org.apache.hc.client5.http.cache.HttpCacheEntry;
-
-/**
- * Default implementation of {@link MemcachedCacheEntryFactory}.
- */
-public class MemcachedCacheEntryFactoryImpl implements MemcachedCacheEntryFactory {
-
-    @Override
-    public MemcachedCacheEntry getMemcachedCacheEntry(final String key, final HttpCacheEntry entry) {
-        return new MemcachedCacheEntryImpl(key, entry);
-    }
-
-    @Override
-    public MemcachedCacheEntry getUnsetCacheEntry() {
-        return new MemcachedCacheEntryImpl(null, null);
-    }
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntryImpl.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntryImpl.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntryImpl.java
deleted file mode 100644
index 566e882..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedCacheEntryImpl.java
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache.memcached;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.ObjectInputStream;
-import java.io.ObjectOutputStream;
-
-import org.apache.hc.client5.http.cache.HttpCacheEntry;
-
-/**
- * Default implementation of {@link MemcachedCacheEntry}. This implementation
- * simply uses Java serialization to serialize the storage key followed by
- * the {@link HttpCacheEntry} into a byte array.
- */
-public class MemcachedCacheEntryImpl implements MemcachedCacheEntry {
-
-    private String key;
-    private HttpCacheEntry httpCacheEntry;
-
-    public MemcachedCacheEntryImpl(final String key, final HttpCacheEntry httpCacheEntry) {
-        this.key = key;
-        this.httpCacheEntry = httpCacheEntry;
-    }
-
-    public MemcachedCacheEntryImpl() {
-    }
-
-    /* (non-Javadoc)
-     * @see org.apache.http.impl.client.cache.memcached.MemcachedCacheEntry#toByteArray()
-     */
-    @Override
-    synchronized public byte[] toByteArray() throws MemcachedSerializationException {
-        final ByteArrayOutputStream bos = new ByteArrayOutputStream();
-        final ObjectOutputStream oos;
-        try {
-            oos = new ObjectOutputStream(bos);
-            oos.writeObject(this.key);
-            oos.writeObject(this.httpCacheEntry);
-            oos.close();
-        } catch (final IOException ioe) {
-            throw new MemcachedSerializationException(ioe);
-        }
-        return bos.toByteArray();
-    }
-
-    /* (non-Javadoc)
-     * @see org.apache.http.impl.client.cache.memcached.MemcachedCacheEntry#getKey()
-     */
-    @Override
-    public synchronized String getStorageKey() {
-        return key;
-    }
-
-    /* (non-Javadoc)
-     * @see org.apache.http.impl.client.cache.memcached.MemcachedCacheEntry#getHttpCacheEntry()
-     */
-    @Override
-    public synchronized HttpCacheEntry getHttpCacheEntry() {
-        return httpCacheEntry;
-    }
-
-    /* (non-Javadoc)
-     * @see org.apache.http.impl.client.cache.memcached.MemcachedCacheEntry#set(byte[])
-     */
-    @Override
-    synchronized public void set(final byte[] bytes) throws MemcachedSerializationException {
-        final ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
-        final ObjectInputStream ois;
-        final String s;
-        final HttpCacheEntry entry;
-        try {
-            ois = new ObjectInputStream(bis);
-            s = (String)ois.readObject();
-            entry = (HttpCacheEntry)ois.readObject();
-            ois.close();
-            bis.close();
-        } catch (final IOException | ClassNotFoundException ioe) {
-            throw new MemcachedSerializationException(ioe);
-        }
-        this.key = s;
-        this.httpCacheEntry = entry;
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedHttpCacheStorage.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedHttpCacheStorage.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedHttpCacheStorage.java
index f132b5f..39d209e 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedHttpCacheStorage.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedHttpCacheStorage.java
@@ -29,14 +29,12 @@ package org.apache.hc.client5.http.impl.cache.memcached;
 import java.io.IOException;
 import java.net.InetSocketAddress;
 
-import org.apache.hc.client5.http.cache.HttpCacheEntry;
-import org.apache.hc.client5.http.cache.HttpCacheStorage;
-import org.apache.hc.client5.http.cache.HttpCacheUpdateCallback;
-import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
+import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
 import org.apache.hc.client5.http.cache.ResourceIOException;
+import org.apache.hc.client5.http.impl.cache.AbstractBinaryCacheStorage;
 import org.apache.hc.client5.http.impl.cache.CacheConfig;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
+import org.apache.hc.client5.http.impl.cache.ByteArrayCacheEntrySerializer;
+import org.apache.hc.core5.util.Args;
 
 import net.spy.memcached.CASResponse;
 import net.spy.memcached.CASValue;
@@ -73,15 +71,6 @@ import net.spy.memcached.OperationTimeoutException;
  * </p>
  *
  * <p>
- * Because this hashing scheme can potentially result in key collisions (though
- * highly unlikely), we need to store the higher-level logical storage key along
- * with the {@link HttpCacheEntry} so that we can re-check it on retrieval. There
- * is a default serialization scheme provided for this, although you can provide
- * your own implementations of {@link MemcachedCacheEntry} and
- * {@link MemcachedCacheEntryFactory} to customize this serialization.
- * </p>
- *
- * <p>
  * Please refer to the <a href="http://code.google.com/p/memcached/wiki/NewStart">
  * memcached documentation</a> and in particular to the documentation for
  * the <a href="http://code.google.com/p/spymemcached/">spymemcached
@@ -91,14 +80,10 @@ import net.spy.memcached.OperationTimeoutException;
  *
  * @since 4.1
  */
-public class MemcachedHttpCacheStorage implements HttpCacheStorage {
-
-    private final Logger log = LogManager.getLogger(getClass());
+public class MemcachedHttpCacheStorage extends AbstractBinaryCacheStorage<CASValue<Object>> {
 
     private final MemcachedClientIF client;
     private final KeyHashingScheme keyHashingScheme;
-    private final MemcachedCacheEntryFactory memcachedCacheEntryFactory;
-    private final int maxUpdateRetries;
 
     /**
      * Create a storage backend talking to a <i>memcached</i> instance
@@ -118,8 +103,7 @@ public class MemcachedHttpCacheStorage implements HttpCacheStorage {
      * @param cache client to use for communicating with <i>memcached</i>
      */
     public MemcachedHttpCacheStorage(final MemcachedClientIF cache) {
-        this(cache, CacheConfig.DEFAULT, new MemcachedCacheEntryFactoryImpl(),
-                new SHA256KeyHashingScheme());
+        this(cache, CacheConfig.DEFAULT, ByteArrayCacheEntrySerializer.INSTANCE, SHA256KeyHashingScheme.INSTANCE);
     }
 
     /**
@@ -128,140 +112,83 @@ public class MemcachedHttpCacheStorage implements HttpCacheStorage {
      * mechanisms.
      * @param client how to talk to <i>memcached</i>
      * @param config apply HTTP cache-related options
-     * @param memcachedCacheEntryFactory Factory pattern used for obtaining
-     *   instances of alternative cache entry serialization mechanisms
+     * @param serializer alternative serialization mechanism
      * @param keyHashingScheme how to map higher-level logical "storage keys"
      *   onto "cache keys" suitable for use with memcached
      */
-    public MemcachedHttpCacheStorage(final MemcachedClientIF client, final CacheConfig config,
-            final MemcachedCacheEntryFactory memcachedCacheEntryFactory,
+    public MemcachedHttpCacheStorage(
+            final MemcachedClientIF client,
+            final CacheConfig config,
+            final HttpCacheEntrySerializer<byte[]> serializer,
             final KeyHashingScheme keyHashingScheme) {
-        this.client = client;
-        this.maxUpdateRetries = config.getMaxUpdateRetries();
-        this.memcachedCacheEntryFactory = memcachedCacheEntryFactory;
+        super((config != null ? config : CacheConfig.DEFAULT).getMaxUpdateRetries(),
+                serializer != null ? serializer : ByteArrayCacheEntrySerializer.INSTANCE);
+        this.client = Args.notNull(client, "Memcached client");
         this.keyHashingScheme = keyHashingScheme;
     }
 
     @Override
-    public void putEntry(final String url, final HttpCacheEntry entry) throws ResourceIOException {
-        final byte[] bytes = serializeEntry(url, entry);
-        final String key = getCacheKey(url);
-        if (key == null) {
-            return;
-        }
-        try {
-            client.set(key, 0, bytes);
-        } catch (final OperationTimeoutException ex) {
-            throw new MemcachedOperationTimeoutException(ex);
-        }
+    protected String digestToStorageKey(final String key) {
+        return keyHashingScheme.hash(key);
     }
 
-    private String getCacheKey(final String url) {
+    @Override
+    protected void store(final String storageKey, final byte[] storageObject) throws ResourceIOException {
         try {
-            return keyHashingScheme.hash(url);
-        } catch (final MemcachedKeyHashingException mkhe) {
-            return null;
-        }
-    }
-
-    private byte[] serializeEntry(final String url, final HttpCacheEntry hce) throws ResourceIOException {
-        final MemcachedCacheEntry mce = memcachedCacheEntryFactory.getMemcachedCacheEntry(url, hce);
-        return mce.toByteArray();
-    }
-
-    private byte[] convertToByteArray(final Object o) {
-        if (o == null) {
-            return null;
-        }
-        if (!(o instanceof byte[])) {
-            log.warn("got a non-bytearray back from memcached: " + o);
-            return null;
+            client.set(storageKey, 0, storageObject);
+        } catch (final OperationTimeoutException ex) {
+            throw new MemcachedOperationTimeoutException(ex);
         }
-        return (byte[])o;
     }
 
-    private MemcachedCacheEntry reconstituteEntry(final Object o) {
-        final byte[] bytes = convertToByteArray(o);
-        if (bytes == null) {
+    private byte[] castAsByteArray(final Object storageObject) throws ResourceIOException {
+        if (storageObject == null) {
             return null;
         }
-        final MemcachedCacheEntry mce = memcachedCacheEntryFactory.getUnsetCacheEntry();
-        try {
-            mce.set(bytes);
-        } catch (final MemcachedSerializationException mse) {
-            return null;
+        if (storageObject instanceof byte[]) {
+            return (byte[]) storageObject;
+        } else {
+            throw new ResourceIOException("Unexpected cache content: " + storageObject.getClass());
         }
-        return mce;
     }
 
     @Override
-    public HttpCacheEntry getEntry(final String url) throws ResourceIOException {
-        final String key = getCacheKey(url);
-        if (key == null) {
-            return null;
-        }
+    protected byte[] restore(final String storageKey) throws ResourceIOException {
         try {
-            final MemcachedCacheEntry mce = reconstituteEntry(client.get(key));
-            if (mce == null || !url.equals(mce.getStorageKey())) {
-                return null;
-            }
-            return mce.getHttpCacheEntry();
+            return castAsByteArray(client.get(storageKey));
         } catch (final OperationTimeoutException ex) {
             throw new MemcachedOperationTimeoutException(ex);
         }
     }
 
     @Override
-    public void removeEntry(final String url) throws ResourceIOException {
-        final String key = getCacheKey(url);
-        if (key == null) {
-            return;
-        }
+    protected CASValue<Object> getForUpdateCAS(final String storageKey) throws ResourceIOException {
         try {
-            client.delete(key);
+            return client.gets(storageKey);
         } catch (final OperationTimeoutException ex) {
             throw new MemcachedOperationTimeoutException(ex);
         }
     }
 
     @Override
-    public void updateEntry(final String url, final HttpCacheUpdateCallback callback)
-            throws HttpCacheUpdateException, ResourceIOException {
-        int numRetries = 0;
-        final String key = getCacheKey(url);
-        if (key == null) {
-            throw new HttpCacheUpdateException("couldn't generate cache key");
-        }
-        do {
-            try {
-                final CASValue<Object> v = client.gets(key);
-                MemcachedCacheEntry mce = (v == null) ? null
-                        : reconstituteEntry(v.getValue());
-                if (mce != null && (!url.equals(mce.getStorageKey()))) {
-                    mce = null;
-                }
-                final HttpCacheEntry existingEntry = (mce == null) ? null
-                        : mce.getHttpCacheEntry();
-                final HttpCacheEntry updatedEntry = callback.update(existingEntry);
-
-                if (existingEntry == null) {
-                    putEntry(url, updatedEntry);
-                    return;
+    protected byte[] getStorageObject(final CASValue<Object> casValue) throws ResourceIOException {
+        return castAsByteArray(casValue.getValue());
+    }
 
-                }
-                final byte[] updatedBytes = serializeEntry(url, updatedEntry);
-                final CASResponse casResult = client.cas(key, v.getCas(),
-                        updatedBytes);
-                if (casResult != CASResponse.OK) {
-                    numRetries++;
-                } else {
-                    return;
-                }
-            } catch (final OperationTimeoutException ex) {
-                throw new MemcachedOperationTimeoutException(ex);
-            }
-        } while (numRetries <= maxUpdateRetries);
+    @Override
+    protected boolean updateCAS(
+            final String storageKey, final CASValue<Object> casValue, final byte[] storageObject) throws ResourceIOException {
+        final CASResponse casResult = client.cas(storageKey, casValue.getCas(), storageObject);
+        return casResult == CASResponse.OK;
+    }
 
-        throw new HttpCacheUpdateException("Failed to processChallenge");
+    @Override
+    protected void delete(final String storageKey) throws ResourceIOException {
+        try {
+            client.delete(storageKey);
+        } catch (final OperationTimeoutException ex) {
+            throw new MemcachedOperationTimeoutException(ex);
+        }
     }
+
 }

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedSerializationException.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedSerializationException.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedSerializationException.java
deleted file mode 100644
index 69cea8a..0000000
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/MemcachedSerializationException.java
+++ /dev/null
@@ -1,41 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache.memcached;
-
-import org.apache.hc.client5.http.cache.ResourceIOException;
-
-/**
- * Raised when there is a problem serializing or deserializing cache
- * entries into a byte representation suitable for memcached storage.
- */
-public class MemcachedSerializationException extends ResourceIOException {
-
-    public MemcachedSerializationException(final Throwable cause) {
-        super(cause != null ? cause.getMessage() : null, cause);
-    }
-
-}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/PrefixKeyHashingScheme.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/PrefixKeyHashingScheme.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/PrefixKeyHashingScheme.java
index f6e2231..b7a1fd4 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/PrefixKeyHashingScheme.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/PrefixKeyHashingScheme.java
@@ -33,7 +33,7 @@ package org.apache.hc.client5.http.impl.cache.memcached;
  * Primarily useful for namespacing a shared memcached cluster, for
  * example.
  */
-public class PrefixKeyHashingScheme implements KeyHashingScheme {
+public final class PrefixKeyHashingScheme implements KeyHashingScheme {
 
     private final String prefix;
     private final KeyHashingScheme backingScheme;

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/SHA256KeyHashingScheme.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/SHA256KeyHashingScheme.java b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/SHA256KeyHashingScheme.java
index 0ebf07e..9b1c13f 100644
--- a/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/SHA256KeyHashingScheme.java
+++ b/httpclient5-cache/src/main/java/org/apache/hc/client5/http/impl/cache/memcached/SHA256KeyHashingScheme.java
@@ -40,7 +40,9 @@ import org.apache.logging.log4j.Logger;
  * digests and hence are always 64-character hexadecimal
  * strings.
  */
-public class SHA256KeyHashingScheme implements KeyHashingScheme {
+public final class SHA256KeyHashingScheme implements KeyHashingScheme {
+
+    public static final SHA256KeyHashingScheme INSTANCE = new SHA256KeyHashingScheme();
 
     private final Logger log = LogManager.getLogger(getClass());
 

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpCacheEntryMatcher.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpCacheEntryMatcher.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpCacheEntryMatcher.java
new file mode 100644
index 0000000..7721d89
--- /dev/null
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/HttpCacheEntryMatcher.java
@@ -0,0 +1,100 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.cache;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.Objects;
+
+import org.apache.hc.client5.http.cache.HttpCacheEntry;
+import org.apache.hc.client5.http.cache.Resource;
+import org.apache.hc.client5.http.cache.ResourceIOException;
+import org.apache.hc.core5.http.Header;
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Factory;
+import org.hamcrest.Matcher;
+
+public class HttpCacheEntryMatcher extends BaseMatcher<HttpCacheEntry> {
+
+    private final HttpCacheEntry expectedValue;
+
+    public HttpCacheEntryMatcher(final HttpCacheEntry expectedValue) {
+        this.expectedValue = expectedValue;
+    }
+
+    @Override
+    public boolean matches(final Object item) {
+        if (item instanceof HttpCacheEntry) {
+            try {
+                final HttpCacheEntry otherValue = (HttpCacheEntry) item;
+
+                final int expectedStatus = expectedValue.getStatus();
+                final int otherStatus = otherValue.getStatus();
+                if (expectedStatus != otherStatus) {
+                    return false;
+                }
+                final Date expectedRequestDate = expectedValue.getRequestDate();
+                final Date otherRequestDate = otherValue.getRequestDate();
+                if (!Objects.equals(expectedRequestDate, otherRequestDate)) {
+                    return false;
+                }
+                final Date expectedResponseDate = expectedValue.getResponseDate();
+                final Date otherResponseDate = otherValue.getResponseDate();
+                if (!Objects.equals(expectedResponseDate, otherResponseDate)) {
+                    return false;
+                }
+                final Header[] expectedHeaders = expectedValue.getAllHeaders();
+                final Header[] otherHeaders = otherValue.getAllHeaders();
+                if (!Arrays.deepEquals(expectedHeaders, otherHeaders)) {
+                    return false;
+                }
+                final Resource expectedResource = expectedValue.getResource();
+                final byte[] expectedContent = expectedResource != null ? expectedResource.get() : null;
+                final Resource otherResource = otherValue.getResource();
+                final byte[] otherContent = otherResource != null ? otherResource.get() : null;
+                if (!Arrays.equals(expectedContent, otherContent)) {
+                    return false;
+                }
+            } catch (final ResourceIOException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public void describeTo(final Description description) {
+        description.appendValue(expectedValue);
+    }
+
+    @Factory
+    public static Matcher<HttpCacheEntry> equivalent(final HttpCacheEntry target) {
+        return new HttpCacheEntryMatcher(target);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAbstractSerializingCacheStorage.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAbstractSerializingCacheStorage.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAbstractSerializingCacheStorage.java
new file mode 100644
index 0000000..c52c469
--- /dev/null
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestAbstractSerializingCacheStorage.java
@@ -0,0 +1,256 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.cache;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.apache.hc.client5.http.cache.HttpCacheEntry;
+import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
+import org.apache.hc.client5.http.cache.HttpCacheUpdateCallback;
+import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
+import org.apache.hc.client5.http.cache.ResourceIOException;
+import org.hamcrest.CoreMatchers;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Answers;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+@SuppressWarnings("boxing") // test code
+public class TestAbstractSerializingCacheStorage {
+
+    public static byte[] serialize(final String key, final HttpCacheEntry value) throws ResourceIOException {
+        return ByteArrayCacheEntrySerializer.INSTANCE.serialize(new HttpCacheStorageEntry(key, value));
+    }
+
+    private AbstractBinaryCacheStorage<String> impl;
+
+    @Before
+    @SuppressWarnings("unchecked")
+    public void setUp() {
+        impl = Mockito.mock(AbstractBinaryCacheStorage.class,
+                Mockito.withSettings().defaultAnswer(Answers.CALLS_REAL_METHODS).useConstructor(3));
+    }
+
+    @Test
+    public void testCachePut() throws Exception {
+        final String key = "foo";
+        final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
+
+        when(impl.digestToStorageKey(key)).thenReturn("bar");
+
+        impl.putEntry(key, value);
+
+        final ArgumentCaptor<byte[]> argumentCaptor = ArgumentCaptor.forClass(byte[].class);
+        verify(impl).store(eq("bar"), argumentCaptor.capture());
+        Assert.assertArrayEquals(serialize(key, value), argumentCaptor.getValue());
+    }
+
+    @Test
+    public void testCacheGetNullEntry() throws Exception {
+        final String key = "foo";
+
+        when(impl.digestToStorageKey(key)).thenReturn("bar");
+        when(impl.restore("bar")).thenReturn(null);
+
+        final HttpCacheEntry resultingEntry = impl.getEntry(key);
+
+        verify(impl).restore("bar");
+
+        Assert.assertThat(resultingEntry, CoreMatchers.nullValue());
+    }
+
+    @Test
+    public void testCacheGet() throws Exception {
+        final String key = "foo";
+        final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
+
+        when(impl.digestToStorageKey(key)).thenReturn("bar");
+        when(impl.restore("bar")).thenReturn(serialize(key, value));
+
+        final HttpCacheEntry resultingEntry = impl.getEntry(key);
+
+        verify(impl).restore("bar");
+
+        Assert.assertThat(resultingEntry, HttpCacheEntryMatcher.equivalent(value));
+    }
+
+    @Test
+    public void testCacheGetKeyMismatch() throws Exception {
+        final String key = "foo";
+        final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
+
+        when(impl.digestToStorageKey(key)).thenReturn("bar");
+        when(impl.restore("bar")).thenReturn(serialize("not-foo", value));
+
+        final HttpCacheEntry resultingEntry = impl.getEntry(key);
+
+        verify(impl).restore("bar");
+
+        Assert.assertThat(resultingEntry, CoreMatchers.nullValue());
+    }
+
+    @Test
+    public void testCacheRemove()  throws Exception{
+        final String key = "foo";
+
+        when(impl.digestToStorageKey(key)).thenReturn("bar");
+        impl.removeEntry(key);
+
+        verify(impl).delete("bar");
+    }
+
+    @Test
+    public void testCacheUpdateNullEntry() throws Exception {
+        final String key = "foo";
+        final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
+
+        when(impl.digestToStorageKey(key)).thenReturn("bar");
+        when(impl.getForUpdateCAS("bar")).thenReturn(null);
+
+        impl.updateEntry(key, new HttpCacheUpdateCallback() {
+
+            @Override
+            public HttpCacheEntry update(final HttpCacheEntry existing) throws ResourceIOException {
+                Assert.assertThat(existing, CoreMatchers.nullValue());
+                return updatedValue;
+            }
+
+        });
+
+        verify(impl).getForUpdateCAS("bar");
+        verify(impl).store(Mockito.eq("bar"), Mockito.<byte[]>any());
+    }
+
+    @Test
+    public void testCacheCASUpdate() throws Exception {
+        final String key = "foo";
+        final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
+        final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
+
+        when(impl.digestToStorageKey(key)).thenReturn("bar");
+        when(impl.getForUpdateCAS("bar")).thenReturn("stuff");
+        when(impl.getStorageObject("stuff")).thenReturn(serialize(key, existingValue));
+        when(impl.updateCAS(Mockito.eq("bar"), Mockito.eq("stuff"), Mockito.<byte[]>any())).thenReturn(true);
+
+        impl.updateEntry(key, new HttpCacheUpdateCallback() {
+
+            @Override
+            public HttpCacheEntry update(final HttpCacheEntry existing) throws ResourceIOException {
+                return updatedValue;
+            }
+
+        });
+
+        verify(impl).getForUpdateCAS("bar");
+        verify(impl).getStorageObject("stuff");
+        verify(impl).updateCAS(Mockito.eq("bar"), Mockito.eq("stuff"), Mockito.<byte[]>any());
+    }
+
+    @Test
+    public void testCacheCASUpdateKeyMismatch() throws Exception {
+        final String key = "foo";
+        final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
+        final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
+
+        when(impl.digestToStorageKey(key)).thenReturn("bar");
+        when(impl.getForUpdateCAS("bar")).thenReturn("stuff");
+        when(impl.getStorageObject("stuff")).thenReturn(serialize("not-foo", existingValue));
+        when(impl.updateCAS(Mockito.eq("bar"), Mockito.eq("stuff"), Mockito.<byte[]>any())).thenReturn(true);
+
+        impl.updateEntry(key, new HttpCacheUpdateCallback() {
+
+            @Override
+            public HttpCacheEntry update(final HttpCacheEntry existing) throws ResourceIOException {
+                Assert.assertThat(existing, CoreMatchers.nullValue());
+                return updatedValue;
+            }
+
+        });
+
+        verify(impl).getForUpdateCAS("bar");
+        verify(impl).getStorageObject("stuff");
+        verify(impl).store(Mockito.eq("bar"), Mockito.<byte[]>any());
+    }
+
+    @Test
+    public void testSingleCacheUpdateRetry() throws Exception {
+        final String key = "foo";
+        final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
+        final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
+
+        when(impl.digestToStorageKey(key)).thenReturn("bar");
+        when(impl.getForUpdateCAS("bar")).thenReturn("stuff");
+        when(impl.getStorageObject("stuff")).thenReturn(serialize(key, existingValue));
+        when(impl.updateCAS(Mockito.eq("bar"), Mockito.eq("stuff"), Mockito.<byte[]>any())).thenReturn(false, true);
+
+        impl.updateEntry(key, new HttpCacheUpdateCallback() {
+
+            @Override
+            public HttpCacheEntry update(final HttpCacheEntry existing) throws ResourceIOException {
+                return updatedValue;
+            }
+
+        });
+
+        verify(impl, Mockito.times(2)).getForUpdateCAS("bar");
+        verify(impl, Mockito.times(2)).getStorageObject("stuff");
+        verify(impl, Mockito.times(2)).updateCAS(Mockito.eq("bar"), Mockito.eq("stuff"), Mockito.<byte[]>any());
+    }
+
+    @Test
+    public void testCacheUpdateFail() throws Exception {
+        final String key = "foo";
+        final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
+        final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
+
+        when(impl.digestToStorageKey(key)).thenReturn("bar");
+        when(impl.getForUpdateCAS("bar")).thenReturn("stuff");
+        when(impl.getStorageObject("stuff")).thenReturn(serialize(key, existingValue));
+        when(impl.updateCAS(Mockito.eq("bar"), Mockito.eq("stuff"), Mockito.<byte[]>any())).thenReturn(false, false, false, true);
+
+        try {
+            impl.updateEntry(key, new HttpCacheUpdateCallback() {
+
+                @Override
+                public HttpCacheEntry update(final HttpCacheEntry existing) throws ResourceIOException {
+                    return updatedValue;
+                }
+
+            });
+            Assert.fail("HttpCacheUpdateException expected");
+        } catch (final HttpCacheUpdateException ignore) {
+        }
+
+        verify(impl, Mockito.times(3)).getForUpdateCAS("bar");
+        verify(impl, Mockito.times(3)).getStorageObject("stuff");
+        verify(impl, Mockito.times(3)).updateCAS(Mockito.eq("bar"), Mockito.eq("stuff"), Mockito.<byte[]>any());
+    }
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestByteArrayCacheEntrySerializer.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestByteArrayCacheEntrySerializer.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestByteArrayCacheEntrySerializer.java
new file mode 100644
index 0000000..b391495
--- /dev/null
+++ b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestByteArrayCacheEntrySerializer.java
@@ -0,0 +1,89 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation.  For more
+ * information on the Apache Software Foundation, please see
+ * <http://www.apache.org/>.
+ *
+ */
+package org.apache.hc.client5.http.impl.cache;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+import java.nio.charset.Charset;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.hc.client5.http.cache.HttpCacheEntry;
+import org.apache.hc.client5.http.cache.HttpCacheStorageEntry;
+import org.apache.hc.core5.http.Header;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TestByteArrayCacheEntrySerializer {
+
+    private static final Charset UTF8 = Charset.forName("UTF-8");
+
+    private ByteArrayCacheEntrySerializer impl;
+
+    @Before
+    public void setUp() {
+        impl = new ByteArrayCacheEntrySerializer();
+    }
+
+    @Test
+    public void canSerializeEntriesWithVariantMaps() throws Exception {
+        readWriteVerify(makeCacheEntryWithVariantMap("key"));
+    }
+
+    public void readWriteVerify(final HttpCacheStorageEntry writeEntry) throws Exception {
+        // write the entry
+        final byte[] bytes = impl.serialize(writeEntry);
+        // read the entry
+        final HttpCacheStorageEntry readEntry = impl.deserialize(bytes);
+        // compare
+        assertEquals(readEntry.getKey(), writeEntry.getKey());
+        assertThat(readEntry.getContent(), HttpCacheEntryMatcher.equivalent(writeEntry.getContent()));
+    }
+
+    private HttpCacheStorageEntry makeCacheEntryWithVariantMap(final String key) {
+        final Header[] headers = new Header[5];
+        for (int i = 0; i < headers.length; i++) {
+            headers[i] = new BasicHeader("header" + i, "value" + i);
+        }
+        final String body = "Lorem ipsum dolor sit amet";
+
+        final Map<String,String> variantMap = new HashMap<>();
+        variantMap.put("test variant 1","true");
+        variantMap.put("test variant 2","true");
+        final HttpCacheEntry cacheEntry = new HttpCacheEntry(new Date(), new Date(),
+                HttpStatus.SC_OK, headers,
+                new HeapResource(Base64.decodeBase64(body.getBytes(UTF8))), variantMap);
+
+        return new HttpCacheStorageEntry(key, cacheEntry);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/httpcomponents-client/blob/f215fdcd/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpCacheEntrySerializers.java
----------------------------------------------------------------------
diff --git a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpCacheEntrySerializers.java b/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpCacheEntrySerializers.java
deleted file mode 100644
index e3238ae..0000000
--- a/httpclient5-cache/src/test/java/org/apache/hc/client5/http/impl/cache/TestHttpCacheEntrySerializers.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you 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.
- * ====================================================================
- *
- * This software consists of voluntary contributions made by many
- * individuals on behalf of the Apache Software Foundation.  For more
- * information on the Apache Software Foundation, please see
- * <http://www.apache.org/>.
- *
- */
-package org.apache.hc.client5.http.impl.cache;
-
-import static org.junit.Assert.assertTrue;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.nio.charset.Charset;
-import java.util.Arrays;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.apache.commons.codec.binary.Base64;
-import org.apache.hc.client5.http.cache.HttpCacheEntry;
-import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
-import org.apache.hc.client5.http.cache.Resource;
-import org.apache.hc.core5.http.Header;
-import org.apache.hc.core5.http.HttpStatus;
-import org.apache.hc.core5.http.message.BasicHeader;
-import org.junit.Before;
-import org.junit.Test;
-
-public class TestHttpCacheEntrySerializers {
-
-    private static final Charset UTF8 = Charset.forName("UTF-8");
-
-    private HttpCacheEntrySerializer impl;
-
-    @Before
-    public void setUp() {
-        impl = new DefaultHttpCacheEntrySerializer();
-    }
-
-    @Test
-    public void canSerializeEntriesWithVariantMaps() throws Exception {
-        readWriteVerify(makeCacheEntryWithVariantMap());
-    }
-
-    public void readWriteVerify(final HttpCacheEntry writeEntry) throws IOException {
-        // write the entry
-        final ByteArrayOutputStream out = new ByteArrayOutputStream();
-        impl.writeTo(writeEntry, out);
-
-        // read the entry
-        final ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
-        final HttpCacheEntry readEntry = impl.readFrom(in);
-
-        // compare
-        assertTrue(areEqual(readEntry, writeEntry));
-    }
-
-    private HttpCacheEntry makeCacheEntryWithVariantMap() {
-        final Header[] headers = new Header[5];
-        for (int i = 0; i < headers.length; i++) {
-            headers[i] = new BasicHeader("header" + i, "value" + i);
-        }
-        final String body = "Lorem ipsum dolor sit amet";
-
-        final Map<String,String> variantMap = new HashMap<>();
-        variantMap.put("test variant 1","true");
-        variantMap.put("test variant 2","true");
-        final HttpCacheEntry cacheEntry = new HttpCacheEntry(new Date(), new Date(),
-                HttpStatus.SC_OK, headers,
-                new HeapResource(Base64.decodeBase64(body.getBytes(UTF8))), variantMap);
-
-        return cacheEntry;
-    }
-
-    private boolean areEqual(final HttpCacheEntry one, final HttpCacheEntry two) throws IOException {
-        // dates are only stored with second precision, so scrub milliseconds
-        if (!((one.getRequestDate().getTime() / 1000) == (two.getRequestDate()
-                .getTime() / 1000))) {
-            return false;
-        }
-        if (!((one.getResponseDate().getTime() / 1000) == (two
-                .getResponseDate().getTime() / 1000))) {
-            return false;
-        }
-
-        final byte[] onesByteArray = resourceToBytes(one.getResource());
-        final byte[] twosByteArray = resourceToBytes(two.getResource());
-
-        if (!Arrays.equals(onesByteArray,twosByteArray)) {
-            return false;
-        }
-
-        final Header[] oneHeaders = one.getAllHeaders();
-        final Header[] twoHeaders = two.getAllHeaders();
-        if (!(oneHeaders.length == twoHeaders.length)) {
-            return false;
-        }
-        for (int i = 0; i < oneHeaders.length; i++) {
-            if (!oneHeaders[i].getName().equals(twoHeaders[i].getName())) {
-                return false;
-            }
-            if (!oneHeaders[i].getValue().equals(twoHeaders[i].getValue())) {
-                return false;
-            }
-        }
-
-        return true;
-    }
-
-    private byte[] resourceToBytes(final Resource res) throws IOException {
-        final InputStream inputStream = res.getInputStream();
-        final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
-
-        int readBytes;
-        final byte[] bytes = new byte[8096];
-        while ((readBytes = inputStream.read(bytes)) > 0) {
-            outputStream.write(bytes, 0, readBytes);
-        }
-
-        final byte[] byteData = outputStream.toByteArray();
-
-        inputStream.close();
-        outputStream.close();
-
-        return byteData;
-    }
-}