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 986475) +++ httpclient-cache/src/test/java/org/apache/http/impl/client/cache/ehcache/TestEhcacheHttpCache.java (working copy) @@ -27,12 +27,15 @@ 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.impl.client.cache.CacheEntry; import org.easymock.EasyMock; import org.junit.Test; @@ -41,38 +44,54 @@ private Ehcache mockCache; private EhcacheHttpCache impl; + private HttpCacheEntrySerializer mockSerializer; public void setUp() { mockCache = EasyMock.createMock(Ehcache.class); - impl = new EhcacheHttpCache(mockCache); + mockSerializer = EasyMock.createMock(HttpCacheEntrySerializer.class); + impl = new EhcacheHttpCache(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 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 +102,8 @@ EasyMock.expect(mockCache.remove(key)).andReturn(true); - EasyMock.replay(mockCache); + replayMocks(); impl.removeEntry(key); - EasyMock.verify(mockCache); + verifyMocks(); } - } 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 testJSONSerializer() throws Exception { + readWriteVerify(new JSONHttpCacheEntrySerializer()); + } + + 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/EhcacheHttpCache.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ehcache/EhcacheHttpCache.java (revision 986475) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/ehcache/EhcacheHttpCache.java (working copy) @@ -26,6 +26,8 @@ */ 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; @@ -33,23 +35,38 @@ import org.apache.http.client.cache.HttpCache; 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.JSONHttpCacheEntrySerializer; public class EhcacheHttpCache implements HttpCache { private final Ehcache cache; - + private final HttpCacheEntrySerializer serializer; + public EhcacheHttpCache(Ehcache cache) { - this.cache = cache; + this(cache, new JSONHttpCacheEntrySerializer()); } + + public EhcacheHttpCache(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) { + public synchronized HttpCacheEntry getEntry(String url) throws IOException { Element e = cache.get(url); - return (e != null) ? (HttpCacheEntry)e.getValue() : null; + if(e == null){ + return null; + } + + byte[] data = (byte[])e.getValue(); + return serializer.readFrom(new ByteArrayInputStream(data)); } public synchronized void removeEntry(String url) { @@ -57,18 +74,19 @@ } 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 { + HttpCacheEntry existingEntry = getEntry(key); 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))) { + Element oldElement = new Element(key, existingEntry); + Element newElement = new Element(key, updatedEntry); + if (!cache.replace(oldElement, newElement)) { throw new IOException(); } } Index: httpclient-cache/src/main/java/org/apache/http/impl/client/cache/JSONHttpCacheEntrySerializer.java =================================================================== --- httpclient-cache/src/main/java/org/apache/http/impl/client/cache/JSONHttpCacheEntrySerializer.java (revision 0) +++ httpclient-cache/src/main/java/org/apache/http/impl/client/cache/JSONHttpCacheEntrySerializer.java (revision 0) @@ -0,0 +1,249 @@ +package org.apache.http.impl.client.cache; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +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.impl.cookie.DateParseException; +import org.apache.http.impl.cookie.DateUtils; +import org.apache.http.message.BasicHeader; +import org.apache.http.message.BasicStatusLine; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.codehaus.jettison.json.JSONWriter; + +public class JSONHttpCacheEntrySerializer implements HttpCacheEntrySerializer { + private static final Charset UTF8 = Charset.forName("UTF-8"); + + private static final String REQUEST_DATE = "requestDate"; + private static final String RESPONSE_DATE = "responseDate"; + private static final String STATUS_LINE = "StatusLine"; + private static final String PROTOCOL_VERSION = "protocol"; + private static final String PROTOCOL_NAME = "protocolName"; + private static final String PROTOCOL_MAJOR = "protocolMajor"; + private static final String PROTOCOL_MINOR = "protocolMinor"; + private static final String STATUS = "status"; + private static final String REASON = "reason"; + private static final String HEADERS = "headers"; + private static final String BODY = "body"; + private static final String VARIANTS = "variants"; + + public void writeTo(HttpCacheEntry cacheEntry, OutputStream os) + throws IOException { + OutputStreamWriter w = null; + try { + w = new OutputStreamWriter(os, UTF8); + + JSONWriter jw = new JSONWriter(w); + jw.object(); + jw.key(REQUEST_DATE).value( + DateUtils.formatDate(cacheEntry.getRequestDate())); + jw.key(RESPONSE_DATE).value( + DateUtils.formatDate(cacheEntry.getResponseDate())); + + // Start encoding StatusLine Object + jw.key(STATUS_LINE); + jw.object(); + + // Start encoding ProtocolVersion Object + jw.key(PROTOCOL_VERSION); + jw.object(); + jw.key(PROTOCOL_MAJOR).value( + cacheEntry.getProtocolVersion().getMajor()); + jw.key(PROTOCOL_MINOR).value( + cacheEntry.getProtocolVersion().getMinor()); + jw.key(PROTOCOL_NAME).value( + cacheEntry.getProtocolVersion().getProtocol()); + jw.endObject(); + // End ProtocolVersion Object + + jw.key(STATUS).value(cacheEntry.getStatusCode()); + jw.key(REASON).value(cacheEntry.getReasonPhrase()); + jw.endObject(); + // End StatusLine Object + + // Start encoding headers + jw.key(HEADERS); + jw.array(); + Header[] headers = cacheEntry.getAllHeaders(); + for (Header h : headers) { + jw.object(); + jw.key(h.getName()).value(h.getValue()); + jw.endObject(); + } + jw.endArray(); + // End encoding headers + + jw.key(BODY).value( + new String(Base64.encodeBase64(resourceToBytes(cacheEntry + .getResource())), UTF8.name())); + + // Start encoding Variants URIs + jw.key(VARIANTS); + jw.array(); + for (String var : cacheEntry.getVariantURIs()) { + jw.value(var); + + } + jw.endArray(); + // End Variant URIs + + jw.endObject(); + } catch (JSONException jsone) { + // if we are doing JSON wrong we have a runtime issue + throw new RuntimeException(jsone); + } finally { + w.close(); + os.close(); + } + } + + public HttpCacheEntry readFrom(InputStream is) throws IOException { + // TODO: Update this to use a streaming API + InputStreamReader r = null; + try { + r = new InputStreamReader(is, UTF8); + StringBuffer sb = new StringBuffer(); + for (int i = r.read(); i != -1; i = r.read()) { + sb.append((char) i); + } + JSONObject json = new JSONObject(sb.toString()); + return buildCacheEntry(json); + } catch (JSONException jsone) { + // if we are doing JSON wrong we have a runtime issue + throw new RuntimeException(jsone); + } finally { + r.close(); + is.close(); + } + } + + private HttpCacheEntry buildCacheEntry(JSONObject json) + throws JSONException { + HttpCacheEntry cacheEntry = new HttpCacheEntry( + buildDate(REQUEST_DATE, json), + buildDate(RESPONSE_DATE, json), + buildStatusLine(STATUS_LINE, json), + buildHeaders(HEADERS, json), + buildBody(BODY, json), + buildVariants(VARIANTS, json)); + + return cacheEntry; + } + + private Date buildDate(String fieldName, JSONObject json) + throws JSONException { + String dateField = json.optString(fieldName); + try { + return null != dateField ? DateUtils.parseDate(dateField) : null; + } catch (DateParseException e) { + throw new RuntimeException(e); + } + } + + private ProtocolVersion buildProtocolVersion(String fieldName, + JSONObject json) throws JSONException { + JSONObject protocolVersionJSONObj = json.optJSONObject(fieldName); + + if(protocolVersionJSONObj != null){ + return new ProtocolVersion( + protocolVersionJSONObj.getString(PROTOCOL_NAME), + protocolVersionJSONObj.getInt(PROTOCOL_MAJOR), + protocolVersionJSONObj.getInt(PROTOCOL_MINOR)); + } else { + return null; + } + } + + private StatusLine buildStatusLine(String fieldName, JSONObject json) + throws JSONException { + JSONObject statusLineJSONObj = json.optJSONObject(fieldName); + + if(statusLineJSONObj != null){ + return new BasicStatusLine( + buildProtocolVersion(PROTOCOL_VERSION, statusLineJSONObj), + statusLineJSONObj.getInt(STATUS), + statusLineJSONObj.getString(REASON)); + } else { + return null; + } + } + + private Header[] buildHeaders(String fieldName, JSONObject json) + throws JSONException { + JSONArray headersJSON = json.optJSONArray(fieldName); + if (null == headersJSON) + return null; + Header[] headers = new Header[headersJSON.length()]; + for (int i = 0; i < headersJSON.length(); i++) { + JSONObject headerJSON = headersJSON.getJSONObject(i); + for (Iterator iter = headerJSON.keys(); iter.hasNext();) { + String name = (String) iter.next(); + String value = headerJSON.getString(name); + headers[i] = new BasicHeader(name, value); + } + } + + return headers; + } + + 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; + } + + private Resource buildBody(String fieldName, JSONObject json) + throws JSONException { + String base64 = json.getString(fieldName); + try { + return null != base64 ? new HeapResource(Base64.decodeBase64(base64 + .getBytes(UTF8.name()))) : null; + } catch (UnsupportedEncodingException usee) { + // unsupported UTF-8 is a runtime exception + throw new RuntimeException(usee); + } + } + + private Set buildVariants(String fieldName, JSONObject json) + throws JSONException { + JSONArray variantsJSON = json.optJSONArray(fieldName); + if (null == variantsJSON) + return null; + Set variants = new HashSet(); + for (int i = 0; i < variantsJSON.length(); i++) { + variants.add(variantsJSON.getString(i)); + } + return variants; + } +} 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; +} Index: httpclient-cache/pom.xml =================================================================== --- httpclient-cache/pom.xml (revision 986475) +++ httpclient-cache/pom.xml (working copy) @@ -80,6 +80,12 @@ test + org.codehaus.jettison + jettison + 1.2 + true + + net.sf.ehcache ehcache-core ${ehcache.version}