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

This commit is contained in:
Oleg Kalnichevski 2017-10-17 16:23:51 +02:00
parent f70c974241
commit f215fdcd32
22 changed files with 741 additions and 1744 deletions

View File

@ -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;
}

View File

@ -24,22 +24,33 @@
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache.memcached;
package org.apache.hc.client5.http.cache;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import java.io.Serializable;
/**
* Default implementation of {@link MemcachedCacheEntryFactory}.
*/
public class MemcachedCacheEntryFactoryImpl implements MemcachedCacheEntryFactory {
import org.apache.hc.core5.util.Args;
@Override
public MemcachedCacheEntry getMemcachedCacheEntry(final String key, final HttpCacheEntry entry) {
return new MemcachedCacheEntryImpl(key, entry);
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 MemcachedCacheEntry getUnsetCacheEntry() {
return new MemcachedCacheEntryImpl(null, null);
public String toString() {
return "[key=" + key + "; content=" + content + "]";
}
}

View File

@ -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
*/

View File

@ -24,18 +24,23 @@
* <http://www.apache.org/>.
*
*/
package org.apache.hc.client5.http.impl.cache.memcached;
package org.apache.hc.client5.http.impl.cache;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.client5.http.cache.HttpCacheEntrySerializer;
/**
* Raised when there is a problem serializing or deserializing cache
* entries into a byte representation suitable for memcached storage.
* Abstract cache backend for serialized binary objects capable of CAS (compare-and-swap) updates.
*
* @since 5.0
*/
public class MemcachedSerializationException extends ResourceIOException {
public abstract class AbstractBinaryCacheStorage<CAS> extends AbstractSerializingCacheStorage<byte[], CAS> {
public MemcachedSerializationException(final Throwable cause) {
super(cause != null ? cause.getMessage() : null, cause);
public AbstractBinaryCacheStorage(final int maxUpdateRetries, final HttpCacheEntrySerializer<byte[]> serializer) {
super(maxUpdateRetries, serializer);
}
public AbstractBinaryCacheStorage(final int maxUpdateRetries) {
super(maxUpdateRetries, ByteArrayCacheEntrySerializer.INSTANCE);
}
}

View File

@ -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;
}
}
}
}

View File

@ -26,14 +26,14 @@
*/
package org.apache.hc.client5.http.impl.cache;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
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.HttpCacheStorageEntry;
import org.apache.hc.client5.http.cache.ResourceIOException;
import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.ThreadingBehavior;
@ -47,21 +47,31 @@ import org.apache.hc.core5.annotation.ThreadingBehavior;
* @since 4.1
*/
@Contract(threading = ThreadingBehavior.IMMUTABLE)
public class DefaultHttpCacheEntrySerializer implements HttpCacheEntrySerializer {
public final class ByteArrayCacheEntrySerializer implements HttpCacheEntrySerializer<byte[]> {
public static final ByteArrayCacheEntrySerializer INSTANCE = new ByteArrayCacheEntrySerializer();
@Override
public void writeTo(final HttpCacheEntry cacheEntry, final OutputStream os) throws ResourceIOException {
try (final ObjectOutputStream oos = new ObjectOutputStream(os)) {
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 HttpCacheEntry readFrom(final InputStream is) throws ResourceIOException {
try (final ObjectInputStream ois = new ObjectInputStream(is)) {
return (HttpCacheEntry) ois.readObject();
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);
}

View File

@ -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);
HttpCacheEntry existingEntry = null;
if(oldElement != null){
final byte[] data = (byte[])oldElement.getObjectValue();
existingEntry = serializer.readFrom(new ByteArrayInputStream(data));
}
final HttpCacheEntry updatedEntry = callback.update(existingEntry);
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");
protected Element getForUpdateCAS(final String storageKey) throws ResourceIOException {
return cache.get(storageKey);
}
@Override
protected byte[] getStorageObject(final Element element) throws ResourceIOException {
return castAsByteArray(element.getObjectValue());
}
@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);
}
@Override
protected void delete(final String storageKey) throws ResourceIOException {
cache.remove(storageKey);
}
}

View File

@ -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 ;
}

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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;
}
protected String digestToStorageKey(final String key) {
return keyHashingScheme.hash(key);
}
@Override
protected void store(final String storageKey, final byte[] storageObject) throws ResourceIOException {
try {
client.set(key, 0, bytes);
client.set(storageKey, 0, storageObject);
} catch (final OperationTimeoutException ex) {
throw new MemcachedOperationTimeoutException(ex);
}
}
private String getCacheKey(final String url) {
try {
return keyHashingScheme.hash(url);
} catch (final MemcachedKeyHashingException mkhe) {
private byte[] castAsByteArray(final Object storageObject) throws ResourceIOException {
if (storageObject == null) {
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 (storageObject instanceof byte[]) {
return (byte[]) storageObject;
} else {
throw new ResourceIOException("Unexpected cache content: " + storageObject.getClass());
}
if (!(o instanceof byte[])) {
log.warn("got a non-bytearray back from memcached: " + o);
return null;
}
return (byte[])o;
}
private MemcachedCacheEntry reconstituteEntry(final Object o) {
final byte[] bytes = convertToByteArray(o);
if (bytes == null) {
return null;
}
final MemcachedCacheEntry mce = memcachedCacheEntryFactory.getUnsetCacheEntry();
try {
mce.set(bytes);
} catch (final MemcachedSerializationException mse) {
return null;
}
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;
}
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);
throw new HttpCacheUpdateException("Failed to processChallenge");
protected byte[] getStorageObject(final CASValue<Object> casValue) throws ResourceIOException {
return castAsByteArray(casValue.getValue());
}
@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;
}
@Override
protected void delete(final String storageKey) throws ResourceIOException {
try {
client.delete(storageKey);
} catch (final OperationTimeoutException ex) {
throw new MemcachedOperationTimeoutException(ex);
}
}
}

