Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/ehcache/TestEhcacheHttpCache.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/ehcache/TestEhcacheHttpCache.java (revision 987194) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/ehcache/TestEhcacheHttpCache.java (working copy) @@ -27,12 +27,16 @@ package org.apache.http.impl.client.cache.ehcache; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import junit.framework.TestCase; import net.sf.ehcache.Ehcache; import net.sf.ehcache.Element; import org.apache.http.client.cache.HttpCacheEntry; +import org.apache.http.client.cache.HttpCacheEntrySerializer; +import org.apache.http.client.cache.HttpCacheUpdateCallback; import org.apache.http.impl.client.cache.CacheEntry; import org.easymock.EasyMock; import org.junit.Test; @@ -41,38 +45,67 @@ private Ehcache mockCache; private EhcacheHttpCacheStorage impl; + private HttpCacheEntrySerializer mockSerializer; public void setUp() { mockCache = EasyMock.createMock(Ehcache.class); - impl = new EhcacheHttpCacheStorage(mockCache); + mockSerializer = EasyMock.createMock(HttpCacheEntrySerializer.class); + impl = new EhcacheHttpCacheStorage(mockCache, mockSerializer); } - + + private void replayMocks(){ + EasyMock.replay(mockCache); + EasyMock.replay(mockSerializer); + } + + private void verifyMocks(){ + EasyMock.verify(mockCache); + EasyMock.verify(mockSerializer); + } + @Test public void testCachePut() throws IOException { final String key = "foo"; final HttpCacheEntry value = new CacheEntry(); - - Element e = new Element(key, value); - + + Element e = new Element(key, new byte[]{}); + + mockSerializer.writeTo(EasyMock.same(value), EasyMock.isA(OutputStream.class)); mockCache.put(e); - - EasyMock.replay(mockCache); + + replayMocks(); impl.putEntry(key, value); - EasyMock.verify(mockCache); + verifyMocks(); } @Test - public void testCacheGet() { + public void testCacheGetNullEntry() throws IOException { + final String key = "foo"; + + EasyMock.expect(mockCache.get(key)).andReturn(null); + + replayMocks(); + HttpCacheEntry resultingEntry = impl.getEntry(key); + verifyMocks(); + + assertNull(resultingEntry); + } + + @Test + public void testCacheGet() throws IOException { final String key = "foo"; final HttpCacheEntry cachedValue = new CacheEntry(); - Element element = new Element(key, cachedValue); + + Element element = new Element(key, new byte[]{}); EasyMock.expect(mockCache.get(key)) .andReturn(element); - - EasyMock.replay(mockCache); + EasyMock.expect(mockSerializer.readFrom(EasyMock.isA(InputStream.class))) + .andReturn(cachedValue); + + replayMocks(); HttpCacheEntry resultingEntry = impl.getEntry(key); - EasyMock.verify(mockCache); + verifyMocks(); assertSame(cachedValue, resultingEntry); } @@ -83,9 +116,62 @@ EasyMock.expect(mockCache.remove(key)).andReturn(true); - EasyMock.replay(mockCache); + replayMocks(); impl.removeEntry(key); - EasyMock.verify(mockCache); + verifyMocks(); } - + + @Test + public void testCacheUpdateNullEntry() throws IOException { + final String key = "foo"; + final HttpCacheEntry updatedValue = new CacheEntry(); + + Element element = new Element(key, new byte[]{}); + + HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback(){ + public HttpCacheEntry update(HttpCacheEntry old){ + assertNull(old); + return updatedValue; + } + }; + + // get empty old entry + EasyMock.expect(mockCache.get(key)).andReturn(null); + + // put new entry + mockSerializer.writeTo(EasyMock.same(updatedValue), EasyMock.isA(OutputStream.class)); + mockCache.put(element); + + replayMocks(); + impl.updateEntry(key, callback); + verifyMocks(); + } + + @Test + public void testCacheUpdate() throws IOException { + final String key = "foo"; + final HttpCacheEntry existingValue = new CacheEntry(); + final HttpCacheEntry updatedValue = new CacheEntry(); + + Element existingElement = new Element(key, new byte[]{}); + + HttpCacheUpdateCallback callback = new HttpCacheUpdateCallback(){ + public HttpCacheEntry update(HttpCacheEntry old){ + assertEquals(existingValue, old); + return updatedValue; + } + }; + + // get existing old entry + EasyMock.expect(mockCache.get(key)).andReturn(existingElement); + EasyMock.expect(mockSerializer.readFrom(EasyMock.isA(InputStream.class))).andReturn(existingValue); + + // update + mockSerializer.writeTo(EasyMock.same(updatedValue), EasyMock.isA(OutputStream.class)); + EasyMock.expect(mockCache.replace(EasyMock.same(existingElement), EasyMock.isA(Element.class))).andReturn(true); + + replayMocks(); + impl.updateEntry(key, callback); + verifyMocks(); + } } Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/ehcache/TestEhcacheProtocolRequirements.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/ehcache/TestEhcacheProtocolRequirements.java (revision 0) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/ehcache/TestEhcacheProtocolRequirements.java (revision 0) @@ -0,0 +1,104 @@ +/* + * ==================================================================== + * 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 + * . + * + */ +package org.apache.http.impl.client.cache.ehcache; + +import net.sf.ehcache.CacheManager; +import net.sf.ehcache.config.CacheConfiguration; +import net.sf.ehcache.config.Configuration; +import net.sf.ehcache.store.MemoryStoreEvictionPolicy; + +import org.apache.http.HttpHost; +import org.apache.http.HttpVersion; +import org.apache.http.client.HttpClient; +import org.apache.http.client.cache.HttpCache; +import org.apache.http.client.cache.HttpCacheStorage; +import org.apache.http.impl.client.cache.BasicHttpCache; +import org.apache.http.impl.client.cache.CacheConfig; +import org.apache.http.impl.client.cache.CachingHttpClient; +import org.apache.http.impl.client.cache.HeapResourceFactory; +import org.apache.http.impl.client.cache.HttpTestUtils; +import org.apache.http.impl.client.cache.TestProtocolRequirements; +import org.apache.http.message.BasicHttpRequest; +import org.easymock.classextension.EasyMock; +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; + +public class TestEhcacheProtocolRequirements extends TestProtocolRequirements{ + + private final String TEST_EHCACHE_NAME = "TestEhcacheProtocolRequirements-cache"; + + private static CacheManager CACHE_MANAGER; + + @BeforeClass + public static void setUpGlobal() { + 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() { + host = new HttpHost("foo.example.com"); + + body = HttpTestUtils.makeBody(entityLength); + + request = new BasicHttpRequest("GET", "/foo", HttpVersion.HTTP_1_1); + + originResponse = make200Response(); + + params = new CacheConfig(); + params.setMaxObjectSizeBytes(MAX_BYTES); + + if (CACHE_MANAGER.cacheExists(TEST_EHCACHE_NAME)){ + CACHE_MANAGER.removeCache(TEST_EHCACHE_NAME); + } + CACHE_MANAGER.addCache(TEST_EHCACHE_NAME); + HttpCacheStorage storage = new EhcacheHttpCacheStorage(CACHE_MANAGER.getCache(TEST_EHCACHE_NAME)); + cache = new BasicHttpCache(new HeapResourceFactory(), storage, params); + mockBackend = EasyMock.createMock(HttpClient.class); + mockCache = EasyMock.createMock(HttpCache.class); + + impl = new CachingHttpClient(mockBackend, cache, params); + } + + @After + public void tearDown(){ + CACHE_MANAGER.removeCache(TEST_EHCACHE_NAME); + } + + @AfterClass + public static void tearDownGlobal(){ + CACHE_MANAGER.shutdown(); + } + +} Index: httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestHttpCacheEntrySerializers.java =================================================================== --- httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestHttpCacheEntrySerializers.java (revision 0) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/TestHttpCacheEntrySerializers.java (revision 0) @@ -0,0 +1,116 @@ +package org.apache.http.impl.client.cache; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.Date; +import java.util.HashSet; +import java.util.Set; + +import junit.framework.TestCase; + +import org.apache.commons.codec.binary.Base64; +import org.apache.http.Header; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.client.cache.HttpCacheEntry; +import org.apache.http.client.cache.HttpCacheEntrySerializer; +import org.apache.http.client.cache.Resource; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicStatusLine; + +public class TestHttpCacheEntrySerializers extends TestCase { + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + public void testDefaultSerializer() throws Exception { + readWriteVerify(new DefaultHttpCacheEntrySerializer()); + } + + public void readWriteVerify(HttpCacheEntrySerializer serializer) throws IOException { + // write the entry + HttpCacheEntry writeEntry = newCacheEntry(); + ByteArrayOutputStream out = new ByteArrayOutputStream(); + serializer.writeTo(writeEntry, out); + + // read the entry + ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray()); + HttpCacheEntry readEntry = serializer.readFrom(in); + + // compare + assertTrue(areEqual(readEntry, writeEntry)); + } + + private HttpCacheEntry newCacheEntry() throws UnsupportedEncodingException { + Header[] headers = new Header[5]; + for (int i = 0; i < headers.length; i++) { + headers[i] = new BasicHeader("header" + i, "value" + i); + } + String body = "Lorem ipsum dolor sit amet"; + + ProtocolVersion pvObj = new ProtocolVersion("HTTP", 1, 1); + StatusLine slObj = new BasicStatusLine(pvObj, 200, "ok"); + Set variants = new HashSet(); + variants.add("test variant 1"); + variants.add("test variant 2"); + + HttpCacheEntry cacheEntry = new HttpCacheEntry(new Date(), new Date(), + slObj, headers, new HeapResource(Base64.decodeBase64(body + .getBytes(UTF8.name()))), variants); + + return cacheEntry; + } + + private boolean areEqual(HttpCacheEntry one, 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; + if (!one.getProtocolVersion().equals(two.getProtocolVersion())) + return false; + + byte[] onesByteArray = resourceToBytes(one.getResource()); + byte[] twosByteArray = resourceToBytes(two.getResource()); + + if (!Arrays.equals(onesByteArray,twosByteArray)) + return false; + + Header[] oneHeaders = one.getAllHeaders(); + Header[] twoHeaders = one.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(Resource res) throws IOException { + InputStream inputStream = res.getInputStream(); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + + int readBytes; + byte[] bytes = new byte[8096]; + while ((readBytes = inputStream.read(bytes)) > 0) { + outputStream.write(bytes, 0, readBytes); + } + + byte[] byteData = outputStream.toByteArray(); + + inputStream.close(); + outputStream.close(); + + return byteData; + } +} Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ehcache/EhcacheHttpCacheStorage.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ehcache/EhcacheHttpCacheStorage.java (revision 987194) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ehcache/EhcacheHttpCacheStorage.java (working copy) @@ -26,49 +26,75 @@ */ package org.apache.http.impl.client.cache.ehcache; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import net.sf.ehcache.Ehcache; import net.sf.ehcache.Element; import org.apache.http.client.cache.HttpCacheEntry; +import org.apache.http.client.cache.HttpCacheEntrySerializer; import org.apache.http.client.cache.HttpCacheStorage; import org.apache.http.client.cache.HttpCacheUpdateCallback; +import org.apache.http.impl.client.cache.DefaultHttpCacheEntrySerializer; public class EhcacheHttpCacheStorage implements HttpCacheStorage { private final Ehcache cache; - + private final HttpCacheEntrySerializer serializer; + public EhcacheHttpCacheStorage(Ehcache cache) { - this.cache = cache; + this(cache, new DefaultHttpCacheEntrySerializer()); } + + public EhcacheHttpCacheStorage(Ehcache cache, HttpCacheEntrySerializer serializer){ + this.cache = cache; + this.serializer = serializer; + } public synchronized void putEntry(String key, HttpCacheEntry entry) throws IOException { - cache.put(new Element(key, entry)); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + serializer.writeTo(entry, bos); + cache.put(new Element(key, bos.toByteArray())); } - public synchronized HttpCacheEntry getEntry(String url) { - Element e = cache.get(url); - return (e != null) ? (HttpCacheEntry)e.getValue() : null; + public synchronized HttpCacheEntry getEntry(String key) throws IOException { + Element e = cache.get(key); + if(e == null){ + return null; + } + + byte[] data = (byte[])e.getValue(); + return serializer.readFrom(new ByteArrayInputStream(data)); } - public synchronized void removeEntry(String url) { - cache.remove(url); + public synchronized void removeEntry(String key) { + cache.remove(key); } public synchronized void updateEntry(String key, HttpCacheUpdateCallback callback) - throws IOException { - Element e = cache.get(key); - HttpCacheEntry existingEntry = (e != null) ? (HttpCacheEntry)e.getValue() : null; + throws IOException { + Element oldElement = cache.get(key); + + HttpCacheEntry existingEntry = null; + if(oldElement != null){ + byte[] data = (byte[])oldElement.getValue(); + existingEntry = serializer.readFrom(new ByteArrayInputStream(data)); + } + HttpCacheEntry updatedEntry = callback.update(existingEntry); - - if (e == null) { + + if (existingEntry == null) { putEntry(key, updatedEntry); } else { // Attempt to do a CAS replace, if we fail throw an IOException for now // While this operation should work fine within this instance, multiple instances // could trample each others' data - if (!cache.replace(e, new Element(key, updatedEntry))) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + serializer.writeTo(updatedEntry, bos); + Element newElement = new Element(key, bos.toByteArray()); + if (!cache.replace(oldElement, newElement)) { throw new IOException(); } } Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/DefaultHttpCacheEntrySerializer.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/DefaultHttpCacheEntrySerializer.java (revision 0) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/DefaultHttpCacheEntrySerializer.java (revision 0) @@ -0,0 +1,146 @@ +/* + * ==================================================================== + * 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 + * . + * + */ +package org.apache.http.impl.client.cache; + +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.util.Date; +import java.util.Set; + +import org.apache.http.Header; +import org.apache.http.NameValuePair; +import org.apache.http.ProtocolVersion; +import org.apache.http.StatusLine; +import org.apache.http.annotation.Immutable; +import org.apache.http.client.cache.HttpCacheEntry; +import org.apache.http.client.cache.HttpCacheEntrySerializer; +import org.apache.http.client.cache.Resource; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.message.BasicStatusLine; + +/** + * {@link HttpCacheEntrySerializer} implementation that uses the default (native) + * serialization. + * + * @see java.io.Serializable + * + * @since 4.1 + */ +@Immutable +public class DefaultHttpCacheEntrySerializer implements HttpCacheEntrySerializer { + + /** + * + * @param cacheEntry + * @param os + * @throws IOException + */ + public void writeTo(HttpCacheEntry cacheEntry, OutputStream os) throws IOException { + + ObjectOutputStream oos = null; + try { + oos = new ObjectOutputStream(os); + + oos.writeObject(cacheEntry.getRequestDate()); + oos.writeObject(cacheEntry.getResponseDate()); + + // workaround to nonserializable BasicStatusLine object + // TODO: can change to directly serialize once new httpcore is released + oos.writeObject(cacheEntry.getStatusLine().getProtocolVersion()); + oos.writeObject(cacheEntry.getStatusLine().getStatusCode()); + oos.writeObject(cacheEntry.getStatusLine().getReasonPhrase()); + + // workaround to nonserializable BasicHeader object + // TODO: can change to directly serialize once new httpcore is released + Header[] headers = cacheEntry.getAllHeaders(); + NameValuePair[] headerNvps = new NameValuePair[headers.length]; + for(int i=0; i variants = (Set)ois.readObject(); + + return new HttpCacheEntry(requestDate, responseDate, statusLine, headers, resource, variants); + } catch (ClassNotFoundException cnfe) { + // CacheEntry should be known, it not we have a runtime issue + throw new RuntimeException(cnfe); + } finally { + if (ois != null) { + ois.close(); + } + is.close(); + } + + } + +} Index: httpclient-cache/src/main/java/org/apache/http/client/cache/HttpCacheEntrySerializer.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/client/cache/HttpCacheEntrySerializer.java (revision 0) +++ httpclient-cache/src/main/java/org/apache/http/client/cache/HttpCacheEntrySerializer.java (revision 0) @@ -0,0 +1,11 @@ +package org.apache.http.client.cache; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public interface HttpCacheEntrySerializer { + public void writeTo(HttpCacheEntry entry, OutputStream os) throws IOException; + + public HttpCacheEntry readFrom(InputStream is) throws IOException; +}