r6968 - in dumbhippo/trunk: openfire/src/plugins/hippo/src/java/com/dumbhippo/jive server/src/com/dumbhippo/dm server/src/com/dumbhippo/dm/annotations server/src/com/dumbhippo/dm/fetch server/src/com/dumbhippo/dm/parser server/src/com/dumbhippo/dm/schema server/src/com/dumbhippo/dm/store server/tests server/tests/com/dumbhippo/dm server/tests/com/dumbhippo/dm/dm server/tests/com/dumbhippo/dm/persistence
- From: commits mugshot org
- To: online-desktop-list gnome org
- Subject: r6968 - in dumbhippo/trunk: openfire/src/plugins/hippo/src/java/com/dumbhippo/jive server/src/com/dumbhippo/dm server/src/com/dumbhippo/dm/annotations server/src/com/dumbhippo/dm/fetch server/src/com/dumbhippo/dm/parser server/src/com/dumbhippo/dm/schema server/src/com/dumbhippo/dm/store server/tests server/tests/com/dumbhippo/dm server/tests/com/dumbhippo/dm/dm server/tests/com/dumbhippo/dm/persistence
- Date: Tue, 4 Dec 2007 23:14:21 -0600 (CST)
Author: otaylor
Date: 2007-12-04 23:14:19 -0600 (Tue, 04 Dec 2007)
New Revision: 6968
Added:
dumbhippo/trunk/server/src/com/dumbhippo/dm/CachedFeed.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/DMFeed.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/DMFeedItem.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/FeedWrapper.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/FeedPropertyHolder.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/store/FeedLog.java
dumbhippo/trunk/server/tests/com/dumbhippo/dm/AbstractFetchTests.java
dumbhippo/trunk/server/tests/com/dumbhippo/dm/FeedTests.java
dumbhippo/trunk/server/tests/com/dumbhippo/dm/dm/TestBlogEntryDMO.java
dumbhippo/trunk/server/tests/com/dumbhippo/dm/dm/TestBlogEntryKey.java
dumbhippo/trunk/server/tests/com/dumbhippo/dm/persistence/TestBlogEntry.java
dumbhippo/trunk/server/tests/feed-fetch-tests.xml
Modified:
dumbhippo/trunk/openfire/src/plugins/hippo/src/java/com/dumbhippo/jive/XmppFetchVisitor.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/ChangeNotification.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/ChangeNotificationSet.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/ClientNotification.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/ClientNotificationSet.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/DMSession.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/ReadOnlySession.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/ReadWriteSession.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/annotations/DMProperty.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/annotations/PropertyType.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/Fetch.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/FetchAttributeType.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/FetchVisitor.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/PropertyFetch.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/PropertyFetchNode.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchLexer.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchParser.g
dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchParser.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchParserTokenTypes.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/DMClassHolder.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/DMPropertyHolder.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/ResourcePropertyHolder.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/store/DMStore.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/store/Registration.java
dumbhippo/trunk/server/src/com/dumbhippo/dm/store/StoreNode.java
dumbhippo/trunk/server/tests/com/dumbhippo/dm/AllTests.java
dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchParserTests.java
dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchResultHandler.java
dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchResultProperty.java
dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchResultVisitor.java
dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchTests.java
dumbhippo/trunk/server/tests/com/dumbhippo/dm/TestSupport.java
dumbhippo/trunk/server/tests/com/dumbhippo/dm/dm/TestUserDMO.java
Log:
FetchParserTests: Test 'property(max=N)'
DMFeed: Class representing a feed
DMFeedItem: Class representing a single feed item.
FeedWrapper: DMFeed wrapper that takes a raw feed and adds filtering and caching
CachedFeed: Class used for storing feed items in the data model cache
DMPropertyHolder FeedPropertyHolder: Add support for feed-valued properties
DMClassHolder: Add getFeedPropertyIndex(), getFeedPropertyCount(); used to
store information about feed-properties of the class in arrays.
DMSesssion ReadOnlySession ReadWriteSession DMClassHolder: Add createFeedWrapper(),
use to implement wrapper getters for feed properties.
DMProperty DMPropertyHolder: add defaultMatchFetch attribute
Fetch PropertyFetch: Take property children and attributes into account when
merging two fetches.
FetchAtttributeTypes: Add 'max' attribute
FetchParser.g: Fix parsing of integer attributes. Add 'max' attribute
PropertyFetchNode PropertyFetch: add 'max' attribute
Fetch: Track maximum values and timestamps when fetch-visiting feeds.
FeedLog: Class with log of changes to a feed so we can tell what items we
need to send to which client
DMStore StoreNode Registration: Keep track of a per-feed-property log and
and a per-feed-property-per-client tx-timestamp-of-the-last-fetch.
ReadWriteSession: Add feedChanged()
ClientNotificationSet ClientNotification ChangeNotificationSet ChangeNotification:
track timestamps for feed changes
FetchVisitor FetchResultVisitor XmppFetchVisitor: add feedProperty() for
adding feeds items to the fetch result.
FetchResultHandler FetchResultProperty: Support feeds.
AbstractFetchTests FetchTests: Move support infrastructure for tests of
fetching into an abstract base class.
TestBlogEntry TestBlogEntryDMO TestBlogEntryKey TestUserDMO TestSupport:
Add TestUser.blogEntries into our test data model to have a feed-valued
properties.
FeedTests feed-fetch-tests.xml AllTests: Tests for feed functionality
Modified: dumbhippo/trunk/openfire/src/plugins/hippo/src/java/com/dumbhippo/jive/XmppFetchVisitor.java
===================================================================
--- dumbhippo/trunk/openfire/src/plugins/hippo/src/java/com/dumbhippo/jive/XmppFetchVisitor.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/openfire/src/plugins/hippo/src/java/com/dumbhippo/jive/XmppFetchVisitor.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -29,6 +29,7 @@
private static final QName RESOURCE_ID_QNAME = QName.get("resourceId", SYSTEM_NAMESPACE);
private static final QName FETCH_QNAME = QName.get("fetch", SYSTEM_NAMESPACE);
private static final QName INDIRECT_QNAME = QName.get("indirect", SYSTEM_NAMESPACE);
+ private static final QName TS_QNAME = QName.get("ts", SYSTEM_NAMESPACE);
private static final QName TYPE_QNAME = QName.get("type", SYSTEM_NAMESPACE);
private static final QName DEFAULT_CHILDREN_QNAME = QName.get("defaultChildren", SYSTEM_NAMESPACE);
private static final QName UPDATE_QNAME = QName.get("update", SYSTEM_NAMESPACE);
@@ -78,10 +79,10 @@
seenProperties.clear();
}
- private Element addPropertyElement(DMPropertyHolder propertyHolder) {
+ private Element addPropertyElement(DMPropertyHolder propertyHolder, boolean incremental) {
Element element = currentResourceElement.addElement(createQName(propertyHolder.getName(), propertyHolder.getNameSpace()));
- if (seenProperties.contains(propertyHolder)) {
+ if (incremental || seenProperties.contains(propertyHolder)) {
element.addAttribute(UPDATE_QNAME, "add");
} else {
element.addAttribute(TYPE_QNAME, propertyHolder.getTypeString());
@@ -98,18 +99,27 @@
}
public void plainProperty(PlainPropertyHolder propertyHolder, Object value) {
- Element element = addPropertyElement(propertyHolder);
+ Element element = addPropertyElement(propertyHolder, false);
element.addText(value.toString());
}
public <KP, TP extends DMObject<KP>> void resourceProperty(ResourcePropertyHolder<?, ?, KP, TP> propertyHolder, KP key) {
- Element element = addPropertyElement(propertyHolder);
+ Element element = addPropertyElement(propertyHolder, false);
DMClassHolder<KP,TP> classHolder = propertyHolder.getResourceClassHolder();
element.addAttribute(RESOURCE_ID_QNAME, classHolder.makeRelativeId(key));
}
+
+ public <KP, TP extends DMObject<KP>> void feedProperty(ResourcePropertyHolder<?, ?, KP, TP> propertyHolder, KP key, long timestamp, boolean incremental) {
+ Element element = addPropertyElement(propertyHolder, incremental);
+
+ DMClassHolder<KP,TP> classHolder = propertyHolder.getResourceClassHolder();
+ element.addAttribute(RESOURCE_ID_QNAME, classHolder.makeRelativeId(key));
+ element.addAttribute(TS_QNAME, Long.toString(timestamp));
+ }
+
public void emptyProperty(DMPropertyHolder propertyHolder) {
Element element = currentResourceElement.addElement(createQName(propertyHolder.getName(), propertyHolder.getNameSpace()));
Added: dumbhippo/trunk/server/src/com/dumbhippo/dm/CachedFeed.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/CachedFeed.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/CachedFeed.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -0,0 +1,127 @@
+package com.dumbhippo.dm;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * This class acts as a store for information about a single feed inside the
+ * data model cache.
+ *
+ * The way that concurrency works is that as long as two different threads
+ * have the same view of what items are in the feed, and as long as items are
+ * always added from the most recent item back without gaps, then both threads
+ * can fill items into the cache at the same time and it will work fine ... duplicates
+ * will be eliminated and at any point, we'll see the cache having the first
+ * max(M,N) items filled ... where M is the number filled by thread 1, and N
+ * is the number filled by thread 2.
+ *
+ * (The code here does support adding items out of order, and then sorts the items
+ * into order, but the concurrency will no longer work right, since a reader will
+ * think that the N items in the cache at any point of time are the *first*
+ * N items.)
+ */
+public class CachedFeed<K> {
+ private DMFeedItem items[] = new DMFeedItem[10];
+ private Map<K, Integer> itemMap = new HashMap<K, Integer>();
+ int itemCount = 0;
+ int maxFetched = 0;
+
+ private DMFeedItem<K> removeItem(int pos) {
+ @SuppressWarnings("unchecked")
+ DMFeedItem<K> item = items[pos];
+
+ System.arraycopy(items, pos + 1, items, pos, itemCount - (pos + 1));
+ itemCount--;
+
+ return item;
+ }
+
+ /**
+ * Add an item into the cache; the item is inserted at a position
+ * corresponding to its timestmap. If the items is already in the cache
+ * it will be restacked with the new timestamp. (Restacking and out
+ * of order insertion are not used currently, since we always start from
+ * a an empty cache and fill it in order. If we did support mutating
+ * the cache as the feed changed, then considerably more work would
+ * have to be put into concurrency issues, possibly including locking
+ * the cache across the entire cluster.)
+ */
+ public synchronized void addItem(K value, long timestamp) {
+ Integer oldPos = itemMap.get(value);
+ DMFeedItem<K> item;
+
+ if (oldPos != null) {
+ item = removeItem(oldPos);
+ item.time = timestamp;
+ } else {
+ item = new DMFeedItem<K>(value, timestamp);
+ }
+
+ int pos = itemCount;
+ while (pos > 0 && timestamp >= items[pos -1].time)
+ pos--;
+
+ if (pos >= items.length) {
+ int newLength = items.length * 2;
+ if (newLength < 0)
+ throw new OutOfMemoryError();
+ DMFeedItem newItems[] = new DMFeedItem[newLength];
+ System.arraycopy(items, 0, newItems, 0, itemCount);
+ items = newItems;
+ }
+
+ if (pos < itemCount)
+ System.arraycopy(items, pos, items, pos + 1, itemCount - pos);
+
+ items[pos] = new DMFeedItem<K>(value, timestamp);
+ itemCount++;
+
+ itemMap.put(value, pos);
+ }
+
+ /**
+ * Get an item from the cached feed store.
+ *
+ * @param pos position at which to get the item. 0 is the most recent
+ * item in the feed.
+ */
+ @SuppressWarnings("unchecked")
+ public synchronized DMFeedItem<K> getItem(int pos) {
+ return items[pos];
+ }
+
+ /**
+ * Get the total number of items in the store.
+ */
+ public synchronized int size() {
+ return itemCount;
+ }
+
+ /**
+ * Get the number of items that would have been stored in the cached
+ * feed store if they were present in feed.
+ *
+ * We need to track this separately from the actual number of stored
+ * items so that we don't repeatedly query the database for items that
+ * aren't actually there.
+ */
+ public synchronized int getMaxFetched() {
+ return maxFetched;
+ }
+
+ /**
+ * Increase the value returned by getMaxFetched(). A thread fetching
+ * items from the feed and caching them in the cached feed store would
+ * call this, after storing all the items it fetched, with the number
+ * of items from the feed that would have been stored if the feed
+ * had all the items it asked for.
+ *
+ * @newMaxFetched the number of items that were attempted to be fetched
+ * (this includes previously fetched items, so if the thread fetched
+ * with start=15 and max=10, it would pass 25 for this parameter)
+ */
+ public synchronized void addToMaxfetched(int newMaxFetched) {
+ if (newMaxFetched > maxFetched)
+ maxFetched = newMaxFetched;
+ }
+}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/ChangeNotification.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/ChangeNotification.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/ChangeNotification.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -6,6 +6,8 @@
import com.dumbhippo.GlobalSetup;
import com.dumbhippo.dm.schema.DMClassHolder;
+import com.dumbhippo.dm.schema.DMPropertyHolder;
+import com.dumbhippo.dm.schema.FeedPropertyHolder;
/**
* ChangeNotification represents pending notifications for a single resource.
@@ -18,18 +20,20 @@
private static Logger logger = GlobalSetup.getLogger(ChangeNotification.class);
- private Class<T> clazz;
+ private DMClassHolder<K, T> classHolder;
private K key;
private long propertyMask; // bitset
private ClientMatcher matcher;
- public ChangeNotification(Class<T> clazz, K key) {
- this.clazz = clazz;
+ private long feedTimestamps[];
+
+ public ChangeNotification(DMClassHolder<K,T> classHolder, K key) {
+ this.classHolder = classHolder;
this.key = key;
}
- public ChangeNotification(Class<T> clazz, K key, ClientMatcher matcher) {
- this.clazz = clazz;
+ public ChangeNotification(DMClassHolder<K,T> classHolder, K key, ClientMatcher matcher) {
+ this.classHolder = classHolder;
this.key = key;
this.matcher = matcher;
}
@@ -38,24 +42,58 @@
this.propertyMask |= 1 << propertyIndex;
}
- public long getPropertyMask() {
- return propertyMask;
+ public void addProperty(String propertyName) {
+ int propertyIndex = classHolder.getPropertyIndex(propertyName);
+ if (propertyIndex < 0)
+ throw new RuntimeException("Class " + classHolder.getBaseClass().getName() + " has no property " + propertyName);
+
+ DMPropertyHolder<K,T,?> property = classHolder.getProperty(propertyIndex);
+ if (property instanceof FeedPropertyHolder)
+ throw new RuntimeException("For feed-valued-properties, you must use session.feedChanged()");
+
+ addProperty(propertyIndex);
}
+ public void addFeedProperty(String propertyName, long itemTimestamp) {
+ int propertyIndex = classHolder.getPropertyIndex(propertyName);
+ if (propertyIndex < 0)
+ throw new RuntimeException("Class " + classHolder.getBaseClass().getName() + " has no property " + propertyName);
+
+ addProperty(propertyIndex);
+
+ DMPropertyHolder<K,T,?> property = classHolder.getProperty(propertyIndex);
+ if (!(property instanceof FeedPropertyHolder))
+ throw new RuntimeException("session.feedChanged() for a non-feed-valued property");
+
+ int feedPropertyIndex = classHolder.getFeedPropertyIndex(property.getName());
+
+ if (feedTimestamps == null) {
+ feedTimestamps = new long[classHolder.getFeedPropertiesCount()];
+ for (int i = 0; i < feedTimestamps.length; i++)
+ feedTimestamps[i] = Long.MAX_VALUE;
+ }
+
+ if (feedTimestamps[feedPropertyIndex] > itemTimestamp)
+ feedTimestamps[feedPropertyIndex] = itemTimestamp;
+ }
+
public void invalidate(DataModel model, long timestamp) {
- @SuppressWarnings("unchecked")
- DMClassHolder<K,T> classHolder = (DMClassHolder<K,T>)model.getClassHolder(clazz);
-
long v = propertyMask;
int propertyIndex = 0;
while (v != 0) {
if ((v & 1) != 0) {
- model.getStore().invalidate(classHolder, key, propertyIndex, timestamp);
+ DMPropertyHolder<K, T, ?> property = classHolder.getProperty(propertyIndex);
+ if (property instanceof FeedPropertyHolder) {
+ int feedPropertyIndex = classHolder.getFeedPropertyIndex(property.getName());
+ model.getStore().invalidateFeed(classHolder, key, propertyIndex, timestamp, feedTimestamps[feedPropertyIndex]);
+ } else
+ model.getStore().invalidate(classHolder, key, propertyIndex, timestamp);
+
logger.debug("Invalidated {}#{}.{}", new Object[] {
classHolder.getBaseClass().getSimpleName(),
key,
- classHolder.getProperty(propertyIndex).getName() });
+ property.getName() });
}
propertyIndex++;
@@ -64,9 +102,6 @@
}
public void resolveNotifications(DataModel model, ClientNotificationSet result) {
- @SuppressWarnings("unchecked")
- DMClassHolder<K,T> classHolder = (DMClassHolder<K,T>)model.getClassHolder(clazz);
-
model.getStore().resolveNotifications(classHolder, key, propertyMask, result, matcher);
}
@@ -77,16 +112,16 @@
ChangeNotification<?,?> other = (ChangeNotification<?,?>)o;
- return clazz == other.clazz && key.equals(other.key);
+ return classHolder == other.classHolder && key.equals(other.key);
}
@Override
public int hashCode() {
- return 11 * clazz.hashCode() + 17 * key.hashCode();
+ return 11 * classHolder.hashCode() + 17 * key.hashCode();
}
@Override
public String toString() {
- return clazz.getSimpleName() + "#" + key.toString();
+ return classHolder.getBaseClass().getSimpleName() + "#" + key.toString();
}
}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/ChangeNotificationSet.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/ChangeNotificationSet.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/ChangeNotificationSet.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -33,13 +33,7 @@
public ChangeNotificationSet(DataModel model) {
}
- public <K, T extends DMObject<K>> void changed(DataModel model, Class<T> clazz, K key, String propertyName, ClientMatcher matcher) {
- @SuppressWarnings("unchecked")
- DMClassHolder<K,T> classHolder = (DMClassHolder<K,T>)model.getClassHolder(clazz);
- int propertyIndex = classHolder.getPropertyIndex(propertyName);
- if (propertyIndex < 0)
- throw new RuntimeException("Class " + clazz.getName() + " has no property " + propertyName);
-
+ private <K, T extends DMObject<K>> ChangeNotification<K,T> getNotification(DMClassHolder<K,T> classHolder, K key, ClientMatcher matcher) {
if (key instanceof DMKey) {
@SuppressWarnings("unchecked")
K clonedKey = (K)((DMKey)key).clone();
@@ -50,26 +44,42 @@
if (matchedNotifications == null)
matchedNotifications = new ArrayList<ChangeNotification<?,?>>();
- ChangeNotification<?,?> notification = new ChangeNotification<K,T>(clazz, key, matcher);
- notification.addProperty(propertyIndex);;
+ ChangeNotification<K,T> notification = new ChangeNotification<K,T>(classHolder, key, matcher);
matchedNotifications.add(notification);
+ return notification;
} else {
if (notifications == null)
notifications = new HashMap<ChangeNotification<?,?>, ChangeNotification<?,?>>();
- ChangeNotification<?,?> notification = new ChangeNotification<K,T>(clazz, key);
- ChangeNotification<?,?> oldNotification = notifications.get(notification);
+ ChangeNotification<K,T> notification = new ChangeNotification<K,T>(classHolder, key);
+ @SuppressWarnings("unchecked")
+ ChangeNotification<K,T> oldNotification = (ChangeNotification<K,T>)notifications.get(notification);
if (oldNotification != null) {
- oldNotification.addProperty(propertyIndex);
+ return oldNotification;
} else {
- notification = new ChangeNotification<K,T>(clazz, key);
notifications.put(notification, notification);
- notification.addProperty(propertyIndex);
+ return notification;
}
}
+
}
-
+ public <K, T extends DMObject<K>> void changed(DataModel model, Class<T> clazz, K key, String propertyName, ClientMatcher matcher) {
+ @SuppressWarnings("unchecked")
+ DMClassHolder<K,T> classHolder = (DMClassHolder<K,T>)model.getClassHolder(clazz);
+
+ ChangeNotification<K,T> notification = getNotification(classHolder, key, matcher);
+ notification.addProperty(propertyName);
+ }
+
+ public <K, T extends DMObject<K>> void feedChanged(DataModel model, Class<T> clazz, K key, String propertyName, long itemTimestamp) {
+ @SuppressWarnings("unchecked")
+ DMClassHolder<K,T> classHolder = (DMClassHolder<K,T>)model.getClassHolder(clazz);
+
+ ChangeNotification<K,T> notification = getNotification(classHolder, key, null);
+ notification.addFeedProperty(propertyName, itemTimestamp);
+ }
+
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/ClientNotification.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/ClientNotification.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/ClientNotification.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -5,7 +5,9 @@
import com.dumbhippo.dm.fetch.Fetch;
import com.dumbhippo.dm.fetch.FetchVisitor;
+import com.dumbhippo.dm.schema.DMClassHolder;
import com.dumbhippo.dm.schema.DMPropertyHolder;
+import com.dumbhippo.dm.schema.FeedPropertyHolder;
import com.dumbhippo.dm.store.StoreClient;
import com.dumbhippo.dm.store.StoreKey;
@@ -31,8 +33,8 @@
this.client = client;
}
- public <K, T extends DMObject<K>>void addObjectProperties(StoreKey<K,T> key, Fetch<K,? super T> fetch, long propertyMask, Fetch<?,?>[] childFetches) {
- notifications.add(new ObjectNotification<K,T>(key, fetch, propertyMask, childFetches));
+ public <K, T extends DMObject<K>>void addObjectProperties(StoreKey<K,T> key, Fetch<K,? super T> fetch, long propertyMask, Fetch<?,?>[] childFetches, int[] maxes) {
+ notifications.add(new ObjectNotification<K,T>(key, fetch, propertyMask, childFetches, maxes));
}
public StoreClient getClient() {
@@ -54,17 +56,32 @@
private Fetch<K, ? super T> fetch;
private long propertyMask;
private Fetch<?,?>[] childFetches;
+ private int[] maxes;
- public ObjectNotification(StoreKey<K,T> key, Fetch<K, ? super T> fetch, long propertiesMask, Fetch<?,?>[] childFetches) {
+ public ObjectNotification(StoreKey<K,T> key, Fetch<K, ? super T> fetch, long propertiesMask, Fetch<?,?>[] childFetches, int[] maxes) {
this.key = key;
this.fetch = fetch;
this.propertyMask = propertiesMask;
this.childFetches = childFetches;
+ this.maxes = maxes;
}
+ private int getMax(FeedPropertyHolder<K,T,?,?> property, int propertyIndex) {
+ int max = -1;
+ if (maxes != null)
+ max = maxes[propertyIndex];
+
+ if (max < property.getDefaultMaxFetch())
+ max = property.getDefaultMaxFetch();
+
+ return max;
+ }
+
public void visitNotification(DMSession session, FetchVisitor visitor) {
T object = session.findUnchecked(key);
- DMPropertyHolder<K,T,?>[] classProperties = key.getClassHolder().getProperties();
+ DMClassHolder<K,T> classHolder = key.getClassHolder();
+ DMPropertyHolder<K,T,?>[] classProperties = classHolder.getProperties();
+ long feedMinTimestamps[] = null;
long v;
int propertyIndex;
@@ -74,7 +91,21 @@
propertyIndex = 0;
while (v != 0) {
if ((v & 1) != 0 && childFetches[propertyIndex] != null) {
- classProperties[propertyIndex].visitChildren(session, childFetches[propertyIndex], object, visitor);
+ if (classProperties[propertyIndex] instanceof FeedPropertyHolder) {
+ @SuppressWarnings("unchecked")
+ FeedPropertyHolder<K,T,?,?> property = (FeedPropertyHolder<K,T,?,?>)classProperties[propertyIndex];
+
+ if (feedMinTimestamps == null) {
+ feedMinTimestamps = new long[key.getClassHolder().getFeedPropertiesCount()];
+ for (int i = 0; i < feedMinTimestamps.length; i++)
+ feedMinTimestamps[i] = -1;
+ }
+
+ int feedPropertyIndex = classHolder.getFeedPropertyIndex(property.getName());
+ feedMinTimestamps[feedPropertyIndex] = property.visitFeedChildren(session, childFetches[propertyIndex], 0, getMax(property, propertyIndex), object, visitor, false);
+ } else {
+ classProperties[propertyIndex].visitChildren(session, childFetches[propertyIndex], object, visitor);
+ }
}
v >>= 1;
@@ -82,14 +113,28 @@
}
}
- visitor.beginResource(key.getClassHolder(), key.getKey(), fetch.getFetchString(key.getClassHolder()), false);
+ visitor.beginResource(key.getClassHolder(), key.getKey(), fetch.getFetchString(classHolder), false);
v = propertyMask;
propertyIndex = 0;
while (v != 0) {
- if ((v & 1) != 0)
- classProperties[propertyIndex].visitProperty(session, object, visitor, true);
+ if ((v & 1) != 0) {
+ if (classProperties[propertyIndex] instanceof FeedPropertyHolder) {
+ @SuppressWarnings("unchecked")
+ FeedPropertyHolder<K,T,?,?> property = (FeedPropertyHolder<K,T,?,?>)classProperties[propertyIndex];
+ long minTimestamp = -1;
+ if (feedMinTimestamps != null) {
+ int feedPropertyIndex = classHolder.getFeedPropertyIndex(property.getName());
+ minTimestamp = feedMinTimestamps[feedPropertyIndex];
+ }
+
+ property.visitFeedProperty(session, 0, getMax(property, propertyIndex), object, visitor, minTimestamp);
+ } else {
+ classProperties[propertyIndex].visitProperty(session, object, visitor, true);
+ }
+ }
+
v >>= 1;
propertyIndex++;
}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/ClientNotificationSet.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/ClientNotificationSet.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/ClientNotificationSet.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -25,7 +25,7 @@
private Map<StoreClient, ClientNotification> notifications;
- public <K, T extends DMObject<K>> void addNotification(StoreClient client, StoreKey<K,T> key, Fetch<K,? super T> fetch, long propertyMask, Fetch<?,?>[] childFetches) {
+ public <K, T extends DMObject<K>> void addNotification(StoreClient client, StoreKey<K,T> key, Fetch<K,? super T> fetch, long propertyMask, Fetch<?,?>[] childFetches, int[] maxes) {
ClientNotification notification = null;
if (notifications == null)
@@ -38,7 +38,7 @@
notifications.put(client, notification);
}
- notification.addObjectProperties(key, fetch, propertyMask, childFetches);
+ notification.addObjectProperties(key, fetch, propertyMask, childFetches, maxes);
}
public Collection<ClientNotification> getNotifications() {
Added: dumbhippo/trunk/server/src/com/dumbhippo/dm/DMFeed.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/DMFeed.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/DMFeed.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -0,0 +1,22 @@
+package com.dumbhippo.dm;
+
+import java.util.Iterator;
+
+/**
+ * This class represents a feed of items; each feed item has a
+ * resource value and a timestamp.
+ *
+ * @param <T> the value type; this class is used for both lists of feed items
+ * as seen by applications and for "dehydrated" feed item list with
+ * just the keys for the resources, which is why it isn't 'T extends DMObject'.
+ */
+public interface DMFeed<T> {
+ /**
+ * Iterate the items in the feed
+ *
+ * @param start start position for iteration (0 is the most recent item in the feed)
+ * @param max maximum number of items to return
+ * @param minTimestamp only iterate items with this timestamp or newer
+ */
+ public Iterator<DMFeedItem<T>> iterator(int start, int max, long minTimestamp);
+}
Added: dumbhippo/trunk/server/src/com/dumbhippo/dm/DMFeedItem.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/DMFeedItem.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/DMFeedItem.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -0,0 +1,33 @@
+package com.dumbhippo.dm;
+
+import java.util.Date;
+
+/**
+ * This class represents a single item in a feed; each feed item has a
+ * resource value and a timestamp.
+ *
+ * @param <T> the value type; this class is used for both feed items
+ * as seen by applications and for "dehydrated" feed items with
+ * just the key for the resource, which is why it isn't 'T extends DMObject'.
+ */
+public class DMFeedItem<T> {
+ protected T value;
+ protected long time;
+
+ public DMFeedItem(T value, long time) {
+ this.value = value;
+ this.time = time;
+ }
+
+ public T getValue() {
+ return value;
+ }
+
+ public long getTime() {
+ return time;
+ }
+
+ public Date getDate() {
+ return new Date(time);
+ }
+}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/DMSession.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/DMSession.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/DMSession.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -161,6 +161,19 @@
* @throws NotCachedException
*/
public abstract <K, T extends DMObject<K>> Object storeAndFilter(StoreKey<K,T> key, int propertyIndex, Object value);
+
+ /**
+ * Internal API: Create a feed wrapper that handles filtering and caching for for the raw feed object.
+ *
+ * @param <K>
+ * @param <T>
+ * @param <TI>
+ * @param key
+ * @param propertyIndex
+ * @param rawFeed
+ * @return
+ */
+ public abstract <K, T extends DMObject<K>> DMFeed<?> createFeedWrapper(StoreKey<K,T> key, int propertyIndex, DMFeed<T> rawFeed);
/**
* Finds the "raw" value for a particular object property. Raw values differ from the normally
Added: dumbhippo/trunk/server/src/com/dumbhippo/dm/FeedWrapper.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/FeedWrapper.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/FeedWrapper.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -0,0 +1,188 @@
+package com.dumbhippo.dm;
+
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import org.slf4j.Logger;
+
+import com.dumbhippo.GlobalSetup;
+import com.dumbhippo.dm.filter.CompiledItemFilter;
+import com.dumbhippo.dm.schema.FeedPropertyHolder;
+
+/**
+ * This class takes the raw DMFeed item as returned from the DMO and adds caching and
+ * filtering to it. For feeds, unlike other property types, this is a dynamic process.
+ * As items are fetched from the feed, they are filtered and cached.
+ *
+ * Note that the start/max positions passed to iterator() are positions in the
+ * *unfiltered* stream. If the all max items are filtered out, no items will be
+ * returned. While this is less than ideal, it keeps things much simpler and
+ * more efficient than the alternative, since we know ahead of time how many
+ * items to fetch from the underlying feed to get max items, and we don't have to
+ * worry about how many items were filtered before 'start'. As long as the fraction
+ * of filtered items is small, the user isn't going to notice if their page of 10
+ * items only has 8 on it.
+ */
+public class FeedWrapper<K, T extends DMObject<K>, KI, TI extends DMObject<KI>> implements DMFeed<TI> {
+ @SuppressWarnings("unused")
+ static final private Logger logger = GlobalSetup.getLogger(FeedWrapper.class);
+
+ private K key;
+ private FeedPropertyHolder<K, T, KI, TI> property;
+ private CachedFeed<KI> cachedFeed;
+ private DMFeed<TI> rawFeed;
+
+ /**
+ * @param property the property holder object for this property
+ * @param key the key of the object that has the feed as a property
+ * @param rawFeed the DMFeed returned by the DMO; this will typically query the database when iterated.
+ * @param cachedFeed cached feed object; this can be null if the current transaction is stale
+ * with respect to the invalidation timestamp in the cache. (Perhaps we should create a dummy CachedFeed
+ * in this case? there is considerable inefficiency the way we do it currently since we'll requery
+ * the database each time we are iterated when cachedFeed is null. Still it's a rare case)
+ */
+ public FeedWrapper(FeedPropertyHolder<K, T, KI,TI> property, K key, DMFeed<TI> rawFeed, CachedFeed<KI> cachedFeed) {
+ this.key = key;
+ this.property = property;
+ this.rawFeed = rawFeed;
+ this.cachedFeed = cachedFeed;
+ }
+
+ public Iterator<DMFeedItem<TI>> iterator(int start, int max, long minTimestamp) {
+ return new FeedWrapperIterator(start, max, minTimestamp);
+ }
+
+ private class FeedWrapperIterator implements Iterator<DMFeedItem<TI>> {
+ private int start;
+ private int max;
+ private long minTimestamp;
+
+ // Because filtering will remove items that are there in the underlying
+ // feed, we need to fetch one item ahead to give an accurate value for
+ // hasNext().
+ private boolean fetchedNext = false;
+ private DMFeedItem<TI> nextItem = null;
+
+ private int pos;
+
+ private Iterator<DMFeedItem<TI>> sourceIterator;
+
+ public FeedWrapperIterator(int start, int max, long minTimestamp) {
+ this.start = start;
+ this.max = max;
+ this.minTimestamp = minTimestamp;
+
+ pos = start;
+ }
+
+ private void fetchNext() {
+ DMSession session = property.getModel().currentSession();
+ CompiledItemFilter<K, T, KI, TI> itemFilter = property.getItemFilter();
+
+ nextItem = null;
+ fetchedNext = true;
+
+ if (sourceIterator == null && cachedFeed != null) {
+ /* We start off by fetching items from the cache
+ */
+ while (pos < max && pos < cachedFeed.size()) {
+ DMFeedItem<KI> cachedItem = cachedFeed.getItem(pos);
+
+ KI filteredKey;
+ if (itemFilter != null)
+ filteredKey = itemFilter.filterKey(session.getViewpoint(), key, cachedItem.getValue());
+ else
+ filteredKey = cachedItem.getValue();
+
+ if (filteredKey != null) {
+ nextItem = new DMFeedItem<TI>(property.rehydrateDMO(filteredKey, session), cachedItem.getTime());
+ pos++;
+ return;
+ }
+
+ pos++;
+ }
+ }
+
+ if (pos == max)
+ return;
+
+ /* Check if the cachedFeed was the result of a fetch that asked for
+ * as many or more items than the current fetch but only returned a limited
+ * amount of items; in that case, we don't need to query the database again.
+ */
+ if (cachedFeed != null && cachedFeed.getMaxFetched() >= start + max)
+ return;
+
+ /*
+ * If there are more items left to fetch once the cache is exhausted, we iterate
+ * from the raw source feed and cache those new items as we go along.
+ */
+
+ if (sourceIterator == null) {
+ if (cachedFeed != null) {
+ /* We don't want to leave gaps in what is stored in cachedFeed, so we
+ * may need to start source iterator before pos, and then skip over
+ * some number of items
+ */
+ int skipPos = cachedFeed.size();
+ sourceIterator = rawFeed.iterator(skipPos, start + max - skipPos, minTimestamp);
+
+ while (skipPos < pos && sourceIterator.hasNext()) {
+ DMFeedItem<TI> item = sourceIterator.next();
+ skipPos++;
+ cachedFeed.addItem(item.getValue().getKey(), item.getTime());
+ }
+ } else {
+ sourceIterator = rawFeed.iterator(pos, start + max - pos, minTimestamp);
+ }
+ }
+
+ while (sourceIterator.hasNext()) {
+ DMFeedItem<TI> item = sourceIterator.next();
+ pos++;
+
+ if (cachedFeed != null)
+ cachedFeed.addItem(item.getValue().getKey(), item.getTime());
+
+ TI filteredValue;
+ if (itemFilter != null)
+ filteredValue = itemFilter.filterObject(session.getViewpoint(), key, item.getValue());
+ else
+ filteredValue = item.getValue();
+
+ if (filteredValue != null) {
+ nextItem = item;
+ return;
+ }
+ }
+
+ if (cachedFeed != null)
+ cachedFeed.addToMaxfetched(start + max);
+ }
+
+ public boolean hasNext() {
+ if (!fetchedNext)
+ fetchNext();
+
+ return nextItem != null && nextItem.getTime() >= minTimestamp;
+ }
+
+ public DMFeedItem<TI> next() {
+ if (!fetchedNext)
+ fetchNext();
+
+ DMFeedItem<TI> result = nextItem;
+ if (result == null || result.getTime() < minTimestamp)
+ throw new NoSuchElementException();
+
+ nextItem = null;
+ fetchedNext = false;
+ return result;
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/ReadOnlySession.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/ReadOnlySession.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/ReadOnlySession.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -4,6 +4,7 @@
import com.dumbhippo.GlobalSetup;
import com.dumbhippo.dm.schema.DMPropertyHolder;
+import com.dumbhippo.dm.schema.FeedPropertyHolder;
import com.dumbhippo.dm.store.StoreKey;
/**
@@ -51,8 +52,17 @@
else
return property.filter(getViewpoint(), key.getKey(), value);
}
-
+
@Override
+ @SuppressWarnings("unchecked")
+ public <K, T extends DMObject<K>> DMFeed<?> createFeedWrapper(StoreKey<K, T> key, int propertyIndex, DMFeed<T> rawFeed) {
+ FeedPropertyHolder<K, T, ?, ?> feedProperty = (FeedPropertyHolder<K, T, ?, ?>)key.getClassHolder().getProperty(propertyIndex);
+
+ CachedFeed cached = model.getStore().getOrCreateCachedFeed(key, propertyIndex, txTimestamp);
+ return new FeedWrapper(feedProperty, key.getKey(), rawFeed, cached);
+ }
+
+ @Override
public void afterCompletion(int status) {
}
}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/ReadWriteSession.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/ReadWriteSession.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/ReadWriteSession.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -6,6 +6,7 @@
import com.dumbhippo.GlobalSetup;
import com.dumbhippo.dm.schema.DMPropertyHolder;
+import com.dumbhippo.dm.schema.FeedPropertyHolder;
import com.dumbhippo.dm.store.StoreKey;
/**
@@ -40,11 +41,18 @@
return property.filter(getViewpoint(), key.getKey(), value);
}
+ @Override
+ @SuppressWarnings("unchecked")
+ public <K, T extends DMObject<K>> DMFeed<?> createFeedWrapper(StoreKey<K, T> key, int propertyIndex, DMFeed<T> rawFeed) {
+ FeedPropertyHolder<K, T, ?, ?> feedProperty = (FeedPropertyHolder<K, T, ?, ?>)key.getClassHolder().getProperty(propertyIndex);
+ return new FeedWrapper(feedProperty, key.getKey(), rawFeed, null);
+ }
+
/**
* Indicate that a resource property has changed; this invalidates any cached value for the
* property and also triggers sending notifications to any clients that have registered
* for notification on the property. Notifications will only be sent after the current
- * transaction commits succesfully.
+ * transaction commits succesfully. For changes to feed-valued properties, see feedChanged().
*
* @param <K>
* @param <T>
@@ -57,6 +65,22 @@
}
/**
+ * Indicate that a feed-valued resource property has changed; This differs from changed()
+ * in that the timestamp of the provided item is also provided.
+ *
+ * @param <K>
+ * @param <T>
+ * @param clazz the class of the resource where the property changed
+ * @param key the key of the resource where the property changed
+ * @param propertyName the name of the property that changed
+ * @param itemTimestamp new timestamp of the item that was inserted or restacked, or -1 to indicate
+ * that an item was deleted.
+ */
+ public <K, T extends DMObject<K>> void feedChanged(Class<T> clazz, K key, String propertyName, long itemTimestamp) {
+ notificationSet.feedChanged(model, clazz, key, propertyName, itemTimestamp);
+ }
+
+ /**
* Indicate that a resource property has changed; this invalidates any cached value for the
* property and also triggers sending notifications to any clients that have registered
* for notification on the property. Notifications will only be sent after the current
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/annotations/DMProperty.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/annotations/DMProperty.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/annotations/DMProperty.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -34,6 +34,17 @@
* be specified for resource-typed properties, forces defaultInclude on)
*/
String defaultChildren() default "";
+
+ /**
+ * For feed properties, the maximum number of feed entries to fetch
+ * if no limit or a smaller limit is specified. (It might be more
+ * appropriately called the minMaxFetch. There's no fundamental reason
+ * we couldn't support a caller reducing the maxFetch from this value,
+ * but it makes bookkeeping tricky, since the correct merge of two
+ * fetch strings 'prop' and 'prop(max=30)' then depends on knowing what
+ * the defaultMaxFetch for 'prop')
+ */
+ int defaultMaxFetch() default 20;
/**
* Whether the property should be cached or not. The main reason to avoid
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/annotations/PropertyType.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/annotations/PropertyType.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/annotations/PropertyType.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -8,6 +8,7 @@
FLOAT('f'),
STRING('s'),
RESOURCE('r'),
+ FEED('f'),
URL('u');
private char typeChar;
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/Fetch.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/Fetch.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/Fetch.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -12,6 +12,7 @@
import com.dumbhippo.dm.DMSession;
import com.dumbhippo.dm.schema.DMClassHolder;
import com.dumbhippo.dm.schema.DMPropertyHolder;
+import com.dumbhippo.dm.schema.FeedPropertyHolder;
import com.dumbhippo.dm.store.StoreClient;
import com.dumbhippo.dm.store.StoreKey;
@@ -44,9 +45,18 @@
return classIndex < classProperties.length ? classProperties[classIndex].getOrdering() : Long.MAX_VALUE;
}
+ private long[] createFeedMinTimestamps(DMClassHolder<?,?> classHolder) {
+ long[] feedMinTimestamps = new long[classHolder.getFeedPropertiesCount()];
+ for (int i = 0; i < feedMinTimestamps.length; i++)
+ feedMinTimestamps[i] = -1;
+
+ return feedMinTimestamps;
+ }
+
public <U extends T> void visit(DMSession session, DMClassHolder<K,U> classHolder, U object, FetchVisitor visitor, boolean indirect) {
DMPropertyHolder<K,U,?>[] classProperties = classHolder.getProperties();
Fetch<K,? super U> oldFetch;
+ long[] feedMinTimestamps = null;
StoreClient storeClient;
DMClient client = session.getClient();
@@ -105,8 +115,53 @@
if (newFetched && !oldFetched)
newAnyFetched = true;
- if (newChildren != null && (oldChildren == null || newChildren != oldChildren))
+ if ((oldFetched || newFetched) && classProperties[classIndex] instanceof FeedPropertyHolder) {
+ @SuppressWarnings("unchecked")
+ FeedPropertyHolder<K,T,?,?> property = (FeedPropertyHolder<K,T,?,?>)classProperties[classIndex];
+
+ /* The advantage of always fetching *at least* the default is that
+ * then we can merge to fetch specifications without knowing the
+ * particular value of the max fetch.
+ */
+ int oldMax = 0;
+ if (oldOrdering == classOrdering) {
+ oldMax = oldFetch.properties[newIndex].getMax();
+ if (oldMax < property.getDefaultMaxFetch())
+ oldMax = property.getDefaultMaxFetch();
+ }
+
+ int newMax = 0;
+ if (newOrdering == classOrdering) {
+ newMax = properties[newIndex].getMax();
+ if (newMax < property.getDefaultMaxFetch())
+ newMax = property.getDefaultMaxFetch();
+ }
+
+ if (newMax > oldMax && (newChildren != null || oldChildren != null)) {
+ Fetch<?,?> children;
+
+ if (newChildren == null)
+ children = oldChildren;
+ else if (oldChildren == null)
+ children = newChildren;
+ else
+ children = newChildren.merge(oldChildren);
+
+ if (feedMinTimestamps == null)
+ feedMinTimestamps = createFeedMinTimestamps(classHolder);
+ int feedPropertyIndex = classHolder.getFeedPropertyIndex(property.getName());
+
+ feedMinTimestamps[feedPropertyIndex] = property.visitFeedChildren(session, children, oldMax, newMax - oldMax, object, visitor, true);
+ } else if (newChildren != null && (oldChildren == null || newChildren != oldChildren)) {
+ if (feedMinTimestamps == null)
+ feedMinTimestamps = createFeedMinTimestamps(classHolder);
+ int feedPropertyIndex = classHolder.getFeedPropertyIndex(property.getName());
+
+ feedMinTimestamps[feedPropertyIndex] = property.visitFeedChildren(session, newChildren, 0, oldMax, object, visitor, false);
+ }
+ } else if (newChildren != null && (oldChildren == null || newChildren != oldChildren)) {
classProperties[classIndex].visitChildren(session, newChildren, object, visitor);
+ }
}
// If this resource is part of the direct result, we must always include the resource,
@@ -161,11 +216,40 @@
while (newOrdering < classOrdering)
newOrdering = propertyOrdering(++newIndex);
- boolean oldFetched = oldOrdering == classOrdering || (oldFetch != null && oldFetch.includeDefault && classProperties[classIndex].getDefaultInclude());
- boolean newFetched = newOrdering == classOrdering || (includeDefault && classProperties[classIndex].getDefaultInclude());
-
- if (newFetched && !oldFetched)
+ boolean oldFetched = (oldOrdering == classOrdering) || (oldFetch != null && oldFetch.includeDefault && classProperties[classIndex].getDefaultInclude());
+ boolean newFetched = (newOrdering == classOrdering) || (includeDefault && classProperties[classIndex].getDefaultInclude());
+
+ if ((oldFetched || newFetched) && classProperties[classIndex] instanceof FeedPropertyHolder) {
+ @SuppressWarnings("unchecked")
+ FeedPropertyHolder<K,T,?,?> property = (FeedPropertyHolder<K,T,?,?>)classProperties[classIndex];
+
+ int oldMax =0;
+ if (oldOrdering == classOrdering) {
+ oldMax = oldFetch.properties[newIndex].getMax();
+ if (oldMax < property.getDefaultMaxFetch())
+ oldMax = property.getDefaultMaxFetch();
+ }
+
+ int newMax = 0;
+ if (newOrdering == classOrdering) {
+ newMax = properties[newIndex].getMax();
+ if (newMax < property.getDefaultMaxFetch())
+ newMax = property.getDefaultMaxFetch();
+ }
+
+ long minTimestamp;
+ if (feedMinTimestamps != null && newMax <= oldMax) {
+ int feedPropertyIndex = classHolder.getFeedPropertyIndex(property.getName());
+ minTimestamp = feedMinTimestamps[feedPropertyIndex];
+ } else {
+ minTimestamp = 0;
+ }
+
+ if (newMax > oldMax)
+ property.visitFeedProperty(session, oldMax, newMax - oldMax, object, visitor, minTimestamp);
+ } else if (newFetched && !oldFetched) {
classProperties[classIndex].visitProperty(session, object, visitor, false);
+ }
}
visitor.endResource();
@@ -240,8 +324,9 @@
return b.toString();
}
- public <U extends T> Fetch<K,? super U> merge(Fetch<K,? super U> other) {
+ public Fetch<?,?> merge(Fetch<?,?> other) {
int newCount = this.properties.length;
+ boolean changedProperties = false;
// Count the total number of properties in the merge of the two fetches
@@ -254,6 +339,8 @@
thisOrdering = this.propertyOrdering(++thisIndex);
} else if (thisOrdering == otherOrdering) {
// In both fetches
+ if (!this.properties[thisIndex].equals(other.properties[otherIndex]))
+ changedProperties = true;
thisOrdering = this.propertyOrdering(++thisIndex);
otherOrdering = other.propertyOrdering(++otherIndex);
} else {
@@ -264,7 +351,7 @@
}
// If the other property is a subset of this one, we can just return this one
- if (newCount == this.properties.length && (this.includeDefault || !other.includeDefault))
+ if (!changedProperties && newCount == this.properties.length && (this.includeDefault || !other.includeDefault))
return this;
PropertyFetch[] newProperties = new PropertyFetch[newCount];
@@ -280,7 +367,7 @@
thisOrdering = this.propertyOrdering(++thisIndex);
} else if (thisOrdering == otherOrdering) {
// In both fetches
- newProperties[newIndex++] = this.properties[thisIndex];
+ newProperties[newIndex++] = this.properties[thisIndex].merge(other.properties[otherIndex]);
thisOrdering = this.propertyOrdering(++thisIndex);
otherOrdering = other.propertyOrdering(++otherIndex);
} else {
@@ -291,13 +378,15 @@
}
@SuppressWarnings("unchecked")
- Fetch<K,? super U> newFetch = new Fetch(newProperties, this.includeDefault || other.includeDefault);
+ Fetch<?, ?> newFetch = new Fetch(newProperties, this.includeDefault || other.includeDefault);
+
return newFetch;
}
public void resolveNotifications(StoreClient client, StoreKey<K,? extends T> key, long propertyMask, ClientNotificationSet result) {
DMPropertyHolder<K,? extends T,?>[] classProperties = key.getClassHolder().getProperties();
Fetch<?,?>[] childFetches = null;
+ int[] maxes = null;
long notifiedMask = 0;
long bit = 1;
@@ -313,11 +402,13 @@
boolean notified = false;
Fetch<?,?> childFetch = null;
+ int max = -1;
if (propertyOrdering == classOrdering) {
if (properties[propertyIndex].getNotify()) {
notified = true;
childFetch = properties[propertyIndex].getChildren();
+ max = properties[propertyIndex].getMax();
}
} else if (includeDefault && classProperties[classIndex].getDefaultInclude()) {
notified = true;
@@ -332,6 +423,16 @@
childFetches = new Fetch[classProperties.length];
childFetches[classIndex] = childFetch;
}
+
+ if (max >= 0) {
+ if (maxes == null) {
+ maxes = new int[classProperties.length];
+ for (int i = 0; i < classProperties.length; i++)
+ maxes[i] = -1;
+ }
+ maxes[classIndex] = max;
+ }
+
}
propertyMask >>= 1;
@@ -340,7 +441,7 @@
}
if (notifiedMask != 0)
- result.addNotification(client, key, this, notifiedMask, childFetches);
+ result.addNotification(client, key, this, notifiedMask, childFetches, maxes);
}
public <U extends T> String getFetchString(DMClassHolder<K,U> classHolder) {
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/FetchAttributeType.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/FetchAttributeType.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/FetchAttributeType.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -1,6 +1,7 @@
package com.dumbhippo.dm.fetch;
public enum FetchAttributeType {
+ MAX("max"),
NOTIFY("notify");
private String lowerName;
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/FetchVisitor.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/FetchVisitor.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/FetchVisitor.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -12,6 +12,7 @@
<K,T extends DMObject<K>> void beginResource(DMClassHolder<K,T> classHolder, K key, String fetchString, boolean indirect);
<K,T extends DMObject<K>> void plainProperty(PlainPropertyHolder<K, T, ?> propertyHolder, Object value);
<KP,TP extends DMObject<KP>> void resourceProperty(ResourcePropertyHolder<?,?,KP,TP> propertyHolder, KP key);
+ <KP,TP extends DMObject<KP>> void feedProperty(ResourcePropertyHolder<?,?,KP,TP> propertyHolder, KP key, long timestamp, boolean incremental);
<K,T extends DMObject<K>> void emptyProperty(DMPropertyHolder<K,T,?> propertyHolder);
void endResource();
}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/PropertyFetch.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/PropertyFetch.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/PropertyFetch.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -7,11 +7,13 @@
private DMPropertyHolder<?,?,?> property;
private Fetch<?,?> children;
private boolean notify;
+ private int max;
- public PropertyFetch(DMPropertyHolder<?,?,?> property, Fetch<?,?> children, boolean notify) {
+ public PropertyFetch(DMPropertyHolder<?,?,?> property, Fetch<?,?> children, boolean notify, int max) {
this.property = property;
this.children = children;
this.notify = notify;
+ this.max = max;
}
public Fetch<?,?> getChildren() {
@@ -22,10 +24,34 @@
return property;
}
+ public int getMax() {
+ return max;
+ }
+
public boolean getNotify() {
return notify;
}
+
+ public PropertyFetch merge(PropertyFetch other) {
+ if (equals(other))
+ return this;
+
+ Fetch<?, ?> newChildren;
+
+ if (children == null)
+ newChildren = other.children;
+ else if (other.children == null)
+ newChildren = children;
+ else
+ newChildren = children.merge(other.children);
+
+ boolean newNotify = notify || other.notify;
+ int newMax = Math.max(max, other.max);
+
+ return new PropertyFetch(property, newChildren, newNotify, newMax);
+ }
+
public int compareTo(PropertyFetch other) {
return property.compareTo(other.property);
}
@@ -42,6 +68,9 @@
if (notify != other.notify)
return false;
+ if (max != other.max)
+ return false;
+
if ((children == null && other.children != null) ||
(children != null && !children.equals(other.children)))
return false;
@@ -65,8 +94,19 @@
b.append(property.getPropertyId());
+ if (max >= 0 || !notify)
+ b.append('(');
+
+ if (max >= 0) {
+ b.append("max=");
+ b.append(max);
+ if (!notify)
+ b.append(",");
+ }
if (!notify)
- b.append("(notify=false)");
+ b.append("notify=false");
+ if (max >= 0 || !notify)
+ b.append(')');
if (children != null) {
b.append(' ');
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/PropertyFetchNode.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/PropertyFetchNode.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/PropertyFetchNode.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -41,7 +41,7 @@
return null;
}
- public <K,T extends DMObject<K>> void bindResourceProperty(ResourcePropertyHolder<?,?,K,T> resourceHolder, List<PropertyFetch> resultList, boolean maybeSkip, boolean notify) {
+ public <K,T extends DMObject<K>> void bindResourceProperty(ResourcePropertyHolder<?,?,K,T> resourceHolder, List<PropertyFetch> resultList, boolean maybeSkip, boolean notify, int max) {
Fetch<K,T> defaultChildren = resourceHolder.getDefaultChildren();
Fetch<K,T> boundChildren = null;
@@ -59,12 +59,12 @@
return;
}
- resultList.add(new PropertyFetch(resourceHolder, boundChildren, notify));
+ resultList.add(new PropertyFetch(resourceHolder, boundChildren, notify, max));
}
- public void bindPlainProperty(DMPropertyHolder<?,?,?> propertyHolder, List<PropertyFetch> resultList, boolean maybeSkip, boolean notify) {
+ public void bindPlainProperty(DMPropertyHolder<?,?,?> propertyHolder, List<PropertyFetch> resultList, boolean maybeSkip, boolean notify, int max) {
if (!maybeSkip)
- resultList.add(new PropertyFetch(propertyHolder, null, notify));
+ resultList.add(new PropertyFetch(propertyHolder, null, notify, max));
}
/**
@@ -79,9 +79,18 @@
* @param resultList list to append the results to
*/
public void bind(DMClassHolder<?,?> classHolder, boolean skipDefault, List<PropertyFetch> resultList) {
+ int max = -1;
boolean notify = true;
for (FetchAttributeNode attribute : attributes) {
switch (attribute.getType()) {
+ case MAX:
+ // FIXME: We probably should make bind() throw an exception and make this fatal
+ if (!(attribute.getValue() instanceof Integer)) {
+ logger.warn("Ignoring non-integer max attribute");
+ continue;
+ }
+ max = ((Integer)(attribute.getValue()));
+ break;
case NOTIFY:
// FIXME: We probably should make bind() throw an exception and make this fatal
if (!(attribute.getValue() instanceof Boolean)) {
@@ -99,9 +108,9 @@
boolean maybeSkip = skipDefault && propertyHolder.getDefaultInclude();
if (propertyHolder instanceof ResourcePropertyHolder) {
- bindResourceProperty(propertyHolder.asResourcePropertyHolder(propertyHolder.getKeyClass()), resultList, maybeSkip, notify);
+ bindResourceProperty(propertyHolder.asResourcePropertyHolder(propertyHolder.getKeyClass()), resultList, maybeSkip, notify, max);
} else {
- bindPlainProperty(propertyHolder, resultList, maybeSkip, notify);
+ bindPlainProperty(propertyHolder, resultList, maybeSkip, notify, max);
}
}
}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchLexer.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchLexer.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchLexer.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -3,9 +3,11 @@
package com.dumbhippo.dm.parser;
import java.io.StringReader;
+import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import com.dumbhippo.dm.fetch.*;
+import com.dumbhippo.dm.parser.ParseException;
import com.dumbhippo.GlobalSetup;
import org.slf4j.Logger;
@@ -49,9 +51,10 @@
caseSensitiveLiterals = true;
setCaseSensitive(true);
literals = new Hashtable();
- literals.put(new ANTLRHashString("true", this), new Integer(16));
- literals.put(new ANTLRHashString("false", this), new Integer(17));
- literals.put(new ANTLRHashString("notify", this), new Integer(14));
+ literals.put(new ANTLRHashString("true", this), new Integer(17));
+ literals.put(new ANTLRHashString("max", this), new Integer(14));
+ literals.put(new ANTLRHashString("false", this), new Integer(18));
+ literals.put(new ANTLRHashString("notify", this), new Integer(15));
}
public Token nextToken() throws TokenStreamException {
@@ -135,7 +138,7 @@
case '4': case '5': case '6': case '7':
case '8': case '9':
{
- mINTEGER(true);
+ mDIGITS(true);
theRetToken=_returnToken;
break;
}
@@ -318,7 +321,7 @@
}
}
{
- _loop378:
+ _loop33:
do {
switch ( LA(1)) {
case 'a': case 'b': case 'c': case 'd':
@@ -357,7 +360,7 @@
}
default:
{
- break _loop378;
+ break _loop33;
}
}
} while (true);
@@ -369,23 +372,23 @@
_returnToken = _token;
}
- public final void mINTEGER(boolean _createToken) throws RecognitionException, CharStreamException, TokenStreamException {
+ public final void mDIGITS(boolean _createToken) throws RecognitionException, CharStreamException, TokenStreamException {
int _ttype; Token _token=null; int _begin=text.length();
- _ttype = INTEGER;
+ _ttype = DIGITS;
int _saveIndex;
{
- int _cnt381=0;
- _loop381:
+ int _cnt36=0;
+ _loop36:
do {
if (((LA(1) >= '0' && LA(1) <= '9'))) {
matchRange('0','9');
}
else {
- if ( _cnt381>=1 ) { break _loop381; } else {throw new NoViableAltForCharException((char)LA(1), getFilename(), getLine(), getColumn());}
+ if ( _cnt36>=1 ) { break _loop36; } else {throw new NoViableAltForCharException((char)LA(1), getFilename(), getLine(), getColumn());}
}
- _cnt381++;
+ _cnt36++;
} while (true);
}
if ( _createToken && _token==null && _ttype!=Token.SKIP ) {
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchParser.g
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchParser.g 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchParser.g 2007-12-05 05:14:19 UTC (rev 6968)
@@ -113,7 +113,8 @@
;
attributeType returns [FetchAttributeType t]
- : "notify" { t = FetchAttributeType.NOTIFY; }
+ : "max" { t = FetchAttributeType.MAX; }
+ | "notify" { t = FetchAttributeType.NOTIFY; }
;
positiveInteger returns [Integer i]
@@ -140,5 +141,5 @@
SEMICOLON : ";" ;
STAR : "*" ;
NAME : ('a' .. 'z' | 'A' .. 'Z' | '_') ('a' .. 'z' | 'A' .. 'Z' | '0' .. '9' | '_')* ;
-INTEGER : ('0' .. '9')+ ;
+DIGITS : ('0' .. '9')+ ;
WS : ( ' ' | '\t' | '\r' | '\n' | '\f' ) { $setType(Token.SKIP); } ;
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchParser.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchParser.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchParser.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -111,7 +111,7 @@
p=propertyFetch();
props.add(p);
{
- _loop83:
+ _loop5:
do {
if ((LA(1)==SEMICOLON)) {
match(SEMICOLON);
@@ -119,7 +119,7 @@
props.add(p);
}
else {
- break _loop83;
+ break _loop5;
}
} while (true);
@@ -266,12 +266,13 @@
match(LPAREN);
{
switch ( LA(1)) {
+ case LITERAL_max:
case LITERAL_notify:
{
a=attribute();
attrs.add(a);
{
- _loop92:
+ _loop14:
do {
if ((LA(1)==COMMA)) {
match(COMMA);
@@ -279,7 +280,7 @@
attrs.add(a);
}
else {
- break _loop92;
+ break _loop14;
}
} while (true);
@@ -361,8 +362,24 @@
FetchAttributeType t;
- match(LITERAL_notify);
- t = FetchAttributeType.NOTIFY;
+ switch ( LA(1)) {
+ case LITERAL_max:
+ {
+ match(LITERAL_max);
+ t = FetchAttributeType.MAX;
+ break;
+ }
+ case LITERAL_notify:
+ {
+ match(LITERAL_notify);
+ t = FetchAttributeType.NOTIFY;
+ break;
+ }
+ default:
+ {
+ throw new NoViableAltException(LT(1), getFilename());
+ }
+ }
return t;
}
@@ -418,11 +435,11 @@
"PLUS",
"STAR",
"EQUALS",
+ "\"max\"",
"\"notify\"",
"DIGITS",
"\"true\"",
"\"false\"",
- "INTEGER",
"WS"
};
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchParserTokenTypes.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchParserTokenTypes.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/parser/FetchParserTokenTypes.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -3,9 +3,11 @@
package com.dumbhippo.dm.parser;
import java.io.StringReader;
+import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
import com.dumbhippo.dm.fetch.*;
+import com.dumbhippo.dm.parser.ParseException;
import com.dumbhippo.GlobalSetup;
import org.slf4j.Logger;
@@ -13,19 +15,19 @@
int EOF = 1;
int NULL_TREE_LOOKAHEAD = 3;
int SEMICOLON = 4;
- int LPAREN = 5;
- int COMMA = 6;
- int RPAREN = 7;
- int LBRACKET = 8;
- int RBRACKET = 9;
- int PLUS = 10;
- int STAR = 11;
- int NAME = 12;
+ int LBRACKET = 5;
+ int RBRACKET = 6;
+ int LPAREN = 7;
+ int COMMA = 8;
+ int RPAREN = 9;
+ int NAME = 10;
+ int PLUS = 11;
+ int STAR = 12;
int EQUALS = 13;
- int LITERAL_notify = 14;
- int DIGITS = 15;
- int LITERAL_true = 16;
- int LITERAL_false = 17;
- int INTEGER = 18;
+ int LITERAL_max = 14;
+ int LITERAL_notify = 15;
+ int DIGITS = 16;
+ int LITERAL_true = 17;
+ int LITERAL_false = 18;
int WS = 19;
}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/DMClassHolder.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/DMClassHolder.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/DMClassHolder.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -60,6 +60,7 @@
private DMPropertyHolder<K,T,?>[] properties;
private boolean[] mustQualify;
private Map<String, Integer> propertiesMap = new HashMap<String, Integer>();
+ private Map<String, Integer> feedPropertiesMap = new HashMap<String, Integer>();
private DMO annotation;
private Filter filter;
@@ -158,6 +159,26 @@
return index;
}
+ /**
+ * Get the index of the property with the specified name among feed-valued
+ * properties of the class. Can be used along with getFeedPropertiesCount()
+ * to store feed-specific data for a instance of this class in an array.
+ */
+ public int getFeedPropertyIndex(String name) {
+ Integer index = feedPropertiesMap.get(name);
+ if (index == null)
+ return -1;
+
+ return index;
+ }
+
+ /**
+ * see getFeedPropertyIndex().
+ */
+ public int getFeedPropertiesCount() {
+ return feedPropertiesMap.size();
+ }
+
public int getPropertyCount() {
return properties.length;
}
@@ -370,10 +391,13 @@
properties = tmpProperties;
mustQualify = new boolean[foundProperties.size()];
+ int feedPropertiesCount = 0;
for (int i = 0; i < properties.length; i++) {
DMPropertyHolder<?,?,?> property = properties[i];
propertiesMap.put(property.getName(), i);
+ if (property instanceof FeedPropertyHolder)
+ feedPropertiesMap.put(property.getName(), feedPropertiesCount++);
mustQualify[i] = nameCount.get(property.getName()) > 1;
}
}
@@ -489,12 +513,34 @@
wrapperCtClass.addMethod(method);
}
+
+ private void addFeedWrapperGetter(CtClass wrapperCtClass, DMPropertyHolder<?,?,?> property, int propertyIndex) throws CannotCompileException {
+ CtMethod wrapperMethod = new CtMethod(property.getCtClass(), property.getMethodName(), new CtClass[] {}, wrapperCtClass);
+
+ if (property.getGroup() >= 0)
+ throw new RuntimeException("Feed property '" + property.getName() + "' cannot be grouped.");
+
+ Template body = new Template(
+ "{" +
+ " if (!_dm_%propertyName%Initialized) {" +
+ " _dm_init();" +
+ " _dm_%propertyName% = _dm_session.createFeedWrapper(getStoreKey(), %propertyIndex%, super.%methodName%());" +
+ " _dm_%propertyName%Initialized = true;" +
+ " }" +
+ " return _dm_%propertyName%;" +
+ "}");
+
+ body.setParameter("methodName", property.getMethodName());
+ body.setParameter("propertyName", property.getName());
+ body.setParameter("propertyIndex", Integer.toString(propertyIndex));
+ wrapperMethod.setBody(body.toString());
+
+ wrapperCtClass.addMethod(wrapperMethod);
+ }
private void addWrapperGetter(CtClass wrapperCtClass, DMPropertyHolder<?,?,?> property, int propertyIndex) throws CannotCompileException, NotFoundException {
CtMethod wrapperMethod = new CtMethod(property.getCtClass(), property.getMethodName(), new CtClass[] {}, wrapperCtClass);
- // TODO: Deal with primitive types, where we need to box/unbox
-
String storeCommands;
int group = property.getGroup();
if (group < 0) {
@@ -553,7 +599,10 @@
for (int i = 0; i < properties.length; i++) {
DMPropertyHolder<?,?,?> property = properties[i];
- addWrapperGetter(wrapperCtClass, property, i);
+ if (property instanceof FeedPropertyHolder)
+ addFeedWrapperGetter(wrapperCtClass, property, i);
+ else
+ addWrapperGetter(wrapperCtClass, property, i);
}
}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/DMPropertyHolder.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/DMPropertyHolder.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/DMPropertyHolder.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -21,9 +21,11 @@
import com.dumbhippo.GlobalSetup;
import com.dumbhippo.StringUtils;
import com.dumbhippo.dm.Cardinality;
+import com.dumbhippo.dm.DMFeed;
import com.dumbhippo.dm.DMObject;
import com.dumbhippo.dm.DMSession;
import com.dumbhippo.dm.DMViewpoint;
+import com.dumbhippo.dm.DataModel;
import com.dumbhippo.dm.annotations.DMFilter;
import com.dumbhippo.dm.annotations.DMProperty;
import com.dumbhippo.dm.annotations.PropertyType;
@@ -219,6 +221,10 @@
return annotation.group();
}
+ public int getDefaultMaxFetch() {
+ return annotation.defaultMaxFetch();
+ }
+
abstract public Object dehydrate(Object value);
abstract public Object rehydrate(DMViewpoint viewpoint, K key, Object value, DMSession session, boolean filter);
abstract public Object filter(DMViewpoint viewpoint, K key, Object value);
@@ -273,6 +279,7 @@
boolean listValued = false;
boolean setValued = false;
+ boolean feedValued = false;
Class<?> elementType;
if (genericType instanceof ParameterizedType) {
@@ -282,8 +289,10 @@
listValued = true;
else if (rawType == Set.class)
setValued = true;
+ else if (rawType == DMFeed.class)
+ feedValued = true;
else
- throw new RuntimeException("List<?> and Set<?> are the only currently supported parameterized types");
+ throw new RuntimeException("List<?>, Set<?>, DMFeed<?> are the only currently supported parameterized types");
if (paramType.getActualTypeArguments().length != 1)
throw new RuntimeException("Couldn't understand type arguments to parameterized return type");
@@ -301,14 +310,21 @@
DMClassInfo<?,? extends DMObject<?>> classInfo = DMClassInfo.getForClass(elementType);
if (classInfo != null) {
- return createResourcePropertyHolder(classHolder, ctMethod, classInfo, property, filter, viewerDependent, listValued, setValued);
+ return createResourcePropertyHolder(classHolder, ctMethod, classInfo, property, filter, viewerDependent, listValued, setValued, feedValued);
} else if (elementType.isPrimitive() || (genericElementType == String.class) || (genericElementType == Date.class)) {
+ if (feedValued)
+ throw new RuntimeException("Feed properties must be resource-valued");
+
return createPlainPropertyHolder(classHolder, ctMethod, elementType, property, filter, viewerDependent, listValued, setValued);
} else {
throw new RuntimeException("Property type must be DMObject, primitive, Date, or String");
}
}
+ public DataModel getModel() {
+ return declaringClassHolder.getModel();
+ }
+
// this is somewhat silly, the unchecked is to avoid having type params on the constructor; eclipse
// will let you put them on there in the way we do with most other cases like this (see below for the plain holders for example),
// but javac gets confused by that
@@ -335,6 +351,17 @@
// will let you put them on there in the way we do with most other cases like this (see below for the plain holders for example),
// but javac gets confused by that
@SuppressWarnings("unchecked")
+ private static <K, T extends DMObject<K>> FeedPropertyHolder<K,T,?,?>
+ newFeedPropertyHolderHack(DMClassHolder<K,T> classHolder, CtMethod ctMethod, DMClassInfo<?,? extends DMObject<?>> classInfo,
+ DMProperty property, DMFilter filter, ViewerDependent viewerDependent) {
+ return new FeedPropertyHolder(classHolder, ctMethod, classInfo,
+ property, filter, viewerDependent);
+ }
+
+ // this is somewhat silly, the unchecked is to avoid having type params on the constructor; eclipse
+ // will let you put them on there in the way we do with most other cases like this (see below for the plain holders for example),
+ // but javac gets confused by that
+ @SuppressWarnings("unchecked")
private static <K, T extends DMObject<K>> SingleResourcePropertyHolder<K,T,?,?>
newSingleResourcePropertyHolderHack(DMClassHolder<K,T> classHolder, CtMethod ctMethod, DMClassInfo<?,? extends DMObject<?>> classInfo,
DMProperty property, DMFilter filter, ViewerDependent viewerDependent) {
@@ -343,11 +370,13 @@
}
private static <K, T extends DMObject<K>> DMPropertyHolder<K,T,?> createResourcePropertyHolder(DMClassHolder<K,T> classHolder, CtMethod ctMethod,
- DMClassInfo<?,? extends DMObject<?>> classInfo, DMProperty property, DMFilter filter, ViewerDependent viewerDependent, boolean listValued, boolean setValued) {
+ DMClassInfo<?,? extends DMObject<?>> classInfo, DMProperty property, DMFilter filter, ViewerDependent viewerDependent, boolean listValued, boolean setValued, boolean feedValued) {
if (listValued)
return newListResourcePropertyHolderHack(classHolder, ctMethod, classInfo, property, filter, viewerDependent);
else if (setValued)
return newSetResourcePropertyHolderHack(classHolder, ctMethod, classInfo, property, filter, viewerDependent);
+ else if (feedValued)
+ return newFeedPropertyHolderHack(classHolder, ctMethod, classInfo, property, filter, viewerDependent);
else
return newSingleResourcePropertyHolderHack(classHolder, ctMethod, classInfo, property, filter, viewerDependent);
}
@@ -434,6 +463,17 @@
}
public abstract void visitChildren(DMSession session, Fetch<?,?> children, T object, FetchVisitor visitor);
+
+ /**
+ *
+ * @param session
+ * @param object
+ * @param visitor
+ * @param forceEmpty if True, call FetchVisitor.emptyProperty() on a missing property even if we
+ * would normally omit it. This is used for a single-valued resource property, where we normally
+ * don't emit any fetch result for missing properties, but must do so if we are notifying
+ * that the property has gone away.
+ */
public abstract void visitProperty(DMSession session, T object, FetchVisitor visitor, boolean forceEmpty);
public abstract Fetch<?,?> getDefaultChildren();
Added: dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/FeedPropertyHolder.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/FeedPropertyHolder.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/FeedPropertyHolder.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -0,0 +1,187 @@
+package com.dumbhippo.dm.schema;
+
+import java.util.Iterator;
+
+import javassist.CtMethod;
+
+import com.dumbhippo.dm.Cardinality;
+import com.dumbhippo.dm.DMClient;
+import com.dumbhippo.dm.DMFeed;
+import com.dumbhippo.dm.DMFeedItem;
+import com.dumbhippo.dm.DMObject;
+import com.dumbhippo.dm.DMSession;
+import com.dumbhippo.dm.DMViewpoint;
+import com.dumbhippo.dm.DataModel;
+import com.dumbhippo.dm.annotations.DMFilter;
+import com.dumbhippo.dm.annotations.DMProperty;
+import com.dumbhippo.dm.annotations.PropertyType;
+import com.dumbhippo.dm.annotations.ViewerDependent;
+import com.dumbhippo.dm.fetch.Fetch;
+import com.dumbhippo.dm.fetch.FetchVisitor;
+import com.dumbhippo.dm.filter.AndFilter;
+import com.dumbhippo.dm.filter.CompiledItemFilter;
+import com.dumbhippo.dm.filter.Filter;
+import com.dumbhippo.dm.filter.FilterCompiler;
+import com.dumbhippo.dm.store.DMStore;
+import com.dumbhippo.dm.store.StoreKey;
+
+public class FeedPropertyHolder<K, T extends DMObject<K>, KI, TI extends DMObject<KI>> extends ResourcePropertyHolder<K,T,KI,TI> {
+ private CompiledItemFilter<K,T,KI,TI> itemFilter;
+
+ public FeedPropertyHolder(DMClassHolder<K,T> declaringClassHolder, CtMethod ctMethod, DMClassInfo<KI,TI> classInfo, DMProperty annotation, DMFilter filter, ViewerDependent viewerDependent) {
+ super(declaringClassHolder, ctMethod, classInfo, annotation, filter, viewerDependent);
+ }
+
+ @Override
+ public void complete() {
+ super.complete();
+
+ Filter classFilter = getResourceClassHolder().getUncompiledItemFilter();
+ if (classFilter != null && propertyFilter == null) {
+ itemFilter = getResourceClassHolder().getItemFilter();
+ } else if (classFilter != null || propertyFilter != null) {
+ Filter toCompile;
+ if (classFilter != null)
+ toCompile = new AndFilter(classFilter, propertyFilter);
+ else
+ toCompile = propertyFilter;
+
+ itemFilter = FilterCompiler.compileItemFilter(declaringClassHolder.getModel(),
+ declaringClassHolder.getKeyClass(),
+ keyType, toCompile);
+ }
+ }
+
+ @Override
+ protected PropertyType getType() {
+ return PropertyType.RESOURCE;
+ }
+
+ @Override
+ public String getUnboxPrefix() {
+ return "(com.dumbhippo.dm.DMFeed)";
+ }
+
+ @Override
+ public String getUnboxSuffix() {
+ return "";
+ }
+
+ @Override
+ public Object dehydrate(Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object rehydrate(DMViewpoint viewpoint, K key, Object value, DMSession session, boolean filter) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Object filter(DMViewpoint viewpoint, K key, Object value) {
+ throw new UnsupportedOperationException();
+ }
+
+ private long updateFeedTimestamp(DMSession session, T object) {
+ DMClient client = session.getClient();
+
+ if (client != null) {
+ DMClassHolder classHolder = object.getClassHolder();
+ int feedPropertyIndex = classHolder.getFeedPropertyIndex(getName());
+ DataModel model = classHolder.getModel();
+ DMStore store = model.getStore();
+
+ @SuppressWarnings("unchecked")
+ StoreKey<K, T> storeKey = (StoreKey<K, T>)object.getStoreKey();
+
+ return store.updateFeedTimestamp(storeKey, feedPropertyIndex, client.getStoreClient(), model.getTimestamp());
+ } else {
+ return -1;
+ }
+ }
+
+ /**
+ * Like DMPropertyHolder.visitChildren(), but specialized for feeds. Recurses and visits
+ * children of the feed.
+ *
+ * @param session
+ * @param children
+ * @param start start position in feed to visit from
+ * @param max maximum of number of items to visit
+ * @param object the object whose property to visit
+ * @param visitor
+ * @param forceAll if true, fetch everything specified by start and max, even if we have a timestamp
+ * indicating that we have already returned the current contents of the feed to the client
+ */
+ @SuppressWarnings("unchecked")
+ public long visitFeedChildren(DMSession session, Fetch<?,?> children, int start, int max, T object, FetchVisitor visitor, boolean forceAll) {
+ long minTimestamp = updateFeedTimestamp(session, object);
+ if (forceAll)
+ minTimestamp = 0;
+
+ Fetch<KI,TI> typedChildren = (Fetch<KI,TI>)children;
+
+ Iterator<DMFeedItem<TI>> iter = ((DMFeed<TI>)getRawPropertyValue(object)).iterator(start, max, minTimestamp);
+ while (iter.hasNext()) {
+ DMFeedItem<TI> item = iter.next();
+ visitChild(session, typedChildren, item.getValue(), visitor);
+ }
+
+ return minTimestamp;
+ }
+
+ /**
+ * Like DMPropertyHolder.visitProperty(), but specialized for feeds. Calls the feedProperty()
+ * method of the visitor for each visited value in the property.
+ *
+ * @param session
+ * @param start start position in feed to visit from
+ * @param max maximum of number of items to visit
+ * @param object the object whose property to visit
+ * @param visitor
+ * @param minTimestamp minimum timestamp for items to fetch. A value of -1 means to compute
+ * this value from what is stored for this client and then update the stored information
+ * for subsequent fetches. Any other value is used literally without updating the stored
+ * information. (So, '0' can be used to force fetch everything)
+ */
+ @SuppressWarnings("unchecked")
+ public void visitFeedProperty(DMSession session, int start, int max, T object, FetchVisitor visitor, long minTimestamp) {
+ boolean seenAny = false;
+
+ if (minTimestamp < 0)
+ minTimestamp = updateFeedTimestamp(session, object);
+
+ boolean fetchingAll = start == 0 && minTimestamp <= 0;
+
+ Iterator<DMFeedItem<TI>> iter = ((DMFeed<TI>)getRawPropertyValue(object)).iterator(start, max, minTimestamp);
+ while (iter.hasNext()) {
+ DMFeedItem<TI> item = iter.next();
+ seenAny = true;
+ visitor.feedProperty(this, item.getValue().getKey(), item.getTime(), !fetchingAll);
+ }
+
+ if (!seenAny && fetchingAll) {
+ visitor.emptyProperty(this);
+ }
+ }
+
+
+ @Override
+ public void visitChildren(DMSession session, Fetch<?, ?> children, T object, FetchVisitor visitor) {
+ throw new UnsupportedOperationException("Must use visitFeedChildren");
+ }
+
+ @Override
+ public void visitProperty(DMSession session, T object, FetchVisitor visitor, boolean forceEmpty) {
+ throw new UnsupportedOperationException("Must use visitFeedProperty");
+ }
+
+ @Override
+ public Cardinality getCardinality() {
+ return Cardinality.ANY;
+ }
+
+ public CompiledItemFilter<K, T, KI, TI> getItemFilter() {
+ return itemFilter;
+ }
+}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/ResourcePropertyHolder.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/ResourcePropertyHolder.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/schema/ResourcePropertyHolder.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -100,7 +100,7 @@
}
@SuppressWarnings("unchecked")
- protected TI rehydrateDMO(Object value, DMSession session) {
+ public TI rehydrateDMO(Object value, DMSession session) {
@SuppressWarnings("unchecked")
KI key = (KI)value;
return session.findUnchecked(objectType, key);
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/store/DMStore.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/store/DMStore.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/store/DMStore.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -7,6 +7,7 @@
import org.slf4j.Logger;
import com.dumbhippo.GlobalSetup;
+import com.dumbhippo.dm.CachedFeed;
import com.dumbhippo.dm.ClientMatcher;
import com.dumbhippo.dm.ClientNotificationSet;
import com.dumbhippo.dm.DMClient;
@@ -103,6 +104,17 @@
// It's not a problem if the node is evicted; the data just won't be stored this time around
node.store(propertyIndex, value, timestamp);
}
+
+ /**
+ * Get a CachedFeed object for the specified property. If one doesn't already exist in the cache,
+ * it will be created and stored there. However, if the passed-in-timestamp is older than
+ * the invalidation timestamp for the property, null is returned.
+ */
+ public <K, T extends DMObject<K>> CachedFeed<?> getOrCreateCachedFeed(StoreKey<K,T> key, int propertyIndex, long timestamp) {
+ StoreNode<K,T> node = ensureNode(key);
+
+ return node.getOrCreateCachedFeed(propertyIndex, timestamp);
+ }
public <K, T extends DMObject<K>> void invalidate(DMClassHolder<K,T> classHolder, K key, int propertyIndex, long timestamp) {
// We need to make sure that we store the timestamp to deal with in-flight
@@ -129,6 +141,25 @@
} while (node.isEvicted());
}
+ /**
+ * Invalidate the cache when a feed-valued property is changed. In addition to the normal actions
+ * of clearing cached values and updating the stored invalidation timestamp, we also maintain
+ * a log of the changed item timestamps by txTimestamp so we can tell which items must be resent
+ * in the resulting notification.
+ *
+ * @param txTimestamp transaction timestamp *after* the read-write transaction that modified the feed
+ * @param itemTimestamp minimum new feed-item timestamp for all the items modified in the feed; a value
+ * of -1 should be used if the change involved deleting items.
+ */
+ public <K, T extends DMObject<K>> void invalidateFeed(DMClassHolder<K,T> classHolder, K key, int propertyIndex, long txTimestamp, long itemTimestamp) {
+ // See comment above
+ StoreNode<K,T> node;
+ do {
+ node = ensureNode(classHolder, key);
+ node.invalidateFeed(propertyIndex, txTimestamp, itemTimestamp);
+ } while (node.isEvicted());
+ }
+
public <K, T extends DMObject<K>> void resolveNotifications(DMClassHolder<K,T> classHolder, K key, long propertyMask, ClientNotificationSet result, ClientMatcher matcher) {
StoreNode<K,T> node = getNode(classHolder, key);
if (node == null)
@@ -164,6 +195,18 @@
return oldFetch;
}
+ public <K, T extends DMObject<K>> long updateFeedTimestamp(StoreKey<K, T> storeKey, int feedPropertyIndex, StoreClient client, long newTimestamp) {
+ StoreNode<K,T> node;
+ Registration<K,T> registration;
+
+ do {
+ node = ensureNode(storeKey);
+ registration = node.createRegistration(client);
+ } while (registration == null);
+
+ return registration.updateFeedTimestamp(feedPropertyIndex, newTimestamp);
+ }
+
public <K, T extends DMObject<K>> void removeRegistration(DMClassHolder<K,T> classHolder, K key, StoreClient client) {
StoreNode<K,T> node = getNode(classHolder, key);
if (node == null)
Added: dumbhippo/trunk/server/src/com/dumbhippo/dm/store/FeedLog.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/store/FeedLog.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/store/FeedLog.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -0,0 +1,79 @@
+package com.dumbhippo.dm.store;
+
+/**
+ * This class maintains a log that indicates the (minimum) item timestamp of
+ * an entry in a feed that changed for each transaction timestamp. This can
+ * be used to determine what portion of a feed needs to be resent to a consumer
+ * of the feed, if we know the txTimestamp of the transaction that was last
+ * used to update them on the feed state.
+ *
+ * Note that because the way txTimestamps work the resending is conservative: we
+ * can tell what items might have changed after the start of the given transaction,
+ * but we can't tell if they actually did or not.
+ *
+ * An itemTimestamp of -1 in the log has a special significance: it means that
+ * the feed changed in some other way than inserting or restacking a feed item,
+ * so the entire feed will need to be resent from scratch.
+ *
+ * @author otaylor
+ */
+public class FeedLog {
+ private static final int INITIALIZE_SIZE = 10;
+
+ private long txTimestamps[];
+ private long itemTimestamps[];
+ private int entryCount = 0;
+
+ public synchronized void addEntry(long txTimestamp, long itemTimestamp) {
+ int insertionPos = entryCount;
+ while (insertionPos > 0 && txTimestamps[insertionPos - 1] >= txTimestamp)
+ insertionPos--;
+
+ /* If the txTimestamp is the same as one already in the log, we just replace
+ * the item timestamp for that log entry.
+ */
+ if (insertionPos < entryCount && txTimestamps[insertionPos] == txTimestamp) {
+ if (itemTimestamps[insertionPos] > itemTimestamp)
+ itemTimestamps[insertionPos] = itemTimestamp;
+ return;
+ }
+
+ if (entryCount == 0) {
+ txTimestamps = new long[INITIALIZE_SIZE];
+ itemTimestamps = new long[INITIALIZE_SIZE];
+ } else if (entryCount == txTimestamps.length) {
+ int newSize = entryCount * 2;
+ if (newSize < 0)
+ throw new OutOfMemoryError();
+
+ long newTxTimestamps[] = new long[newSize];
+ System.arraycopy(txTimestamps, 0, newTxTimestamps, 0, entryCount);
+ txTimestamps = newTxTimestamps;
+
+ long newItemTimestamps[] = new long[newSize];
+ System.arraycopy(itemTimestamps, 0, newItemTimestamps, 0, entryCount);
+ itemTimestamps = newItemTimestamps;
+ }
+
+ System.arraycopy(txTimestamps, insertionPos, txTimestamps, insertionPos + 1, entryCount - insertionPos);
+ System.arraycopy(itemTimestamps, insertionPos, itemTimestamps, insertionPos + 1, entryCount - insertionPos);
+ entryCount++;
+
+ txTimestamps[insertionPos] = txTimestamp;
+ itemTimestamps[insertionPos] = itemTimestamp;
+ }
+
+ public synchronized long getMinItemTimestamp(long txTimestamp) {
+ long minTimestamp = Long.MAX_VALUE;
+
+ for (int i = entryCount - 1; i >= 0; i--) {
+ if (txTimestamps[i] < txTimestamp)
+ break;
+
+ if (itemTimestamps[i] < minTimestamp)
+ minTimestamp = itemTimestamps[i];
+ }
+
+ return minTimestamp;
+ }
+}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/store/Registration.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/store/Registration.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/store/Registration.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -7,6 +7,7 @@
private StoreClient client;
private StoreNode<K,T> node;
private Fetch<K,? super T> fetch;
+ private long[] feedTxTimestamps;
public Registration(StoreNode<K,T> node, StoreClient client) {
this.node = node;
@@ -25,7 +26,8 @@
Fetch<K,? super T> oldFetch = fetch;
if (oldFetch != null) {
- Fetch<K, ? super T> mergedFetch = oldFetch.merge(newFetch);
+ @SuppressWarnings("unchecked")
+ Fetch<K, ? super T> mergedFetch = (Fetch<K, ? super T>)oldFetch.merge(newFetch);
fetch = mergedFetch;
} else
fetch = newFetch;
@@ -40,4 +42,44 @@
public synchronized Fetch<K,? super T> getFetch() {
return fetch;
}
+
+ public long updateFeedTimestamp(int feedPropertyIndex, long newTimestamp) {
+ /* We can have the situation where:
+ *
+ * T1) Notification updates the timestamp and fetches new items
+ * T2) Fetch finds no new items with the updated timestamp
+ * T2) Fetch returns nothing
+ * T1) Notification is sent out
+ *
+ */
+ long oldTimestamp;
+
+ // Synchronizing the entire method would risk a deadlock, because of the
+ // call to the synchronized StoreNode.getFeedLock() below
+ synchronized(this) {
+ if (feedTxTimestamps == null) {
+ feedTxTimestamps = new long[node.getClassHolder().getFeedPropertiesCount()];
+ for (int i = 0; i < feedTxTimestamps.length; i++)
+ feedTxTimestamps[i] = -1;
+ }
+
+ oldTimestamp = feedTxTimestamps[feedPropertyIndex];
+ feedTxTimestamps[feedPropertyIndex] = newTimestamp;
+ }
+
+ long result;
+ if (oldTimestamp == -1) {
+ // -1 is the "haven't previously updated the timestamp" timestamp, so we use 0
+ // to signal "everything"
+ result = 0;
+ } else {
+ FeedLog log = node.getFeedLog(feedPropertyIndex);
+ if (log != null)
+ result = log.getMinItemTimestamp(oldTimestamp);
+ else
+ result = Long.MAX_VALUE;
+ }
+
+ return result;
+ }
}
Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/store/StoreNode.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/store/StoreNode.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/store/StoreNode.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -8,11 +8,13 @@
import org.slf4j.Logger;
import com.dumbhippo.GlobalSetup;
+import com.dumbhippo.dm.CachedFeed;
import com.dumbhippo.dm.ClientMatcher;
import com.dumbhippo.dm.ClientNotificationSet;
import com.dumbhippo.dm.DMObject;
import com.dumbhippo.dm.NotCachedException;
import com.dumbhippo.dm.schema.DMClassHolder;
+import com.dumbhippo.dm.schema.DMPropertyHolder;
public class StoreNode<K,T extends DMObject<K>> extends StoreKey<K,T> {
@SuppressWarnings("unused")
@@ -23,6 +25,7 @@
private long timestamp = -1;
private Object[] properties;
private List<Registration<K,T>> registrations;
+ private FeedLog[] feedLogs;
private boolean evicted;
StoreNode(DMClassHolder<K,T> classHolder, K key) {
@@ -46,11 +49,38 @@
properties[propertyIndex] = value != null ? value : nil;
}
+ public synchronized CachedFeed<?> getOrCreateCachedFeed(int propertyIndex, long timestamp) {
+ if (this.timestamp > timestamp)
+ return null;
+
+ if (properties[propertyIndex] == null) {
+ // Actual key type is irrelevant
+ properties[propertyIndex] = new CachedFeed<Object>();
+ }
+
+ return (CachedFeed<?>)properties[propertyIndex];
+ }
+
public synchronized void invalidate(int propertyIndex, long timestamp) {
this.timestamp = timestamp;
properties[propertyIndex] = null;
}
+ public synchronized void invalidateFeed(int propertyIndex, long txTimestamp, long itemTimestamp) {
+ invalidate(propertyIndex, timestamp);
+
+ DMPropertyHolder<K, T, ?> property = classHolder.getProperty(propertyIndex);
+ int feedPropertyIndex = classHolder.getFeedPropertyIndex(property.getName());
+
+ if (feedLogs == null)
+ feedLogs = new FeedLog[classHolder.getFeedPropertiesCount()];
+
+ if (feedLogs[feedPropertyIndex] == null)
+ feedLogs[feedPropertyIndex] = new FeedLog();
+
+ feedLogs[feedPropertyIndex].addEntry(txTimestamp, itemTimestamp);
+ }
+
public synchronized Collection<Registration<K,T>> markEvicted() {
evicted = true;
return registrations;
@@ -118,4 +148,11 @@
registration.getFetch().resolveNotifications(registration.getClient(), this, properties, result);
}
}
+
+ public FeedLog getFeedLog(int feedPropertyIndex) {
+ if (feedLogs == null)
+ return null;
+ else
+ return feedLogs[feedPropertyIndex];
+ }
}
Added: dumbhippo/trunk/server/tests/com/dumbhippo/dm/AbstractFetchTests.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/AbstractFetchTests.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/AbstractFetchTests.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -0,0 +1,134 @@
+package com.dumbhippo.dm;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+
+import com.dumbhippo.GlobalSetup;
+import com.dumbhippo.XmlBuilder;
+import com.dumbhippo.dm.fetch.Fetch;
+import com.dumbhippo.dm.fetch.FetchNode;
+import com.dumbhippo.dm.parser.FetchParser;
+import com.dumbhippo.dm.parser.ParseException;
+
+public class AbstractFetchTests extends AbstractSupportedTests {
+ static private final Logger logger = GlobalSetup.getLogger(AbstractFetchTests.class);
+
+ private Map<String, FetchResult> expectedResults;
+ private String filename;
+
+ protected AbstractFetchTests(String filename) {
+ this.filename = filename;
+ }
+
+ @Override
+ protected void setUp() {
+ super.setUp();
+
+ if (expectedResults == null)
+ readFetchResults();
+ }
+
+ private void readFetchResults() {
+ expectedResults = new HashMap<String, FetchResult>();
+
+ URL resource = this.getClass().getResource("/" + filename);
+ if (resource == null)
+ throw new RuntimeException("Cannot find " + filename);
+
+ try {
+ InputStream input = resource.openStream();
+ for (FetchResult result : FetchResultHandler.parse(input)) {
+ expectedResults.put(result.getId(), result);
+ }
+ input.close();
+ } catch (IOException e) {
+ throw new RuntimeException("Error reading " + filename, e);
+ } catch (SAXException e) {
+ if (e instanceof SAXParseException) {
+ SAXParseException pe = (SAXParseException)e;
+ logger.error("{}:{}:{}: {}",
+ new Object[] { filename, pe.getLineNumber(), pe.getColumnNumber(), pe.getMessage() });
+ throw new RuntimeException("Cannot parse " + filename, e);
+ }
+
+ throw new RuntimeException("Error parsing " + filename, e);
+ }
+ }
+
+ // Basic test of the test infrastructure; load all the fetch results, test
+ // that they can be converted to XML and back and that the result of the
+ // round-trip validates as the same thing as the original.
+ public void testRoundTrip() {
+ for (FetchResult result : expectedResults.values()) {
+ XmlBuilder builder = new XmlBuilder();
+ builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+ builder.openElement("fetchResults");
+ result.writeToXmlBuilder(builder);
+ builder.closeElement();
+
+ String asString = builder.toString();
+
+ FetchResult roundTripped;
+ try {
+ InputStream input = new ByteArrayInputStream(asString.getBytes("UTF-8"));
+ List<FetchResult> results = FetchResultHandler.parse(input);
+ if (results.size() != 1)
+ throw new RuntimeException("Round-trip of " + result.getId() + " to FetchResult gave " + results.size() + " results!");
+ roundTripped = results.get(0);
+ input.close();
+
+ } catch (IOException e) {
+ throw new RuntimeException("Error parsing recoverted " + result.getId(), e);
+ } catch (SAXException e) {
+ if (e instanceof SAXParseException) {
+ SAXParseException pe = (SAXParseException)e;
+ logger.error("<reconverted>:{}:{}: {}",
+ new Object[] { pe.getLineNumber(), pe.getColumnNumber(), pe.getMessage() });
+ throw new RuntimeException("Cannot parse recoverted " + result.getId(), e);
+ }
+
+ throw new RuntimeException("Error parsing recoverted " + result.getId(), e);
+ }
+
+ try {
+ roundTripped.validateAgainst(result);
+ } catch (FetchValidationException e) {
+ throw new RuntimeException("Round-trip of " + result.getId() + " didn't validate " + e, e);
+ }
+ }
+ }
+
+ protected FetchResult getExpected(String resultId, String... parameters) {
+ Map<String, String> parametersMap = new HashMap<String, String>();
+ for (int i = 0; i < parameters.length; i += 2)
+ parametersMap.put(parameters[i], parameters[i + 1]);
+
+ FetchResult raw = expectedResults.get(resultId);
+ if (raw == null)
+ throw new RuntimeException("No expected result set with id='" + resultId + "'");
+
+ return raw.substitute(parametersMap);
+ }
+
+ protected <K,T extends DMObject<K>> void doFetchTest(Class<K> keyClass, Class<T> objectClass, T object, String fetchString, String resultId, String... parameters) throws ParseException, FetchValidationException {
+ FetchNode fetchNode = FetchParser.parse(fetchString);
+ Fetch<K,T> fetch = fetchNode.bind(support.getModel().getClassHolder(keyClass, objectClass));
+
+ FetchResultVisitor visitor = new FetchResultVisitor();
+ support.currentSessionRO().visitFetch(object, fetch, visitor);
+
+ FetchResult expected = getExpected(resultId, parameters);
+
+ logger.debug("Result for {} is {}", resultId, visitor.getResult());
+ visitor.getResult().validateAgainst(expected);
+ }
+}
Modified: dumbhippo/trunk/server/tests/com/dumbhippo/dm/AllTests.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/AllTests.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/AllTests.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -23,6 +23,8 @@
suite.addTest(new TestSuite(ThreadingTests.class));
suite.addTest(new TestSuite(FilterTests.class));
+ suite.addTest(new TestSuite(FeedTests.class));
+
return suite;
}
}
Added: dumbhippo/trunk/server/tests/com/dumbhippo/dm/FeedTests.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/FeedTests.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/FeedTests.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -0,0 +1,183 @@
+package com.dumbhippo.dm;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Iterator;
+
+import javax.persistence.EntityManager;
+
+import org.slf4j.Logger;
+
+import com.dumbhippo.GlobalSetup;
+import com.dumbhippo.dm.dm.TestBlogEntryDMO;
+import com.dumbhippo.dm.dm.TestUserDMO;
+import com.dumbhippo.dm.persistence.TestBlogEntry;
+import com.dumbhippo.dm.persistence.TestUser;
+import com.dumbhippo.identity20.Guid;
+
+public class FeedTests extends AbstractFetchTests {
+ @SuppressWarnings("unused")
+ static private final Logger logger = GlobalSetup.getLogger(FeedTests.class);
+
+ private static DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
+
+ private static Date parseDate(String str) {
+ try {
+ return DATE_FORMAT.parse(str);
+ } catch (ParseException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Date DATE1 = parseDate("2006-06-23 16:01:32 -0400");
+ private static Date DATE2 = parseDate("2006-11-14 15:31:52 -0400");
+ private static Date DATE3 = parseDate("2007-03-23 09:44:04 -0400");
+
+ public FeedTests() {
+ super("feed-fetch-tests.xml");
+ }
+
+ private void createData(Guid bobId) {
+ EntityManager em = support.beginTransaction();
+
+ TestUser bob = new TestUser("Bob");
+ bob.setId(bobId.toString());
+ em.persist(bob);
+
+ TestBlogEntry entry1 = new TestBlogEntry(bob, 1, DATE1);
+ entry1.setTitle("My Life");
+ em.persist(entry1);
+
+ TestBlogEntry entry2 = new TestBlogEntry(bob, 2, DATE2);
+ entry2.setTitle("Stupid Alligator Tricks");
+ em.persist(entry2);
+
+ em.getTransaction().commit();
+ }
+
+ private void addBlogEntry(Guid bobId) {
+ EntityManager em = support.beginSessionRW(new TestViewpoint(bobId));
+
+ TestUser bob = em.find(TestUser.class, bobId.toString());
+
+ TestBlogEntry entry3 = new TestBlogEntry(bob, 3, DATE3);
+ entry3.setTitle("My 9-fingered Life");
+ em.persist(entry3);
+
+ support.currentSessionRW().feedChanged(TestUserDMO.class, bob.getGuid(), "blogEntries", DATE3.getTime());
+
+ em.getTransaction().commit();
+ }
+
+ public void testBasicFeed() throws Exception {
+ TestViewpoint viewpoint = new TestViewpoint(Guid.createNew());
+ EntityManager em;
+ ReadOnlySession session;
+
+ /////////////////////////////////////////////////
+ // Setup
+
+ Guid bobId = Guid.createNew();
+
+ createData(bobId);
+
+ //////////////////////////////////////////////////
+
+ em = support.beginSessionRO(viewpoint);
+ session = support.currentSessionRO();
+
+ TestUserDMO bobDMO = session.find(TestUserDMO.class, bobId);
+ assertEquals("Bob", bobDMO.getName());
+
+ DMFeed<TestBlogEntryDMO> blogEntries = bobDMO.getBlogEntries();
+
+ Iterator<DMFeedItem<TestBlogEntryDMO>> iter;
+
+ // Fetch just one item
+ iter = blogEntries.iterator(0, 1, -1);
+ assertEquals(iter.next().getValue().getTimestamp(), DATE2.getTime());
+ assertFalse(iter.hasNext());
+
+ // Fetch all the items, the first will be cached, the second newly fetched
+ iter = blogEntries.iterator(0, 10, -1);
+ assertEquals(iter.next().getValue().getTimestamp(), DATE2.getTime());
+ assertEquals(iter.next().getValue().getTimestamp(), DATE1.getTime());
+ assertFalse(iter.hasNext());
+
+ em.getTransaction().commit();
+
+ //////////////////////////////////////////////////
+
+ // Try adding another entry, check if we see the proper new contents
+
+ addBlogEntry(bobId);
+
+ em = support.beginSessionRO(viewpoint);
+ session = support.currentSessionRO();
+
+ bobDMO = session.find(TestUserDMO.class, bobId);
+ assertEquals("Bob", bobDMO.getName());
+
+ blogEntries = bobDMO.getBlogEntries();
+
+ iter = blogEntries.iterator(0, 10, -1);
+ assertEquals(iter.next().getValue().getTimestamp(), DATE3.getTime());
+ assertEquals(iter.next().getValue().getTimestamp(), DATE2.getTime());
+ assertEquals(iter.next().getValue().getTimestamp(), DATE1.getTime());
+ assertFalse(iter.hasNext());
+
+ em.getTransaction().commit();
+ }
+
+ public void testFeedFetch() throws Exception {
+ TestViewpoint viewpoint = new TestViewpoint(Guid.createNew());
+ TestDMClient client = new TestDMClient(support.getModel(), viewpoint.getViewerId());
+ EntityManager em;
+ ReadOnlySession session;
+
+ /////////////////////////////////////////////////
+ // Setup
+
+ Guid bobId = Guid.createNew();
+
+ createData(bobId);
+
+ //////////////////////////////////////////////////
+
+ em = support.beginSessionRO(client);
+ session = support.currentSessionRO();
+
+ TestUserDMO bobDMO = session.find(TestUserDMO.class, bobId);
+ assertEquals("Bob", bobDMO.getName());
+
+ // Fetch just one item (the defaultMaxFetch for this property)
+ doFetchTest(Guid.class, TestUserDMO.class, bobDMO, "blogEntries title", "bobsFirstFeed",
+ "bob", bobId.toString());
+
+ // Fetch all available items
+ doFetchTest(Guid.class, TestUserDMO.class, bobDMO, "blogEntries(max=10) title", "bobsOlderFeed",
+ "bob", bobId.toString());
+
+ // Fetching again with the same maximum should give us nothing.
+ doFetchTest(Guid.class, TestUserDMO.class, bobDMO, "blogEntries(max=10) title", "bobsAlreadyFetchedFeed",
+ "bob", bobId.toString());
+
+ em.getTransaction().commit();
+
+
+ // Add an entry, see if we get the right notification
+
+ addBlogEntry(bobId);
+
+ support.getModel().waitForAllNotifications();
+
+ assertNotNull(client.getLastNotification());
+
+ logger.debug("Notification from addition of feed entry is {}", client.getLastNotification());
+
+ FetchResult expected = getExpected("bobsFeedNotification", "bob", bobId.toString());
+ client.getLastNotification().validateAgainst(expected);
+ }
+}
Modified: dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchParserTests.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchParserTests.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchParserTests.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -17,6 +17,7 @@
expectIdentity("contact +");
expectIdentity("name;contact [name;photoUrl]");
expectIdentity("name(notify=false) [name;photoUrl(notify=false)]");
+ expectIdentity("blogEntries(max=10) [title;description]");
expectIdentity("a;b [a;b [a;b]]");
expectSuccess("member()[]", "member");
Modified: dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchResultHandler.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchResultHandler.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchResultHandler.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -161,18 +161,25 @@
private void startPropertyElement(String uri, String localName, Attributes attributes) throws SAXParseException {
String resourceId = null;
+ long timestamp = -1;
for (int i = 0; i < attributes.getLength(); i++) {
if (MUGSHOT_SYSTEM_NS.equals(attributes.getURI(i))) {
String name = attributes.getLocalName(i);
if ("resourceId".equals(name))
resourceId = resolveResourceId(attributes.getValue(i));
+ else if ("ts".equals(name))
+ timestamp = Long.parseLong(attributes.getValue(i));
+
}
}
- if (resourceId != null)
- currentProperty = FetchResultProperty.createResource(localName, uri, resourceId);
- else {
+ if (resourceId != null) {
+ if (timestamp >= 0)
+ currentProperty = FetchResultProperty.createFeed(localName, uri, resourceId, timestamp);
+ else
+ currentProperty = FetchResultProperty.createResource(localName, uri, resourceId);
+ } else {
currentProperty = FetchResultProperty.createSimple(localName, uri);
currentChars = new StringBuilder();
}
Modified: dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchResultProperty.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchResultProperty.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchResultProperty.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -10,21 +10,27 @@
private String propertyId;
private String value;
private boolean resource;
+ private long timestamp;
static public FetchResultProperty createSimple(String name, String namespace) {
- return new FetchResultProperty(name, namespace, null, false);
+ return new FetchResultProperty(name, namespace, null, false, 1);
}
static public FetchResultProperty createResource(String name, String namespace, String resourceId) {
- return new FetchResultProperty(name, namespace, resourceId, true);
+ return new FetchResultProperty(name, namespace, resourceId, true, -1);
}
- private FetchResultProperty(String name, String namespace, String value, boolean resource) {
+ public static FetchResultProperty createFeed(String name, String namespace, String resourceId, long timestamp) {
+ return new FetchResultProperty(name, namespace, resourceId, true, timestamp);
+ }
+
+ private FetchResultProperty(String name, String namespace, String value, boolean resource, long timestamp) {
this.name = name;
this.namespace = namespace;
this.propertyId = namespace + "#" + name;
this.value = value;
this.resource = resource;
+ this.timestamp = timestamp;
}
public String getName() {
@@ -47,10 +53,16 @@
return resource;
}
+ public long getTimestamp() {
+ return timestamp;
+ }
+
public void writeToXmlBuilder(XmlBuilder builder, String defaultNS) {
String ns = defaultNS.equals(namespace) ? null : namespace;
- if (resource)
+ if (resource && timestamp >= 0)
+ builder.appendEmptyNode(name, "m:resourceId", value, "m:ts", Long.toString(timestamp), "xmlns", ns);
+ else if (resource)
builder.appendEmptyNode(name, "m:resourceId", value, "xmlns", ns);
else
builder.appendTextNode(name, value, "xmlns", ns);
@@ -65,12 +77,18 @@
if (resource != other.resource)
throw new FetchValidationException("%s:%s expected isResource %s, got %s", context, propertyId, other.resource, resource);
+
+ if (timestamp != other.timestamp)
+ throw new FetchValidationException("%s:%s expected timestamp %ld, got %ld", context, propertyId, other.timestamp, timestamp);
}
public FetchResultProperty substitute(Map<String, String> parametersMap) {
if (resource) {
String newResourceId = FetchResultResource.substituteResourceId(value, parametersMap);
- return createResource(name, namespace, newResourceId);
+ if (timestamp >= 0)
+ return createFeed(name, namespace, newResourceId, timestamp);
+ else
+ return createResource(name, namespace, newResourceId);
} else {
return this;
}
Modified: dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchResultVisitor.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchResultVisitor.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchResultVisitor.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -41,6 +41,14 @@
currentResource.addProperty(property);
}
+ public <KP, TP extends DMObject<KP>> void feedProperty(ResourcePropertyHolder<?, ?, KP, TP> propertyHolder, KP key, long timestamp, boolean incremental) {
+ DMClassHolder<KP,TP> classHolder = propertyHolder.getResourceClassHolder();
+ String resourceId = classHolder.makeResourceId(key);
+
+ FetchResultProperty property = FetchResultProperty.createFeed(propertyHolder.getName(), propertyHolder.getNameSpace(), resourceId, timestamp);
+ currentResource.addProperty(property);
+ }
+
public FetchResult getResult() {
return result;
}
Modified: dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchTests.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchTests.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchTests.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -1,144 +1,23 @@
package com.dumbhippo.dm;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URL;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
import javax.persistence.EntityManager;
import org.slf4j.Logger;
-import org.xml.sax.SAXException;
-import org.xml.sax.SAXParseException;
import com.dumbhippo.GlobalSetup;
-import com.dumbhippo.XmlBuilder;
import com.dumbhippo.dm.dm.TestGroupDMO;
-import com.dumbhippo.dm.fetch.Fetch;
-import com.dumbhippo.dm.fetch.FetchNode;
-import com.dumbhippo.dm.parser.FetchParser;
-import com.dumbhippo.dm.parser.ParseException;
import com.dumbhippo.dm.persistence.TestGroup;
import com.dumbhippo.dm.persistence.TestGroupMember;
import com.dumbhippo.dm.persistence.TestUser;
-import com.dumbhippo.dm.schema.DMClassHolder;
-import com.dumbhippo.dm.schema.DMClassInfo;
import com.dumbhippo.identity20.Guid;
-public class FetchTests extends AbstractSupportedTests {
+public class FetchTests extends AbstractFetchTests {
static private final Logger logger = GlobalSetup.getLogger(BasicTests.class);
- private Map<String, FetchResult> expectedResults;
- @Override
- protected void setUp() {
- super.setUp();
-
- if (expectedResults == null) {
- expectedResults = new HashMap<String, FetchResult>();
-
- URL resource = this.getClass().getResource("/fetch-tests.xml");
- if (resource == null)
- throw new RuntimeException("Cannot find fetch-tests.xml");
-
- try {
- InputStream input = resource.openStream();
- for (FetchResult result : FetchResultHandler.parse(input)) {
- expectedResults.put(result.getId(), result);
- }
- input.close();
- } catch (IOException e) {
- throw new RuntimeException("Error reading fetch-tests.xml", e);
- } catch (SAXException e) {
- if (e instanceof SAXParseException) {
- SAXParseException pe = (SAXParseException)e;
- logger.error("fetch-tests.xml:{}:{}: {}",
- new Object[] { pe.getLineNumber(), pe.getColumnNumber(), pe.getMessage() });
- throw new RuntimeException("Cannot parse fetch-tests.xml", e);
- }
-
- throw new RuntimeException("Error parsing fetch-tests.xml", e);
- }
- }
+ public FetchTests() {
+ super("fetch-tests.xml");
}
- // Basic test of the test infrastructure; load all the fetch results, test
- // that they can be converted to XML and back and that the result of the
- // round-trip validates as the same thing as the original.
- public void testRoundTrip() {
- for (FetchResult result : expectedResults.values()) {
- XmlBuilder builder = new XmlBuilder();
- builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
- builder.openElement("fetchResults");
- result.writeToXmlBuilder(builder);
- builder.closeElement();
-
- String asString = builder.toString();
-
- FetchResult roundTripped;
- try {
- InputStream input = new ByteArrayInputStream(asString.getBytes("UTF-8"));
- List<FetchResult> results = FetchResultHandler.parse(input);
- if (results.size() != 1)
- throw new RuntimeException("Round-trip of " + result.getId() + " to FetchResult gave " + results.size() + " results!");
- roundTripped = results.get(0);
- input.close();
-
- } catch (IOException e) {
- throw new RuntimeException("Error parsing recoverted " + result.getId(), e);
- } catch (SAXException e) {
- if (e instanceof SAXParseException) {
- SAXParseException pe = (SAXParseException)e;
- logger.error("fetch-tests.xml:{}:{}: {}",
- new Object[] { pe.getLineNumber(), pe.getColumnNumber(), pe.getMessage() });
- throw new RuntimeException("Cannot parse recoverted " + result.getId(), e);
- }
-
- throw new RuntimeException("Error parsing recoverted " + result.getId(), e);
- }
-
- try {
- roundTripped.validateAgainst(result);
- } catch (FetchValidationException e) {
- throw new RuntimeException("Round-trip of " + result.getId() + " didn't validate " + e, e);
- }
- }
- }
-
- public FetchResult getExpected(String resultId, String... parameters) {
- Map<String, String> parametersMap = new HashMap<String, String>();
- for (int i = 0; i < parameters.length; i += 2)
- parametersMap.put(parameters[i], parameters[i + 1]);
-
- FetchResult raw = expectedResults.get(resultId);
- if (raw == null)
- throw new RuntimeException("No expected result set with id='" + resultId + "'");
-
- return raw.substitute(parametersMap);
- }
-
- // Hack to work around generic system
- public <K,T extends DMObject<K>> Fetch<?,?> bindToClass(FetchNode fetchNode, DMClassInfo<K,T> classInfo) {
- @SuppressWarnings("unchecked")
- DMClassHolder<K,T> classHolder = (DMClassHolder<K,T>) support.getModel().getClassHolder(classInfo.getObjectClass());
- return fetchNode.bind(classHolder);
- }
-
- public <K,T extends DMObject<K>> void doTest(Class<K> keyClass, Class<T> objectClass, T object, String fetchString, String resultId, String... parameters) throws ParseException, FetchValidationException {
- FetchNode fetchNode = FetchParser.parse(fetchString);
- Fetch<K,T> fetch = fetchNode.bind(support.getModel().getClassHolder(keyClass, objectClass));
-
- FetchResultVisitor visitor = new FetchResultVisitor();
- support.currentSessionRO().visitFetch(object, fetch, visitor);
-
- FetchResult expected = getExpected(resultId, parameters);
-
- logger.debug("Result for {} is {}", resultId, visitor.getResult());
- visitor.getResult().validateAgainst(expected);
- }
-
private void createData(Guid bobId, Guid janeId, Guid groupId) {
EntityManager em;
@@ -191,7 +70,7 @@
em = support.beginSessionRO(viewpoint);
TestGroupDMO groupDMO = support.currentSessionRO().find(TestGroupDMO.class, groupId);
- doTest(Guid.class, TestGroupDMO.class, groupDMO, "name;members member name", "bobAndJane",
+ doFetchTest(Guid.class, TestGroupDMO.class, groupDMO, "name;members member name", "bobAndJane",
"group", groupId.toString(),
"bob", bobId.toString(),
"jane", janeId.toString());
@@ -218,7 +97,7 @@
em = support.beginSessionRO(viewpoint);
TestGroupDMO groupDMO = support.currentSessionRO().find(TestGroupDMO.class, groupId);
- doTest(Guid.class, TestGroupDMO.class, groupDMO, "+;members +", "bobAndJane",
+ doFetchTest(Guid.class, TestGroupDMO.class, groupDMO, "+;members +", "bobAndJane",
"group", groupId.toString(),
"bob", bobId.toString(),
"jane", janeId.toString());
@@ -246,17 +125,17 @@
em = support.beginSessionRO(client);
TestGroupDMO groupDMO = support.currentSessionRO().find(TestGroupDMO.class, groupId);
- doTest(Guid.class, TestGroupDMO.class, groupDMO, "name", "bobAndJaneSmall",
+ doFetchTest(Guid.class, TestGroupDMO.class, groupDMO, "name", "bobAndJaneSmall",
"group", groupId.toString(),
"bob", bobId.toString(),
"jane", janeId.toString());
- doTest(Guid.class, TestGroupDMO.class, groupDMO, "+;members +", "bobAndJaneRemaining",
+ doFetchTest(Guid.class, TestGroupDMO.class, groupDMO, "+;members +", "bobAndJaneRemaining",
"group", groupId.toString(),
"bob", bobId.toString(),
"jane", janeId.toString());
- doTest(Guid.class, TestGroupDMO.class, groupDMO, "members group", "bobAndJaneAddOn",
+ doFetchTest(Guid.class, TestGroupDMO.class, groupDMO, "members group", "bobAndJaneAddOn",
"group", groupId.toString(),
"bob", bobId.toString(),
"jane", janeId.toString());
@@ -284,7 +163,7 @@
em = support.beginSessionRO(client);
TestGroupDMO groupDMO = support.currentSessionRO().find(TestGroupDMO.class, groupId);
- doTest(Guid.class, TestGroupDMO.class, groupDMO, "+;members group +", "bobAndJaneLoop",
+ doFetchTest(Guid.class, TestGroupDMO.class, groupDMO, "+;members group +", "bobAndJaneLoop",
"group", groupId.toString(),
"bob", bobId.toString(),
"jane", janeId.toString());
@@ -313,7 +192,7 @@
em = support.beginSessionRO(client);
TestGroupDMO groupDMO = support.currentSessionRO().find(TestGroupDMO.class, groupId);
- doTest(Guid.class, TestGroupDMO.class, groupDMO, "name;members member name", "bobAndJane",
+ doFetchTest(Guid.class, TestGroupDMO.class, groupDMO, "name;members member name", "bobAndJane",
"group", groupId.toString(),
"bob", bobId.toString(),
"jane", janeId.toString());
Modified: dumbhippo/trunk/server/tests/com/dumbhippo/dm/TestSupport.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/TestSupport.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/TestSupport.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -10,6 +10,7 @@
import org.hibernate.ejb.HibernateEntityManager;
import org.hibernate.ejb.HibernateEntityManagerFactory;
+import com.dumbhippo.dm.dm.TestBlogEntryDMO;
import com.dumbhippo.dm.dm.TestGroupDMO;
import com.dumbhippo.dm.dm.TestGroupMemberDMO;
import com.dumbhippo.dm.dm.TestUserDMO;
@@ -40,6 +41,7 @@
model.addDMClass(TestUserDMO.class);
model.addDMClass(TestGroupDMO.class);
model.addDMClass(TestGroupMemberDMO.class);
+ model.addDMClass(TestBlogEntryDMO.class);
model.completeDMClasses();
}
Added: dumbhippo/trunk/server/tests/com/dumbhippo/dm/dm/TestBlogEntryDMO.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/dm/TestBlogEntryDMO.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/dm/TestBlogEntryDMO.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -0,0 +1,62 @@
+package com.dumbhippo.dm.dm;
+
+import javax.persistence.EntityManager;
+import javax.persistence.Query;
+
+import org.slf4j.Logger;
+
+import com.dumbhippo.GlobalSetup;
+import com.dumbhippo.dm.DMObject;
+import com.dumbhippo.dm.DMSession;
+import com.dumbhippo.dm.annotations.DMO;
+import com.dumbhippo.dm.annotations.DMProperty;
+import com.dumbhippo.dm.annotations.Inject;
+import com.dumbhippo.dm.persistence.TestBlogEntry;
+import com.dumbhippo.server.NotFoundException;
+
+ DMO(classId="http://mugshot.org/p/o/test/blogEntry", resourceBase="/o/test/blogEntry")
+public abstract class TestBlogEntryDMO extends DMObject<TestBlogEntryKey> {
+ @SuppressWarnings("unused")
+ static private final Logger logger = GlobalSetup.getLogger(TestBlogEntryDMO.class);
+
+ @Inject
+ private EntityManager em;
+
+ @Inject
+ private DMSession session;
+
+ private TestBlogEntry blogEntry;
+
+ protected TestBlogEntryDMO(TestBlogEntryKey key) {
+ super(key);
+ }
+
+ @Override
+ protected void init() throws NotFoundException {
+ Query q = em.createQuery(
+ "SELECT entry from TestBlogEntry entry " +
+ " WHERE entry.user.id = :userId " +
+ " AND entry.serial = :serial "
+ );
+
+ q.setParameter("userId", getKey().getUserId().toString());
+ q.setParameter("serial", getKey().getSerial());
+
+ blogEntry = (TestBlogEntry)q.getSingleResult();
+ }
+
+ @DMProperty(defaultInclude=true)
+ public long getTimestamp() {
+ return blogEntry.getTimestamp();
+ }
+
+ @DMProperty(defaultInclude=true)
+ public String getTitle() {
+ return blogEntry.getTitle();
+ }
+
+ @DMProperty
+ public TestUserDMO getUser() {
+ return session.findUnchecked(TestUserDMO.class, blogEntry.getUser().getGuid());
+ }
+}
Added: dumbhippo/trunk/server/tests/com/dumbhippo/dm/dm/TestBlogEntryKey.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/dm/TestBlogEntryKey.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/dm/TestBlogEntryKey.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -0,0 +1,76 @@
+package com.dumbhippo.dm.dm;
+
+import com.dumbhippo.dm.BadIdException;
+import com.dumbhippo.dm.DMKey;
+import com.dumbhippo.dm.persistence.TestBlogEntry;
+import com.dumbhippo.identity20.Guid;
+import com.dumbhippo.identity20.Guid.ParseException;
+
+public class TestBlogEntryKey implements DMKey {
+ private static final long serialVersionUID = -5724889995159821821L;
+
+ private Guid userId;
+ private long serial;
+
+ public TestBlogEntryKey(Guid groupId, long serial) {
+ this.userId = groupId;
+ this.serial = serial;
+ }
+
+ public TestBlogEntryKey(String keyString) throws BadIdException {
+ String[] strings = keyString.split("\\.");
+ if (strings.length != 2)
+ throw new BadIdException("Invalid blog entry key: " + keyString);
+
+ try {
+ this.userId = new Guid(strings[0]);
+ } catch (ParseException e) {
+ throw new BadIdException("Invalid GUID in blog entry key");
+ }
+
+ try {
+ this.serial = Long.parseLong(strings[1]);
+ } catch (NumberFormatException e) {
+ throw new BadIdException("Invalid serial in blog entry key");
+ }
+ }
+
+ public TestBlogEntryKey(TestBlogEntry blogEntry) {
+ this.userId = blogEntry.getUser().getGuid();
+ this.serial = blogEntry.getSerial();
+ }
+
+
+ public Guid getUserId() {
+ return userId;
+ }
+
+ public long getSerial() {
+ return serial;
+ }
+
+ @Override
+ public TestBlogEntryKey clone() {
+ return this; // Immutable, nothing session-specific
+ }
+
+ @Override
+ public int hashCode() {
+ return (int)(userId.hashCode() * 11 + serial * 17);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof TestBlogEntryKey))
+ return false;
+
+ TestBlogEntryKey other = (TestBlogEntryKey)o;
+ return other.userId.equals(userId) && other.serial == serial;
+
+ }
+
+ @Override
+ public String toString() {
+ return userId.toString() + "." + Long.toString(serial);
+ }
+}
Modified: dumbhippo/trunk/server/tests/com/dumbhippo/dm/dm/TestUserDMO.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/dm/TestUserDMO.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/dm/TestUserDMO.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -1,16 +1,27 @@
package com.dumbhippo.dm.dm;
+import java.util.ArrayList;
import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
import java.util.Set;
import javax.persistence.EntityManager;
+import javax.persistence.Query;
+import org.slf4j.Logger;
+
+import com.dumbhippo.GlobalSetup;
+import com.dumbhippo.TypeUtils;
+import com.dumbhippo.dm.DMFeed;
+import com.dumbhippo.dm.DMFeedItem;
import com.dumbhippo.dm.DMObject;
import com.dumbhippo.dm.DMSession;
import com.dumbhippo.dm.annotations.DMFilter;
import com.dumbhippo.dm.annotations.DMO;
import com.dumbhippo.dm.annotations.DMProperty;
import com.dumbhippo.dm.annotations.Inject;
+import com.dumbhippo.dm.persistence.TestBlogEntry;
import com.dumbhippo.dm.persistence.TestGroupMember;
import com.dumbhippo.dm.persistence.TestUser;
import com.dumbhippo.identity20.Guid;
@@ -18,6 +29,9 @@
@DMO(classId="http://mugshot.org/p/o/test/user", resourceBase="/o/test/user")
public abstract class TestUserDMO extends DMObject<Guid> {
+ @SuppressWarnings("unused")
+ static private final Logger logger = GlobalSetup.getLogger(TestUserDMO.class);
+
@Inject
EntityManager em;
@@ -54,4 +68,39 @@
return result;
}
+
+ // The small defaultMaxFetch here is because we always fetch *at least* the defaultMaxFetch
+ // so a larger one makes testing max= in fetch strings hard.
+ @DMProperty(defaultMaxFetch=1)
+ public DMFeed<TestBlogEntryDMO> getBlogEntries() {
+ return new BlogEntryFeed();
+ }
+
+ private class BlogEntryFeed implements DMFeed<TestBlogEntryDMO> {
+ public Iterator<DMFeedItem<TestBlogEntryDMO>> iterator(int start, int max, long minTimestamp) {
+ logger.debug("Querying blog entries from database, start={}, max={}, minTimestamp={}",
+ new Object[] { start, max, minTimestamp });
+
+ Query q = em.createQuery(
+ "SELECT entry from TestBlogEntry entry " +
+ " WHERE entry.user = :user " +
+ " AND entry.timestamp >= :minTimestamp " +
+ " ORDER BY entry.timestamp DESC"
+ );
+
+ q.setParameter("user", user);
+ q.setParameter("minTimestamp", minTimestamp);
+
+ q.setFirstResult(start);
+ q.setMaxResults(max);
+
+ List<DMFeedItem<TestBlogEntryDMO>> items = new ArrayList<DMFeedItem<TestBlogEntryDMO>>();
+ for (TestBlogEntry entry : TypeUtils.castList(TestBlogEntry.class, q.getResultList())) {
+ TestBlogEntryDMO entryDMO = session.findUnchecked(TestBlogEntryDMO.class, new TestBlogEntryKey(entry));
+ items.add(new DMFeedItem<TestBlogEntryDMO>(entryDMO, entry.getTimestamp()));
+ }
+
+ return items.iterator();
+ }
+ }
}
Added: dumbhippo/trunk/server/tests/com/dumbhippo/dm/persistence/TestBlogEntry.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/persistence/TestBlogEntry.java 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/persistence/TestBlogEntry.java 2007-12-05 05:14:19 UTC (rev 6968)
@@ -0,0 +1,60 @@
+package com.dumbhippo.dm.persistence;
+
+import java.util.Date;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+
+ Entity
+public class TestBlogEntry extends TestDBUnique {
+ private long serial;
+ private TestUser user;
+ private long timestamp;
+ private String title;
+
+ public TestBlogEntry(TestUser user, long serial, Date timestamp) {
+ this.user = user;
+ this.serial = serial;
+ this.timestamp = timestamp.getTime();
+ }
+
+ protected TestBlogEntry() {
+ }
+
+ @Column(nullable=false)
+ public long getSerial() {
+ return this.serial;
+ }
+
+ protected void setSerial(long serial) {
+ this.serial = serial;
+ }
+
+ public long getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(long timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ @ManyToOne
+ @JoinColumn(nullable=false)
+ public TestUser getUser() {
+ return user;
+ }
+
+ protected void setUser(TestUser user) {
+ this.user = user;
+ }
+}
Added: dumbhippo/trunk/server/tests/feed-fetch-tests.xml
===================================================================
--- dumbhippo/trunk/server/tests/feed-fetch-tests.xml 2007-12-05 01:23:39 UTC (rev 6967)
+++ dumbhippo/trunk/server/tests/feed-fetch-tests.xml 2007-12-05 05:14:19 UTC (rev 6968)
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<fetchResults
+ xmlns:m="http://mugshot.org/p/system"
+ xmlns:u="http://mugshot.org/p/o/test/user"
+ xmlns:be="http://mugshot.org/p/o/test/blogEntry"
+ m:resourceBase="http://mugshot.org/o/test/">
+
+ <fetchResult id="bobsFirstFeed">
+ <be:resource m:resourceId="blogEntry/$(bob).2" m:indirect="true" m:fetch="title">
+ <be:title>Stupid Alligator Tricks</be:title>
+ </be:resource>
+ <u:resource m:resourceId="user/$(bob)" m:fetch="blogEntries">
+ <u:blogEntries m:resourceId="blogEntry/$(bob).2" m:ts="1163532712000"/>
+ </u:resource>
+ </fetchResult>
+
+ <fetchResult id="bobsOlderFeed">
+ <be:resource m:resourceId="blogEntry/$(bob).1" m:indirect="true" m:fetch="title">
+ <be:title>My Life</be:title>
+ </be:resource>
+ <u:resource m:resourceId="user/$(bob)" m:fetch="blogEntries">
+ <u:blogEntries m:resourceId="blogEntry/$(bob).1" m:ts="1151092892000"/>
+ </u:resource>
+ </fetchResult>
+
+ <fetchResult id="bobsAlreadyFetchedFeed">
+ <u:resource m:resourceId="user/$(bob)" m:fetch="blogEntries">
+ </u:resource>
+ </fetchResult>
+
+ <fetchResult id="bobsFeedNotification">
+ <be:resource m:resourceId="blogEntry/$(bob).3" m:indirect="true" m:fetch="title">
+ <be:title>My 9-fingered Life</be:title>
+ </be:resource>
+ <u:resource m:resourceId="user/$(bob)" m:fetch="blogEntries">
+ <u:blogEntries m:resourceId="blogEntry/$(bob).3" m:ts="1174657444000"/>
+ </u:resource>
+ </fetchResult>
+</fetchResults>
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]