View File

@ -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;

View File

@ -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());

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -1,247 +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.ehcache;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.io.InputStream;
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.HttpCacheUpdateCallback;
import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
import org.apache.hc.client5.http.impl.cache.CacheConfig;
import org.apache.hc.client5.http.impl.cache.HttpTestUtils;
import org.junit.Test;
import junit.framework.TestCase;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
@SuppressWarnings("boxing") // test code
public class TestEhcacheHttpCacheStorage extends TestCase {
private Ehcache mockCache;
private EhcacheHttpCacheStorage impl;
private HttpCacheEntrySerializer mockSerializer;
@Override
public void setUp() {
mockCache = mock(Ehcache.class);
final CacheConfig config = CacheConfig.custom().setMaxUpdateRetries(1).build();
mockSerializer = mock(HttpCacheEntrySerializer.class);
impl = new EhcacheHttpCacheStorage(mockCache, config, mockSerializer);
}
@Test
public void testCachePut() throws IOException {
final String key = "foo";
final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
final Element e = new Element(key, new byte[]{});
impl.putEntry(key, value);
verify(mockSerializer).writeTo(same(value), isA(OutputStream.class));
verify(mockCache).put(e);;
}
@Test
public void testCacheGetNullEntry() throws IOException {
final String key = "foo";
when(mockCache.get(key)).thenReturn(null);
final HttpCacheEntry resultingEntry = impl.getEntry(key);
verify(mockCache).get(key);
assertNull(resultingEntry);
}
@Test
public void testCacheGet() throws IOException {
final String key = "foo";
final HttpCacheEntry cachedValue = HttpTestUtils.makeCacheEntry();
final Element element = new Element(key, new byte[]{});
when(mockCache.get(key))
.thenReturn(element);
when(mockSerializer.readFrom(isA(InputStream.class)))
.thenReturn(cachedValue);
final HttpCacheEntry resultingEntry = impl.getEntry(key);
verify(mockCache).get(key);
verify(mockSerializer).readFrom(isA(InputStream.class));
assertSame(cachedValue, resultingEntry);
}
@Test
public void testCacheRemove() {
final String key = "foo";
when(mockCache.remove(key)).thenReturn(true);
impl.removeEntry(key);
verify(mockCache).remove(key);
}
@Test
public void testCacheUpdateNullEntry() throws IOException, HttpCacheUpdateException {
final String key = "foo";
final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
final Element element = new Element(key, new byte[]{});
final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback(){
@Override
public HttpCacheEntry update(final HttpCacheEntry old){
assertNull(old);
return updatedValue;
}
};
// get empty old entry
when(mockCache.get(key)).thenReturn(null);
// put new entry
mockSerializer.writeTo(same(updatedValue), isA(OutputStream.class));
impl.updateEntry(key, callback);
verify(mockCache).get(key);
verify(mockSerializer).writeTo(same(updatedValue), isA(OutputStream.class));
verify(mockCache).put(element);
}
@Test
public void testCacheUpdate() throws IOException, HttpCacheUpdateException {
final String key = "foo";
final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
final Element existingElement = new Element(key, new byte[]{});
final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback(){
@Override
public HttpCacheEntry update(final HttpCacheEntry old){
assertEquals(existingValue, old);
return updatedValue;
}
};
// get existing old entry
when(mockCache.get(key)).thenReturn(existingElement);
when(mockSerializer.readFrom(isA(InputStream.class))).thenReturn(existingValue);
// processChallenge
mockSerializer.writeTo(same(updatedValue), isA(OutputStream.class));
when(mockCache.replace(same(existingElement), isA(Element.class))).thenReturn(true);
impl.updateEntry(key, callback);
verify(mockCache).get(key);
verify(mockSerializer).readFrom(isA(InputStream.class));
verify(mockSerializer).writeTo(same(updatedValue), isA(OutputStream.class));
verify(mockCache).replace(same(existingElement), isA(Element.class));
}
@Test
public void testSingleCacheUpdateRetry() throws IOException, HttpCacheUpdateException {
final String key = "foo";
final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
final Element existingElement = new Element(key, new byte[]{});
final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback(){
@Override
public HttpCacheEntry update(final HttpCacheEntry old){
assertEquals(existingValue, old);
return updatedValue;
}
};
// get existing old entry, will happen twice
when(mockCache.get(key)).thenReturn(existingElement);
when(mockSerializer.readFrom(isA(InputStream.class))).thenReturn(existingValue);
// Fail first and then succeed
when(mockCache.replace(same(existingElement), isA(Element.class))).thenReturn(false).thenReturn(true);
impl.updateEntry(key, callback);
verify(mockCache, times(2)).get(key);
verify(mockSerializer, times(2)).readFrom(isA(InputStream.class));
verify(mockSerializer, times(2)).writeTo(same(updatedValue), isA(OutputStream.class));
verify(mockCache, times(2)).replace(same(existingElement), isA(Element.class));
}
@Test
public void testCacheUpdateFail() throws IOException {
final String key = "foo";
final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
final Element existingElement = new Element(key, new byte[]{});
final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback(){
@Override
public HttpCacheEntry update(final HttpCacheEntry old){
assertEquals(existingValue, old);
return updatedValue;
}
};
// get existing old entry
when(mockCache.get(key)).thenReturn(existingElement);
when(mockSerializer.readFrom(isA(InputStream.class))).thenReturn(existingValue);
// processChallenge but fail
when(mockCache.replace(same(existingElement), isA(Element.class))).thenReturn(false);
try{
impl.updateEntry(key, callback);
fail("Expected HttpCacheUpdateException");
} catch (final HttpCacheUpdateException e) { }
verify(mockCache, times(2)).get(key);
verify(mockSerializer, times(2)).readFrom(isA(InputStream.class));
verify(mockSerializer, times(2)).writeTo(same(updatedValue), isA(OutputStream.class));
verify(mockCache, times(2)).replace(same(existingElement), isA(Element.class));
}
}

