Author: bleny Date: 2010-08-04 17:32:36 +0200 (Wed, 04 Aug 2010) New Revision: 228 Url: http://nuiton.org/repositories/revision/wikitty/228 Log: #793 implemented wikitty cache with copy-on-write proxies Added: trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyCopyOnWrite.java trunk/wikitty-api/src/test/java/org/nuiton/wikitty/cache/ trunk/wikitty-api/src/test/java/org/nuiton/wikitty/cache/WikittyServiceCachedTest.java Modified: trunk/wikitty-api/src/main/java/org/nuiton/wikitty/Wikitty.java trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyCache.java trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyImpl.java trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyServiceCached.java trunk/wikitty-api/src/test/java/org/nuiton/wikitty/api/CommonTest.java Modified: trunk/wikitty-api/src/main/java/org/nuiton/wikitty/Wikitty.java =================================================================== --- trunk/wikitty-api/src/main/java/org/nuiton/wikitty/Wikitty.java 2010-08-04 12:52:36 UTC (rev 227) +++ trunk/wikitty-api/src/main/java/org/nuiton/wikitty/Wikitty.java 2010-08-04 15:32:36 UTC (rev 228) @@ -7,7 +7,7 @@ import java.util.List; import java.util.Set; -public interface Wikitty { +public interface Wikitty extends Cloneable { void addPropertyChangeListener(PropertyChangeListener listener); @@ -135,5 +135,7 @@ void setFqField(String fieldName, Object value); boolean isEmpty(); + + Wikitty clone() throws CloneNotSupportedException; } \ No newline at end of file Modified: trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyCache.java =================================================================== --- trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyCache.java 2010-08-04 12:52:36 UTC (rev 227) +++ trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyCache.java 2010-08-04 15:32:36 UTC (rev 228) @@ -40,6 +40,7 @@ static private Log log = LogFactory.getLog(WikittyCache.class); + /** keys are wikitty ids */ protected Map<String, Wikitty> wikittyCache; /** Added: trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyCopyOnWrite.java =================================================================== --- trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyCopyOnWrite.java (rev 0) +++ trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyCopyOnWrite.java 2010-08-04 15:32:36 UTC (rev 228) @@ -0,0 +1,247 @@ +package org.nuiton.wikitty; + +import java.beans.PropertyChangeListener; +import java.math.BigDecimal; +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** this class wrap a wikitty in a proxy that copy the wikitty before applying any change + * + * It is used for caching purpose. This class own a reference to an actual + * wikitty, multiple instance of this class can own some references to a same + * actual wikitty. To prevent the target to be modified, a copy is created + * to prevent side-effect. + * + * So, when a wikitty restored from cache is modified, a copy is modified, so + * if change are cancelled, next restore will restore the original and not + * the modified version (until the modified version is stored). + * + * used in {@link WikittyServiceCached} + */ +public class WikittyCopyOnWrite implements Wikitty { + + private static final Log log = LogFactory.getLog(WikittyCopyOnWrite.class); + + protected Wikitty target; + + /** only WikittyService with cache should create instances */ + protected WikittyCopyOnWrite(Wikitty target) { + this.target = target; + } + + /** replace {@link #target} with a clone + * + * this method must be called to prevent any modification on target + */ + protected void substituteTargetWithCopy() { + try { + target = target.clone(); + if (log.isTraceEnabled()) { + log.trace(this + " now has for target " + target); + } + } catch (CloneNotSupportedException e) { + log.error("unable to clone wikitty " + target, e); + } + } + + public boolean equals(Object obj) { + // FIXME 20100804 bleny side-effect ? + return target.equals(obj); + } + + public int hashCode() { + // FIXME 20100804 bleny side-effect ? + return target.hashCode(); + } + + public String toString() { + return "[" + WikittyCopyOnWrite.class.getName() + "]" + target.toString(); + } + + @Override + public Wikitty clone() throws CloneNotSupportedException { + // return a clone of the target + return target.clone(); + } + + + + /* ** below are only delegation code with copy-on-write */ + + public void addPropertyChangeListener(PropertyChangeListener listener) { + target.addPropertyChangeListener(listener); + } + + public void removePropertyChangeListener(PropertyChangeListener listener) { + target.removePropertyChangeListener(listener); + } + + public void addPropertyChangeListener(String propertyName, + PropertyChangeListener listener) { + target.addPropertyChangeListener(propertyName, listener); + } + + public void removePropertyChangeListener(String propertyName, + PropertyChangeListener listener) { + target.removePropertyChangeListener(propertyName, listener); + } + + public String getId() { + return target.getId(); + } + + public boolean isDeleted() { + return target.isDeleted(); + } + + public Date getDeleteDate() { + return target.getDeleteDate(); + } + + public void setDeleteDate(Date delete) { + target.setDeleteDate(delete); + } + + public void addExtension(WikittyExtension ext) { + substituteTargetWithCopy(); + target.addExtension(ext); + } + + public void addExtension(List<WikittyExtension> exts) { + substituteTargetWithCopy(); + target.addExtension(exts); + } + + public boolean hasExtension(String extName) { + return target.hasExtension(extName); + } + + public boolean hasField(String extName, String fieldName) { + return target.hasField(extName, fieldName); + } + + public WikittyExtension getExtension(String ext) { + return target.getExtension(ext); + } + + public Collection<String> getExtensionNames() { + return target.getExtensionNames(); + } + + public Collection<WikittyExtension> getExtensions() { + return target.getExtensions(); + } + + public Collection<WikittyExtension> getExtensionDependencies(String ext, + boolean recursively) { + return target.getExtensionDependencies(ext, recursively); + } + + public FieldType getFieldType(String fqfieldName) { + return target.getFieldType(fqfieldName); + } + + public void setField(String ext, String fieldName, Object value) { + substituteTargetWithCopy(); + target.setField(ext, fieldName, value); + } + + public Object getFieldAsObject(String ext, String fieldName) { + return target.getFieldAsObject(ext, fieldName); + } + + public boolean getFieldAsBoolean(String ext, String fieldName) { + return target.getFieldAsBoolean(ext, fieldName); + } + + public BigDecimal getFieldAsBigDecimal(String ext, String fieldName) { + return target.getFieldAsBigDecimal(ext, fieldName); + } + + public int getFieldAsInt(String ext, String fieldName) { + return target.getFieldAsInt(ext, fieldName); + } + + public long getFieldAsLong(String ext, String fieldName) { + return target.getFieldAsLong(ext, fieldName); + } + + public float getFieldAsFloat(String ext, String fieldName) { + return target.getFieldAsFloat(ext, fieldName); + } + + public double getFieldAsDouble(String ext, String fieldName) { + return target.getFieldAsDouble(ext, fieldName); + } + + public String getFieldAsString(String ext, String fieldName) { + return target.getFieldAsString(ext, fieldName); + } + + public Date getFieldAsDate(String ext, String fieldName) { + return target.getFieldAsDate(ext, fieldName); + } + + public String getFieldAsWikitty(String ext, String fieldName) { + return target.getFieldAsWikitty(ext, fieldName); + } + + public <E> List<E> getFieldAsList(String ext, String fieldName, + Class<E> clazz) { + return target.getFieldAsList(ext, fieldName, clazz); + } + + public <E> Set<E> getFieldAsSet(String ext, String fieldName, Class<E> clazz) { + return target.getFieldAsSet(ext, fieldName, clazz); + } + + public void addToField(String ext, String fieldName, Object value) { + substituteTargetWithCopy(); + target.addToField(ext, fieldName, value); + } + + public void removeFromField(String ext, String fieldName, Object value) { + substituteTargetWithCopy(); + target.removeFromField(ext, fieldName, value); + } + + public void clearField(String ext, String fieldName) { + substituteTargetWithCopy(); + target.clearField(ext, fieldName); + } + + public Set<String> fieldNames() { + return target.fieldNames(); + } + + public Object getFqField(String fqFieldName) { + return target.getFqField(fqFieldName); + } + + public String getVersion() { + return target.getVersion(); + } + + public void setVersion(String version) { + substituteTargetWithCopy(); + target.setVersion(version); + } + + public void clearDirty() { + target.clearDirty(); + } + + public void setFqField(String fieldName, Object value) { + target.setFqField(fieldName, value); + } + + public boolean isEmpty() { + return target.isEmpty(); + } + +} Modified: trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyImpl.java =================================================================== --- trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyImpl.java 2010-08-04 12:52:36 UTC (rev 227) +++ trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyImpl.java 2010-08-04 15:32:36 UTC (rev 228) @@ -42,7 +42,7 @@ * Last update: $Date$ * by : $Author$ */ -public class WikittyImpl implements Serializable, Wikitty { +public class WikittyImpl implements Serializable, Wikitty, Cloneable { /** serialVersionUID. */ private static final long serialVersionUID = 4910886672760691052L; @@ -769,4 +769,16 @@ return str; } + @Override + public WikittyImpl clone() throws CloneNotSupportedException { + WikittyImpl clone = (WikittyImpl) super.clone(); + // deep copy of field and values + clone.fieldValue = new HashMap<String, Object>(); + for (Map.Entry<String, Object> aFieldValue : fieldValue.entrySet()) { + clone.fieldValue.put(aFieldValue.getKey(), aFieldValue.getValue()); + } + clone.clearDirty(); + return clone; + } + } Modified: trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyServiceCached.java =================================================================== --- trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyServiceCached.java 2010-08-04 12:52:36 UTC (rev 227) +++ trunk/wikitty-api/src/main/java/org/nuiton/wikitty/WikittyServiceCached.java 2010-08-04 15:32:36 UTC (rev 228) @@ -39,7 +39,7 @@ * by : $Author$ */ public class WikittyServiceCached implements WikittyService { - + /** to use log facility, just put in your code: log.info(\"...\"); */ static private Log log = LogFactory.getLog(WikittyServiceCached.class); @@ -290,12 +290,14 @@ if (result != null) { cache.putWikitty(result); } - } - else { + } else { if (log.isTraceEnabled()) { log.trace("Use cached wikitty " + id); } } + + // wrap wikitty to prevent conflict in cache modification + result = new WikittyCopyOnWrite(result); return result; } @@ -308,7 +310,7 @@ @Override public List<Wikitty> restore(String securityToken, List<String> ids) { ArrayList<String> notInCache = new ArrayList<String>(); - // linked to maintains the ordre + // linked to maintains the order LinkedHashMap<String, Wikitty> fromCache = new LinkedHashMap<String, Wikitty>(); for (String id : ids) { @@ -330,7 +332,12 @@ } Collection<Wikitty> tmp = fromCache.values(); - ArrayList<Wikitty> result = new ArrayList<Wikitty>(tmp); + + // wrap the resulting wikitties to prevent cache conflicts + ArrayList<Wikitty> result = new ArrayList<Wikitty>(); + for (Wikitty w : tmp) { + result.add(new WikittyCopyOnWrite(w)); + } return result; } @@ -368,8 +375,13 @@ } Collection<Wikitty> tmp = fromCache.values(); - ArrayList<Wikitty> result = new ArrayList<Wikitty>(tmp); + // wrap the resulting wikitties to prevent cache conflicts + ArrayList<Wikitty> result = new ArrayList<Wikitty>(); + for (Wikitty w : tmp) { + result.add(new WikittyCopyOnWrite(w)); + } + return result; } Modified: trunk/wikitty-api/src/test/java/org/nuiton/wikitty/api/CommonTest.java =================================================================== --- trunk/wikitty-api/src/test/java/org/nuiton/wikitty/api/CommonTest.java 2010-08-04 12:52:36 UTC (rev 227) +++ trunk/wikitty-api/src/test/java/org/nuiton/wikitty/api/CommonTest.java 2010-08-04 15:32:36 UTC (rev 228) @@ -308,7 +308,7 @@ m.put(StorageTest.EXTNAME + ".fieldName1", intValue); Date dateValue = new Date(); - m.put(StorageTest.EXTNAME + ".fieldName2", new Date()); + m.put(StorageTest.EXTNAME + ".fieldName2", dateValue); } long timeSetM = System.currentTimeMillis() - time; Added: trunk/wikitty-api/src/test/java/org/nuiton/wikitty/cache/WikittyServiceCachedTest.java =================================================================== --- trunk/wikitty-api/src/test/java/org/nuiton/wikitty/cache/WikittyServiceCachedTest.java (rev 0) +++ trunk/wikitty-api/src/test/java/org/nuiton/wikitty/cache/WikittyServiceCachedTest.java 2010-08-04 15:32:36 UTC (rev 228) @@ -0,0 +1,91 @@ +package org.nuiton.wikitty.cache; + + +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Before; +import org.junit.Test; +import org.nuiton.wikitty.ExtensionFactory; +import org.nuiton.wikitty.FieldType.TYPE; +import org.nuiton.wikitty.Wikitty; +import org.nuiton.wikitty.WikittyExtension; +import org.nuiton.wikitty.WikittyImpl; +import org.nuiton.wikitty.WikittyService; +import org.nuiton.wikitty.WikittyServiceCached; +import org.nuiton.wikitty.WikittyServiceInMemory; + +/** check that the cache */ +public class WikittyServiceCachedTest { + + /** a wikitty service (in memory) with a cache */ + protected WikittyService service; + + protected static final String EXT_NAME = "myextension"; + protected static final String FIELD_NAME = "myfield"; + + /** an extension */ + protected WikittyExtension extension; + + /** a wikitty with extension */ + protected Wikitty aWikitty; + + protected String token; + + /** create a service, an extension, a wikitty, login, and store wikitty */ + @Before + public void setUp() throws Exception { + service = new WikittyServiceCached(new WikittyServiceInMemory()); + token = service.login(null, null); + + + extension = ExtensionFactory.create(EXT_NAME, "1") + .addField(FIELD_NAME, TYPE.STRING) + .extension(); + aWikitty = new WikittyImpl(); + aWikitty.addExtension(extension); + aWikitty.setField(EXT_NAME, FIELD_NAME, "myvalue"); + service.store(token, aWikitty); + } + + /** setting a field value doesn't corrupt cache */ + @Test + public void testRestore() throws Exception { + Wikitty anotherWikitty = service.restore(token, aWikitty.getId()); + + // we set the value of a field + anotherWikitty.setField(EXT_NAME, FIELD_NAME, "myothervalue"); + + // now let's suppose, the user cancel its modification + // we don't have call store() + anotherWikitty = service.restore(token, anotherWikitty.getId()); + + // the remaining wikitty should hold old value + assertEquals("myvalue", anotherWikitty.getFieldAsString(EXT_NAME, FIELD_NAME)); + } + + /** same as testRestore() using methods that restore multiple ids */ + @Test + public void testRestoreMultipleIds() throws Exception { + // now, let's do the same test, just by using others restore() available + List<String> idsToRestore = new ArrayList<String>(); + idsToRestore.add(aWikitty.getId()); + + List<Wikitty> otherWikitties = service.restore(token, idsToRestore); + Wikitty anotherWikitty = otherWikitties.get(0); + + // we set the value of a field + anotherWikitty.setField(EXT_NAME, FIELD_NAME, "myothervalue"); + + // now let's suppose, the user cancel its modification + // we don't have call store() + otherWikitties = service.restore(token, idsToRestore); + anotherWikitty = otherWikitties.get(0); + + // the remaining wikitty should hold old value + assertEquals("myvalue", anotherWikitty.getFieldAsString(EXT_NAME, FIELD_NAME)); + } + +}