View File

@ -1,88 +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.ehcache;
import org.apache.hc.client5.http.cache.HttpCacheStorage;
import org.apache.hc.client5.http.classic.ExecChain;
import org.apache.hc.client5.http.impl.cache.CacheConfig;
import org.apache.hc.client5.http.impl.cache.CachingExec;
import org.apache.hc.client5.http.impl.cache.HeapResourceFactory;
import org.apache.hc.client5.http.impl.cache.TestProtocolRequirements;
import org.easymock.EasyMock;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.config.CacheConfiguration;
import net.sf.ehcache.config.Configuration;
import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
public class TestEhcacheProtocolRequirements extends TestProtocolRequirements{
protected final String TEST_EHCACHE_NAME = "TestEhcacheProtocolRequirements-cache";
protected static CacheManager CACHE_MANAGER;
@BeforeClass
public static void setUpGlobal() {
final Configuration config = new Configuration();
config.addDefaultCache(
new CacheConfiguration("default", Integer.MAX_VALUE)
.memoryStoreEvictionPolicy(MemoryStoreEvictionPolicy.LFU)
.overflowToDisk(false));
CACHE_MANAGER = CacheManager.create(config);
}
@Override
@Before
public void setUp() {
super.setUp();
config = CacheConfig.custom().setMaxObjectSize(MAX_BYTES).build();
if (CACHE_MANAGER.cacheExists(TEST_EHCACHE_NAME)){
CACHE_MANAGER.removeCache(TEST_EHCACHE_NAME);
}
CACHE_MANAGER.addCache(TEST_EHCACHE_NAME);
final HttpCacheStorage storage = new EhcacheHttpCacheStorage(CACHE_MANAGER.getCache(TEST_EHCACHE_NAME));
mockExecChain = EasyMock.createNiceMock(ExecChain.class);
impl = new CachingExec(new HeapResourceFactory(), storage, config);
}
@After
public void tearDown(){
CACHE_MANAGER.removeCache(TEST_EHCACHE_NAME);
}
@AfterClass
public static void tearDownGlobal(){
CACHE_MANAGER.shutdown();
}
}

View File

@ -1,48 +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 static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertSame;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.impl.cache.HttpTestUtils;
import org.junit.Test;
public class TestMemcachedCacheEntryFactoryImpl {
@Test
public void createsMemcachedCacheEntryImpls() {
final String key = "key";
final HttpCacheEntry entry = HttpTestUtils.makeCacheEntry();
final MemcachedCacheEntryFactoryImpl impl = new MemcachedCacheEntryFactoryImpl();
final MemcachedCacheEntry result = impl.getMemcachedCacheEntry(key, entry);
assertNotNull(result);
assertSame(key, result.getStorageKey());
assertSame(entry, result.getHttpCacheEntry());
}
}

View File

@ -1,119 +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 static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.impl.cache.DefaultHttpCacheEntrySerializer;
import org.apache.hc.client5.http.impl.cache.HttpTestUtils;
import org.junit.Before;
import org.junit.Test;
public class TestMemcachedCacheEntryImpl {
private MemcachedCacheEntryImpl impl;
private HttpCacheEntry entry;
@Before
public void setUp() {
entry = HttpTestUtils.makeCacheEntry();
impl = new MemcachedCacheEntryImpl("foo", entry);
}
@Test
public void canBeCreatedEmpty() {
impl = new MemcachedCacheEntryImpl();
assertNull(impl.getStorageKey());
assertNull(impl.getHttpCacheEntry());
}
@Test
public void canBeSerialized() throws Exception {
final byte[] bytes = impl.toByteArray();
assertNotNull(bytes);
assertTrue(bytes.length > 0);
}
@Test
public void knowsItsCacheKey() {
assertEquals("foo", impl.getStorageKey());
}
@Test
public void knowsItsCacheEntry() {
assertEquals(entry, impl.getHttpCacheEntry());
}
@Test
public void canBeReconstitutedFromByteArray() throws Exception {
final String key = impl.getStorageKey();
final HttpCacheEntry entry1 = impl.getHttpCacheEntry();
final byte[] bytes = impl.toByteArray();
impl = new MemcachedCacheEntryImpl();
impl.set(bytes);
assertEquals(key, impl.getStorageKey());
assertEquivalent(entry1, impl.getHttpCacheEntry());
}
@Test(expected=MemcachedSerializationException.class)
public void cannotReconstituteFromGarbage() throws Exception {
impl = new MemcachedCacheEntryImpl();
final byte[] bytes = HttpTestUtils.getRandomBytes(128);
impl.set(bytes);
}
private void assertEquivalent(final HttpCacheEntry entry,
final HttpCacheEntry resultEntry) throws IOException {
/* Ugh. Implementing HttpCacheEntry#equals is problematic
* due to the Resource response body (may cause unexpected
* I/O to users). Checking that two entries
* serialize to the same bytes seems simpler, on the whole,
* (while still making for a somewhat yucky test). At
* least we encapsulate it off here in its own method so
* the test that uses it remains clear.
*/
final DefaultHttpCacheEntrySerializer ser = new DefaultHttpCacheEntrySerializer();
final ByteArrayOutputStream bos1 = new ByteArrayOutputStream();
ser.writeTo(entry, bos1);
final byte[] bytes1 = bos1.toByteArray();
final ByteArrayOutputStream bos2 = new ByteArrayOutputStream();
ser.writeTo(resultEntry, bos2);
final byte[] bytes2 = bos2.toByteArray();
assertEquals(bytes1.length, bytes2.length);
for(int i = 0; i < bytes1.length; i++) {
assertEquals(bytes1[i], bytes2[i]);
}
}
}

View File

@ -1,603 +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 static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.io.IOException;
import org.apache.hc.client5.http.cache.HttpCacheEntry;
import org.apache.hc.client5.http.cache.HttpCacheUpdateCallback;
import org.apache.hc.client5.http.cache.HttpCacheUpdateException;
import org.apache.hc.client5.http.impl.cache.CacheConfig;
import org.apache.hc.client5.http.impl.cache.HttpTestUtils;
import org.junit.Before;
import org.junit.Test;
import junit.framework.TestCase;
import net.spy.memcached.CASResponse;
import net.spy.memcached.CASValue;
import net.spy.memcached.MemcachedClientIF;
import net.spy.memcached.OperationTimeoutException;
public class TestMemcachedHttpCacheStorage extends TestCase {
private MemcachedHttpCacheStorage impl;
private MemcachedClientIF mockMemcachedClient;
private KeyHashingScheme mockKeyHashingScheme;
private MemcachedCacheEntryFactory mockMemcachedCacheEntryFactory;
private MemcachedCacheEntry mockMemcachedCacheEntry;
private MemcachedCacheEntry mockMemcachedCacheEntry2;
private MemcachedCacheEntry mockMemcachedCacheEntry3;
private MemcachedCacheEntry mockMemcachedCacheEntry4;
@Override
@Before
public void setUp() throws Exception {
mockMemcachedClient = mock(MemcachedClientIF.class);
mockKeyHashingScheme = mock(KeyHashingScheme.class);
mockMemcachedCacheEntryFactory = mock(MemcachedCacheEntryFactory.class);
mockMemcachedCacheEntry = mock(MemcachedCacheEntry.class);
mockMemcachedCacheEntry2 = mock(MemcachedCacheEntry.class);
mockMemcachedCacheEntry3 = mock(MemcachedCacheEntry.class);
mockMemcachedCacheEntry4 = mock(MemcachedCacheEntry.class);
final CacheConfig config = CacheConfig.custom().setMaxUpdateRetries(1).build();
impl = new MemcachedHttpCacheStorage(mockMemcachedClient, config,
mockMemcachedCacheEntryFactory, mockKeyHashingScheme);
}
@Test
public void testSuccessfulCachePut() throws IOException {
final String url = "foo";
final String key = "key";
final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
final byte[] serialized = HttpTestUtils.getRandomBytes(128);
when(mockMemcachedCacheEntryFactory.getMemcachedCacheEntry(url, value))
.thenReturn(mockMemcachedCacheEntry);
when(mockMemcachedCacheEntry.toByteArray())
.thenReturn(serialized);
when(mockKeyHashingScheme.hash(url))
.thenReturn(key);
when(mockMemcachedClient.set(key, 0, serialized))
.thenReturn(null);
impl.putEntry(url, value);
verify(mockMemcachedCacheEntryFactory).getMemcachedCacheEntry(url, value);
verify(mockMemcachedCacheEntry).toByteArray();
verify(mockKeyHashingScheme).hash(url);
verify(mockMemcachedClient).set(key, 0, serialized);
}
@Test
public void testCachePutFailsSilentlyWhenWeCannotHashAKey() throws IOException {
final String url = "foo";
final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
final byte[] serialized = HttpTestUtils.getRandomBytes(128);
when(mockMemcachedCacheEntryFactory.getMemcachedCacheEntry(url, value))
.thenReturn(mockMemcachedCacheEntry);
when(mockMemcachedCacheEntry.toByteArray())
.thenReturn(serialized);
when(mockKeyHashingScheme.hash(url))
.thenThrow(new MemcachedKeyHashingException(new Exception()));
impl.putEntry(url, value);
verify(mockMemcachedCacheEntryFactory).getMemcachedCacheEntry(url, value);
verify(mockMemcachedCacheEntry).toByteArray();
verify(mockKeyHashingScheme).hash(url);
}
public void testThrowsIOExceptionWhenMemcachedPutTimesOut() throws Exception {
final String url = "foo";
final String key = "key";
final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
final byte[] serialized = HttpTestUtils.getRandomBytes(128);
when(mockMemcachedCacheEntryFactory.getMemcachedCacheEntry(url, value))
.thenReturn(mockMemcachedCacheEntry);
when(mockMemcachedCacheEntry.toByteArray())
.thenReturn(serialized);
when(mockKeyHashingScheme.hash(url))
.thenReturn(key);
when(mockMemcachedClient.set(key, 0, serialized))
.thenThrow(new OperationTimeoutException("timed out"));
try {
impl.putEntry(url, value);
fail("should have thrown exception");
} catch (final IOException expected) {
}
verify(mockMemcachedCacheEntryFactory).getMemcachedCacheEntry(url, value);
verify(mockMemcachedCacheEntry).toByteArray();
verify(mockKeyHashingScheme).hash(url);
verify(mockMemcachedClient).set(key, 0, serialized);
}
@Test
public void testCachePutThrowsIOExceptionIfCannotSerializeEntry() throws Exception {
final String url = "foo";
final String key = "key";
final HttpCacheEntry value = HttpTestUtils.makeCacheEntry();
when(mockMemcachedCacheEntryFactory.getMemcachedCacheEntry(url, value))
.thenReturn(mockMemcachedCacheEntry);
when(mockMemcachedCacheEntry.toByteArray())
.thenThrow(new MemcachedSerializationException(new Exception()));
try {
impl.putEntry(url, value);
fail("should have thrown exception");
} catch (final IOException expected) {
}
verify(mockMemcachedCacheEntryFactory).getMemcachedCacheEntry(url, value);
verify(mockMemcachedCacheEntry).toByteArray();
}
@Test
public void testSuccessfulCacheGet() throws Exception {
final String url = "foo";
final String key = "key";
final byte[] serialized = HttpTestUtils.getRandomBytes(128);
final HttpCacheEntry cacheEntry = HttpTestUtils.makeCacheEntry();
when(mockKeyHashingScheme.hash(url)).thenReturn(key);
when(mockMemcachedClient.get(key)).thenReturn(serialized);
when(mockMemcachedCacheEntryFactory.getUnsetCacheEntry())
.thenReturn(mockMemcachedCacheEntry);
when(mockMemcachedCacheEntry.getStorageKey()).thenReturn(url);
when(mockMemcachedCacheEntry.getHttpCacheEntry()).thenReturn(cacheEntry);
final HttpCacheEntry resultingEntry = impl.getEntry(url);
verify(mockKeyHashingScheme).hash(url);
verify(mockMemcachedClient).get(key);
verify(mockMemcachedCacheEntryFactory).getUnsetCacheEntry();
verify(mockMemcachedCacheEntry).set(serialized);
verify(mockMemcachedCacheEntry).getStorageKey();
verify(mockMemcachedCacheEntry).getHttpCacheEntry();
assertSame(cacheEntry, resultingEntry);
}
@Test
public void testTreatsNoneByteArrayFromMemcachedAsCacheMiss() throws Exception {
final String url = "foo";
final String key = "key";
when(mockKeyHashingScheme.hash(url)).thenReturn(key);
when(mockMemcachedClient.get(key)).thenReturn(new Object());
final HttpCacheEntry resultingEntry = impl.getEntry(url);
verify(mockKeyHashingScheme).hash(url);
verify(mockMemcachedClient).get(key);
assertNull(resultingEntry);
}
@Test
public void testTreatsNullFromMemcachedAsCacheMiss() throws Exception {
final String url = "foo";
final String key = "key";
when(mockKeyHashingScheme.hash(url)).thenReturn(key);
when(mockMemcachedClient.get(key)).thenReturn(null);
final HttpCacheEntry resultingEntry = impl.getEntry(url);
verify(mockKeyHashingScheme).hash(url);
verify(mockMemcachedClient).get(key);
assertNull(resultingEntry);
}
@Test
public void testTreatsAsCacheMissIfCannotReconstituteEntry() throws Exception {
final String url = "foo";
final String key = "key";
final byte[] serialized = HttpTestUtils.getRandomBytes(128);
when(mockKeyHashingScheme.hash(url)).thenReturn(key);
when(mockMemcachedClient.get(key)).thenReturn(serialized);
when(mockMemcachedCacheEntryFactory.getUnsetCacheEntry())
.thenReturn(mockMemcachedCacheEntry);
doThrow(new MemcachedSerializationException(new Exception())).when(mockMemcachedCacheEntry).set(serialized);
assertNull(impl.getEntry(url));
verify(mockKeyHashingScheme).hash(url);
verify(mockMemcachedClient).get(key);
verify(mockMemcachedCacheEntryFactory).getUnsetCacheEntry();
verify(mockMemcachedCacheEntry).set(serialized);
}
@Test
public void testTreatsAsCacheMissIfCantHashStorageKey() throws Exception {
final String url = "foo";
when(mockKeyHashingScheme.hash(url)).thenThrow(new MemcachedKeyHashingException(new Exception()));
assertNull(impl.getEntry(url));
verify(mockKeyHashingScheme).hash(url);
}
@Test
public void testThrowsIOExceptionIfMemcachedTimesOutOnGet() {
final String url = "foo";
final String key = "key";
when(mockKeyHashingScheme.hash(url)).thenReturn(key);
when(mockMemcachedClient.get(key))
.thenThrow(new OperationTimeoutException(""));
try {
impl.getEntry(url);
fail("should have thrown exception");
} catch (final IOException expected) {
}
verify(mockKeyHashingScheme).hash(url);
verify(mockMemcachedClient).get(key);
}
@Test
public void testCacheRemove() throws IOException {
final String url = "foo";
final String key = "key";
when(mockKeyHashingScheme.hash(url)).thenReturn(key);
when(mockMemcachedClient.delete(key)).thenReturn(null);
impl.removeEntry(url);
verify(mockKeyHashingScheme).hash(url);
verify(mockMemcachedClient).delete(key);
}
@Test
public void testCacheRemoveHandlesKeyHashingFailure() throws IOException {
final String url = "foo";
when(mockKeyHashingScheme.hash(url)).thenReturn(null);
impl.removeEntry(url);
verify(mockKeyHashingScheme).hash(url);
}
@Test
public void testCacheRemoveThrowsIOExceptionOnMemcachedTimeout() {
final String url = "foo";
final String key = "key";
when(mockKeyHashingScheme.hash(url)).thenReturn(key);
when(mockMemcachedClient.delete(key))
.thenThrow(new OperationTimeoutException(""));
try {
impl.removeEntry(url);
fail("should have thrown exception");
} catch (final IOException expected) {
}
verify(mockKeyHashingScheme).hash(url);
verify(mockMemcachedClient).delete(key);
}
@Test
public void testCacheUpdateCanUpdateNullEntry() throws IOException,
HttpCacheUpdateException {
final String url = "foo";
final String key = "key";
final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
final byte[] serialized = HttpTestUtils.getRandomBytes(128);
final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() {
@Override
public HttpCacheEntry update(final HttpCacheEntry old) {
assertNull(old);
return updatedValue;
}
};
// get empty old entry
when(mockKeyHashingScheme.hash(url)).thenReturn(key);
when(mockMemcachedClient.gets(key)).thenReturn(null);
when(mockMemcachedCacheEntryFactory.getMemcachedCacheEntry(url, updatedValue))
.thenReturn(mockMemcachedCacheEntry);
when(mockMemcachedCacheEntry.toByteArray()).thenReturn(serialized);
when(
mockMemcachedClient.set(key, 0,
serialized)).thenReturn(null);
impl.updateEntry(url, callback);
verify(mockKeyHashingScheme, times(2)).hash(url);
verify(mockMemcachedClient).gets(key);
verify(mockMemcachedCacheEntryFactory).getMemcachedCacheEntry(url, updatedValue);
verify(mockMemcachedCacheEntry).toByteArray();
verify(mockMemcachedClient).set(key, 0, serialized);
}
@Test
public void testCacheUpdateOverwritesNonMatchingHashCollision() throws IOException,
HttpCacheUpdateException {
final String url = "foo";
final String key = "key";
final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
final byte[] oldBytes = HttpTestUtils.getRandomBytes(128);
final CASValue<Object> casValue = new CASValue<Object>(-1, oldBytes);
final byte[] newBytes = HttpTestUtils.getRandomBytes(128);
final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() {
@Override
public HttpCacheEntry update(final HttpCacheEntry old) {
assertNull(old);
return updatedValue;
}
};
// get empty old entry
when(mockKeyHashingScheme.hash(url)).thenReturn(key);
when(mockMemcachedClient.gets(key)).thenReturn(casValue);
when(mockMemcachedCacheEntryFactory.getUnsetCacheEntry())
.thenReturn(mockMemcachedCacheEntry);
when(mockMemcachedCacheEntry.getStorageKey()).thenReturn("not" + url);
when(mockMemcachedCacheEntryFactory.getMemcachedCacheEntry(url, updatedValue))
.thenReturn(mockMemcachedCacheEntry2);
when(mockMemcachedCacheEntry2.toByteArray()).thenReturn(newBytes);
when(
mockMemcachedClient.set(key, 0,
newBytes)).thenReturn(null);
impl.updateEntry(url, callback);
verify(mockKeyHashingScheme, times(2)).hash(url);
verify(mockMemcachedClient).gets(key);
verify(mockMemcachedCacheEntryFactory).getUnsetCacheEntry();
verify(mockMemcachedCacheEntry).getStorageKey();
verify(mockMemcachedCacheEntryFactory).getMemcachedCacheEntry(url, updatedValue);
verify(mockMemcachedCacheEntry2).toByteArray();
verify(mockMemcachedClient).set(key, 0, newBytes);
}
@Test
public void testCacheUpdateCanUpdateExistingEntry() throws IOException,
HttpCacheUpdateException {
final String url = "foo";
final String key = "key";
final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
final byte[] oldBytes = HttpTestUtils.getRandomBytes(128);
final CASValue<Object> casValue = new CASValue<Object>(1, oldBytes);
final byte[] newBytes = HttpTestUtils.getRandomBytes(128);
final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() {
@Override
public HttpCacheEntry update(final HttpCacheEntry old) {
assertSame(existingValue, old);
return updatedValue;
}
};
// get empty old entry
when(mockKeyHashingScheme.hash(url)).thenReturn(key);
when(mockMemcachedClient.gets(key)).thenReturn(casValue);
when(mockMemcachedCacheEntryFactory.getUnsetCacheEntry())
.thenReturn(mockMemcachedCacheEntry);
when(mockMemcachedCacheEntry.getStorageKey()).thenReturn(url);
when(mockMemcachedCacheEntry.getHttpCacheEntry()).thenReturn(existingValue);
when(mockMemcachedCacheEntryFactory.getMemcachedCacheEntry(url, updatedValue))
.thenReturn(mockMemcachedCacheEntry2);
when(mockMemcachedCacheEntry2.toByteArray()).thenReturn(newBytes);
when(
mockMemcachedClient.cas(key, casValue.getCas(),
newBytes)).thenReturn(CASResponse.OK);
impl.updateEntry(url, callback);
verify(mockKeyHashingScheme).hash(url);
verify(mockMemcachedClient).gets(key);
verify(mockMemcachedCacheEntryFactory).getUnsetCacheEntry();
verify(mockMemcachedCacheEntry).getStorageKey();
verify(mockMemcachedCacheEntry).getHttpCacheEntry();
verify(mockMemcachedCacheEntryFactory).getMemcachedCacheEntry(url, updatedValue);
verify(mockMemcachedCacheEntry2).toByteArray();
verify(mockMemcachedClient).cas(key, casValue.getCas(), newBytes);
}
@Test
public void testCacheUpdateThrowsExceptionsIfCASFailsEnoughTimes() throws IOException {
final String url = "foo";
final String key = "key";
final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
final byte[] oldBytes = HttpTestUtils.getRandomBytes(128);
final CASValue<Object> casValue = new CASValue<Object>(1, oldBytes);
final byte[] newBytes = HttpTestUtils.getRandomBytes(128);
final CacheConfig config = CacheConfig.custom().setMaxUpdateRetries(0).build();
impl = new MemcachedHttpCacheStorage(mockMemcachedClient, config,
mockMemcachedCacheEntryFactory, mockKeyHashingScheme);
final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() {
@Override
public HttpCacheEntry update(final HttpCacheEntry old) {
assertSame(existingValue, old);
return updatedValue;
}
};
// get empty old entry
when(mockKeyHashingScheme.hash(url)).thenReturn(key);
when(mockMemcachedClient.gets(key)).thenReturn(casValue);
when(mockMemcachedCacheEntryFactory.getUnsetCacheEntry())
.thenReturn(mockMemcachedCacheEntry);
when(mockMemcachedCacheEntry.getStorageKey()).thenReturn(url);
when(mockMemcachedCacheEntry.getHttpCacheEntry()).thenReturn(existingValue);
when(mockMemcachedCacheEntryFactory.getMemcachedCacheEntry(url, updatedValue))
.thenReturn(mockMemcachedCacheEntry2);
when(mockMemcachedCacheEntry2.toByteArray()).thenReturn(newBytes);
when(
mockMemcachedClient.cas(key, casValue.getCas(),
newBytes)).thenReturn(CASResponse.EXISTS);
try {
impl.updateEntry(url, callback);
fail("should have thrown exception");
} catch (final HttpCacheUpdateException expected) {
}
verify(mockKeyHashingScheme).hash(url);
verify(mockMemcachedClient).gets(key);
verify(mockMemcachedCacheEntryFactory).getUnsetCacheEntry();
verify(mockMemcachedCacheEntry).getStorageKey();
verify(mockMemcachedCacheEntry).getHttpCacheEntry();
verify(mockMemcachedCacheEntryFactory).getMemcachedCacheEntry(url, updatedValue);
verify(mockMemcachedCacheEntry2).toByteArray();
verify(mockMemcachedClient).cas(key, casValue.getCas(), newBytes);
}
@Test
public void testCacheUpdateCanUpdateExistingEntryWithRetry() throws IOException,
HttpCacheUpdateException {
final String url = "foo";
final String key = "key";
final HttpCacheEntry existingValue = HttpTestUtils.makeCacheEntry();
final HttpCacheEntry existingValue2 = HttpTestUtils.makeCacheEntry();
final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
final HttpCacheEntry updatedValue2 = HttpTestUtils.makeCacheEntry();
final byte[] oldBytes2 = HttpTestUtils.getRandomBytes(128);
final CASValue<Object> casValue2 = new CASValue<Object>(2, oldBytes2);
final byte[] newBytes2 = HttpTestUtils.getRandomBytes(128);
final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() {
@Override
public HttpCacheEntry update(final HttpCacheEntry old) {
if (old == existingValue) {
return updatedValue;
}
assertSame(existingValue2, old);
return updatedValue2;
}
};
when(mockKeyHashingScheme.hash(url)).thenReturn(key);
// take two
when(mockMemcachedClient.gets(key)).thenReturn(casValue2);
when(mockMemcachedCacheEntryFactory.getUnsetCacheEntry())
.thenReturn(mockMemcachedCacheEntry3);
when(mockMemcachedCacheEntry3.getStorageKey()).thenReturn(url);
when(mockMemcachedCacheEntry3.getHttpCacheEntry()).thenReturn(existingValue2);
when(mockMemcachedCacheEntryFactory.getMemcachedCacheEntry(url, updatedValue2))
.thenReturn(mockMemcachedCacheEntry4);
when(mockMemcachedCacheEntry4.toByteArray()).thenReturn(newBytes2);
when(
mockMemcachedClient.cas(key, casValue2.getCas(),
newBytes2)).thenReturn(CASResponse.OK);
impl.updateEntry(url, callback);
verify(mockKeyHashingScheme).hash(url);
verify(mockMemcachedClient).gets(key);
verify(mockMemcachedCacheEntryFactory).getUnsetCacheEntry();
verify(mockMemcachedCacheEntry3).set(oldBytes2);
verify(mockMemcachedCacheEntry3).getStorageKey();
verify(mockMemcachedCacheEntry3).getHttpCacheEntry();
verify(mockMemcachedCacheEntryFactory).getMemcachedCacheEntry(url, updatedValue2);
verify(mockMemcachedCacheEntry4).toByteArray();
verify(mockMemcachedClient).cas(key, casValue2.getCas(), newBytes2);
verifyNoMoreInteractions(mockMemcachedClient);
verifyNoMoreInteractions(mockKeyHashingScheme);
verifyNoMoreInteractions(mockMemcachedCacheEntry);
verifyNoMoreInteractions(mockMemcachedCacheEntry2);
verifyNoMoreInteractions(mockMemcachedCacheEntry3);
verifyNoMoreInteractions(mockMemcachedCacheEntry4);
verifyNoMoreInteractions(mockMemcachedCacheEntryFactory);
}
@Test
public void testUpdateThrowsIOExceptionIfMemcachedTimesOut() throws HttpCacheUpdateException {
final String url = "foo";
final String key = "key";
final HttpCacheEntry updatedValue = HttpTestUtils.makeCacheEntry();
final HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback() {
@Override
public HttpCacheEntry update(final HttpCacheEntry old) {
assertNull(old);
return updatedValue;
}
};
// get empty old entry
when(mockKeyHashingScheme.hash(url)).thenReturn(key);
when(mockMemcachedClient.gets(key))
.thenThrow(new OperationTimeoutException(""));
try {
impl.updateEntry(url, callback);
fail("should have thrown exception");
} catch (final IOException expected) {
}
verify(mockKeyHashingScheme).hash(url);
verify(mockMemcachedClient).gets(key);
}
@Test(expected=HttpCacheUpdateException.class)
public void testThrowsExceptionOnUpdateIfCannotHashStorageKey() throws Exception {
final String url = "foo";
when(mockKeyHashingScheme.hash(url))
.thenThrow(new MemcachedKeyHashingException(new Exception()));
try {
impl.updateEntry(url, null);
fail("should have thrown exception");
} catch (final HttpCacheUpdateException expected) {
}
verify(mockKeyHashingScheme).hash(url);
}
}