r7166 - in dumbhippo/trunk/server: src/com/dumbhippo/dm src/com/dumbhippo/dm/fetch tests tests/com/dumbhippo/dm



Author: otaylor
Date: 2008-01-09 16:44:07 -0600 (Wed, 09 Jan 2008)
New Revision: 7166

Modified:
   dumbhippo/trunk/server/src/com/dumbhippo/dm/ClientNotification.java
   dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/BoundFetch.java
   dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/FetchNode.java
   dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/PropertyFetchNode.java
   dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchParserTests.java
   dumbhippo/trunk/server/tests/feed-fetch-tests.xml
   dumbhippo/trunk/server/tests/fetch-tests.xml
Log:
FetchNode PropertyFetchNode FetchParserTests: Add FetchNode.merge()

BoundFetch FetchNode; Store the original FetchNode that was bound

BoundFetch fetch-tests.xml feed-fetch-tests.xml: Change what we
  send for m:fetch to reflect more accurately what the client asked
  for ... in particular, to include child fetches. This is needed
  to avoid round trips on spontaneous notifications, since otherwise
  the client doesn't know that child fetches are satisfied.


Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/ClientNotification.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/ClientNotification.java	2008-01-09 22:43:41 UTC (rev 7165)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/ClientNotification.java	2008-01-09 22:44:07 UTC (rev 7166)
@@ -113,7 +113,7 @@
 				}
 			}
 			
-			visitor.beginResource(key.getClassHolder(), key.getKey(), fetch.getFetchString(classHolder), false);
+			visitor.beginResource(key.getClassHolder(), key.getKey(), fetch.getFetchString(), false);
 			
 			v = propertyMask;
 			propertyIndex = 0;

Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/BoundFetch.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/BoundFetch.java	2008-01-09 22:43:41 UTC (rev 7165)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/BoundFetch.java	2008-01-09 22:44:07 UTC (rev 7166)
@@ -22,10 +22,12 @@
 	
 	private PropertyFetch[] properties;
 	private boolean includeDefault;
+	private FetchNode unboundFetch;
 	
-	public BoundFetch(PropertyFetch[] properties, boolean includeDefault) {
+	public BoundFetch(PropertyFetch[] properties, boolean includeDefault, FetchNode unboundFetch) {
 		this.properties = properties;
 		this.includeDefault = includeDefault;
+		this.unboundFetch = unboundFetch;
 	}
 	
 	public PropertyFetch[] getProperties() {
@@ -73,8 +75,8 @@
 		else
 			oldFetch = null;
 
-		boolean allFetched = true;
-		boolean noneFetched = true;
+		// boolean allFetched = true;
+		// boolean noneFetched = true;
 		boolean newAnyFetched = false;
 		
 		int newIndex = 0, oldIndex = 0;
@@ -108,10 +110,10 @@
 				newChildren = classProperties[classIndex].getDefaultChildren();
 			}
 			
-			if (oldFetched || newFetched)
-				noneFetched = false;
-			else
-				allFetched = false;
+			// if (oldFetched || newFetched)
+			//	noneFetched = false;
+			// else
+			//	 allFetched = false;
 			
 			if (newFetched && !oldFetched)
 				newAnyFetched = true;
@@ -176,41 +178,28 @@
 		// Otherwise, if there are no new properties to fetch, we are done 
 		if (indirect && oldFetch != null && !newAnyFetched)
 			return;
-		
-		String fetchString = null;
-		if (needFetch == true) {
-			if (noneFetched)
-				fetchString = "";
-			else if (allFetched)
-				fetchString = "*";
-			else {
-				StringBuilder sb = new StringBuilder();
-				
-				if ((oldFetch != null && oldFetch.getIncludeDefault()) || getIncludeDefault())
-					sb.append("+");
-				
-				newIndex = 0; oldIndex = 0;
-				newOrdering = propertyOrdering(0);
-				oldOrdering = oldFetch != null ? oldFetch.propertyOrdering(0) : Long.MAX_VALUE;
-				for (int classIndex = 0; classIndex < classProperties.length; classIndex++) {
-					long classOrdering = classOrdering(classProperties, classIndex);
 
-					while (oldOrdering < classOrdering)
-						oldOrdering = oldFetch.propertyOrdering(++oldIndex);
-					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 (oldFetched || newFetched)
-						appendToFetchString(sb, classProperties[classIndex], classHolder.mustQualifyProperty(classIndex));
-				}
-				
-				fetchString = sb.toString();
-			}
-		}
-		
+		// The protocol requirement here is that the fetch string that we send
+		// includes all newly fetched and monitored properties; it doesn't need
+		// to include previously fetched properties. We keep things simple for
+		// now by sending exactly the fetch supplied by the client. (For a notification, 
+		// that will be merge of previous fetches; for a request it's the fetch in that 
+		// request.)
+		//
+		// We could:
+		//
+		//  * Expand '+' to '+;<property1> <property1 children>;<property2> ....'
+		//    saves round trips at the expense of protocol efficiency, since the
+		//    client can know to short-circuit a later request for property1.
+		//
+		//  * Add a '*' if all properties are fetched, to let the client know
+		//    that fetches for additional properties will not succeed.
+		//
+		//  * Omit properties from the fetch string that were previously sent.
+		//    even if they are explicitly requested again. (We already do this
+		//    for property *values*, just not in the fetch string.)
+		// 
+		String fetchString = needFetch ? unboundFetch.toString() : null;
 		visitor.beginResource(classHolder, object.getKey(), fetchString, indirect);
 
 		newIndex = 0; oldIndex = 0;
@@ -271,15 +260,6 @@
 		visit(session, object, visitor, false);
 	}
 	
-	private void appendToFetchString(StringBuilder sb, DMPropertyHolder<?,?,?> propertyHolder, boolean qualify) {
-		if (sb.length() > 0)
-			sb.append(";");
-		if (qualify)
-			sb.append(propertyHolder.getPropertyId());
-		else
-			sb.append(propertyHolder.getName());
-	}
-
 	@Override
 	public boolean equals(Object o) {
 		if (!(o instanceof BoundFetch))
@@ -362,9 +342,16 @@
 			}
 		}
 		
+		FetchNode unboundFetch = this.unboundFetch.merge(other.unboundFetch);
+		
 		// If the other property is a subset of this one, we can just return this one
-		if (!changedProperties && newCount == this.properties.length && (this.includeDefault || !other.includeDefault))
-			return this;
+		// and vice-versa
+		if (!changedProperties) {
+			if (newCount == this.properties.length && (this.includeDefault || !other.includeDefault) && unboundFetch == this.unboundFetch)
+				return this;
+			else if (newCount == other.properties.length && (other.includeDefault || !this.includeDefault) && unboundFetch == other.unboundFetch)
+				return other;
+		}
 		
 		PropertyFetch[] newProperties = new PropertyFetch[newCount];
 		
@@ -390,7 +377,7 @@
 		}
 		
 		@SuppressWarnings("unchecked")
-		BoundFetch<?, ?> newFetch = new BoundFetch(newProperties, this.includeDefault || other.includeDefault);
+		BoundFetch<?, ?> newFetch = new BoundFetch(newProperties, this.includeDefault || other.includeDefault, unboundFetch);
 		
 		return newFetch;
 	}
@@ -456,49 +443,7 @@
 			result.addNotification(client, key, this, notifiedMask, childFetches, maxes);
 	}
 	
-	public <U extends T> String getFetchString(DMClassHolder<K,U> classHolder) {
-		DMPropertyHolder<K,U,?>[] classProperties = classHolder.getProperties();
-		
-		boolean allFetched = true;
-		boolean noneFetched = true;
-		
-		int propertyIndex = 0;
-		long propertyOrdering = propertyOrdering(0);
-		for (int classIndex = 0; classIndex < classProperties.length; classIndex++) {
-			long classOrdering = classOrdering(classProperties, classIndex);
-
-			while (propertyOrdering < classOrdering)
-				propertyOrdering = propertyOrdering(++propertyIndex);
-
-			boolean fetch = propertyOrdering == classOrdering || (includeDefault && classProperties[classIndex].getDefaultInclude());
-			
-			if (fetch)
-				noneFetched = false;
-			else
-				allFetched = false;
-		}
-		
-		if (noneFetched)
-			return "";
-		else if (allFetched)
-			return "*";
-
-		StringBuilder sb = new StringBuilder();
-			
-		propertyIndex = 0;
-		propertyOrdering = propertyOrdering(0);
-		for (int classIndex = 0; classIndex < classProperties.length; classIndex++) {
-			long classOrdering = classOrdering(classProperties, classIndex);
-
-			while (propertyOrdering < classOrdering)
-				propertyOrdering = propertyOrdering(++propertyIndex);
-			
-			boolean fetch = propertyOrdering == classOrdering || (includeDefault && classProperties[classIndex].getDefaultInclude());
-			
-			if (fetch)
-				appendToFetchString(sb, classProperties[classIndex], classHolder.mustQualifyProperty(classIndex));
-		}
-			
-		return sb.toString();
+	public <U extends T> String getFetchString() {
+		return unboundFetch.toString();
 	}
 }

Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/FetchNode.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/FetchNode.java	2008-01-09 22:43:41 UTC (rev 7165)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/FetchNode.java	2008-01-09 22:44:07 UTC (rev 7166)
@@ -1,6 +1,7 @@
 package com.dumbhippo.dm.fetch;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
@@ -17,6 +18,7 @@
 	
 	public FetchNode(PropertyFetchNode[] properties) {
 		this.properties = properties;
+		Arrays.sort(properties);
 	}
 	
 	public PropertyFetchNode[] getProperties() {
@@ -45,7 +47,7 @@
 
 		Collections.sort(boundProperties);
 		
-		return new BoundFetch<K,T>(boundProperties.toArray(new PropertyFetch[boundProperties.size()]), includeDefault);
+		return new BoundFetch<K,T>(boundProperties.toArray(new PropertyFetch[boundProperties.size()]), includeDefault, this);
 	}
 	
 	@Override
@@ -60,4 +62,101 @@
 		
 		return b.toString();
 	}
+
+	public FetchNode merge(FetchNode other) {
+		int count = 0;
+		int i = 0;
+		int j = 0;
+		boolean changedProperties = false;
+		
+		while (true) {
+			int cmp;
+			
+			if (i < properties.length && j < other.properties.length) {
+				cmp = properties[i].compareTo(other.properties[j]);
+			} else if (i < properties.length)
+				cmp = -1;
+			else if (j < other.properties.length)
+				cmp = 1;
+			else
+				break;
+			
+			if (cmp == 0) {
+				if (!properties[i].equals(other.properties[j]))
+					changedProperties = true; 
+
+				i++;
+				j++;
+			} else if (cmp < 0) {
+				i++;
+			} else {
+				j++;
+			}
+				
+			count++;
+		}
+		
+		// If the other property is a subset of this one, we can just return this one
+		// and vice-versa
+		if (!changedProperties) {
+			if (count == properties.length)
+				return this;
+			else if (count == other.properties.length)
+				return other;
+		}
+		
+		PropertyFetchNode[] newProperties = new PropertyFetchNode[count];
+
+		count = 0; i = 0; j = 0;
+		
+		while (true) {
+			int cmp;
+			
+			if (i < properties.length && j < other.properties.length)
+				cmp = properties[i].compareTo(other.properties[j]);
+			else if (i < properties.length)
+				cmp = -1;
+			else if (j < other.properties.length)
+				cmp = 1;
+			else
+				break;
+			
+			logger.debug("{} {} {}", new Object[] { 
+					i < properties.length ? properties[i].getProperty() : null, 
+					j < other.properties.length ? other.properties[j].getProperty() : null, 
+					cmp });
+			
+			if (cmp == 0) {
+				newProperties[count] = properties[i].merge(other.properties[j]);
+				i++;
+				j++;
+			} else if (cmp < 0) {
+				newProperties[count] = properties[i];
+				i++;
+			} else {
+				newProperties[count] = other.properties[j];
+				j++;
+			}
+				
+			count++;
+		}
+		
+		return new FetchNode(newProperties);
+	}
+	
+	@Override
+	public boolean equals(Object o) {
+		if (!(o instanceof FetchNode))
+			return false;
+		
+		FetchNode other = (FetchNode)o;
+		if (other.properties.length != properties.length)
+			return false;
+		
+		for (int i = 0; i < properties.length; i++)
+			if (!other.properties[i].equals(properties[i]))
+				return false;
+		
+		return true;
+	}
 }

Modified: dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/PropertyFetchNode.java
===================================================================
--- dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/PropertyFetchNode.java	2008-01-09 22:43:41 UTC (rev 7165)
+++ dumbhippo/trunk/server/src/com/dumbhippo/dm/fetch/PropertyFetchNode.java	2008-01-09 22:44:07 UTC (rev 7166)
@@ -11,7 +11,8 @@
 import com.dumbhippo.dm.schema.ResourcePropertyHolder;
 
 
-public class PropertyFetchNode {
+public class PropertyFetchNode implements Comparable<PropertyFetchNode> {
+	@SuppressWarnings("unused")
 	static private final Logger logger = GlobalSetup.getLogger(PropertyFetchNode.class); 
 	
 	private String property;
@@ -81,6 +82,28 @@
 		}
 	}
 
+	// FIXME: We should probably validate the types during or immediately after parsing
+
+	private boolean getNotify() {
+		for (FetchAttributeNode attr : attributes) {
+			if (attr.getType() == FetchAttributeType.NOTIFY &&
+				attr.getValue() instanceof Boolean)
+				return (Boolean)attr.getValue();
+		}
+		
+		return true;
+	}
+	
+	private int getMax() {
+		for (FetchAttributeNode attr : attributes) {
+			if (attr.getType() == FetchAttributeType.MAX &&
+				attr.getValue() instanceof Integer)
+				return (Integer)attr.getValue();
+		}
+		
+		return -1;
+	}
+
 	/**
 	 * Finds all properties in the given class and in *subclasses* of the given class
 	 * that match this node, bind them and return the result.
@@ -91,28 +114,8 @@
 	 * @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)) {
-					logger.warn("Ignoring non-boolean notify attribute");
-					continue;
-				}
-				notify = ((Boolean)(attribute.getValue())).booleanValue();
-				break;
-			}
-		}
+		int max = getMax();
+		boolean notify = getNotify();
 		
 		bindToClass(classHolder, skipDefault, resultList, max, notify);
 		for (DMClassHolder<?,?> subclassHolder : classHolder.getDerivedClasses())
@@ -148,4 +151,85 @@
 		
 		return b.toString();
 	}
+
+	/* This sort is used in FetchNode to sort properties to make merging easy */
+	public int compareTo(PropertyFetchNode other) {
+		return property.compareTo(other.property);
+	}
+
+	public PropertyFetchNode merge(PropertyFetchNode other) {
+		assert(property.equals(other.property));
+		
+		FetchNode newChildren;
+		
+		if (other.children == null)
+			newChildren = children;
+		else if (children == null)
+			newChildren = other.children;
+		else
+			newChildren = children.merge(other.children);
+
+		FetchAttributeNode newAttr[];
+
+		if (other.attributes.length == 0)
+			newAttr = attributes;
+		else if (attributes.length == 0)
+			newAttr = other.attributes;
+		else {
+			int newMax = Math.max(getMax(), other.getMax());
+			boolean newNotify = getNotify() || other.getNotify();
+			
+			if (newMax >= 0 && !newNotify)
+				newAttr = new FetchAttributeNode[] {
+						new FetchAttributeNode(FetchAttributeType.MAX, newMax),
+						new FetchAttributeNode(FetchAttributeType.NOTIFY, newNotify),
+			    };
+			else if (newMax >= 0)
+				newAttr = new FetchAttributeNode[] {
+						new FetchAttributeNode(FetchAttributeType.MAX, newMax)
+			    };
+			else if (!newNotify)
+				newAttr = new FetchAttributeNode[] {
+						new FetchAttributeNode(FetchAttributeType.NOTIFY, newNotify),
+			    };
+			else
+				newAttr = new FetchAttributeNode[0];
+		}
+		
+		// Common special case is "name".merge("name"), avoid allocation for that
+		if (newAttr == attributes && newChildren == children)
+			return this;
+		else if (newAttr == other.attributes && newChildren == other.children)
+			return other;
+		else
+			return new PropertyFetchNode(property, newAttr, newChildren);
+	}
+	
+	@Override
+	public boolean equals(Object o) {
+		if (!(o instanceof PropertyFetchNode))
+			return false;
+		
+		PropertyFetchNode other = (PropertyFetchNode)o;
+		
+		if (!other.property.equals(property))
+			return false;
+		
+		// We count "property []" as different than "property" for convenience 
+		if (children != null) {
+			if (!children.equals(other.children))
+				return false;
+		} else {
+			if (other.children != null)
+				return false;
+		}
+		
+		if (getMax() != other.getMax())
+			return false;
+		
+		if (getNotify() != other.getNotify())
+			return false;
+		
+		return true;
+	}
 }

Modified: dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchParserTests.java
===================================================================
--- dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchParserTests.java	2008-01-09 22:43:41 UTC (rev 7165)
+++ dumbhippo/trunk/server/tests/com/dumbhippo/dm/FetchParserTests.java	2008-01-09 22:44:07 UTC (rev 7166)
@@ -1,5 +1,6 @@
 package com.dumbhippo.dm;
 
+import com.dumbhippo.dm.fetch.FetchNode;
 import com.dumbhippo.dm.parser.FetchParser;
 import com.dumbhippo.dm.parser.ParseException;
 
@@ -15,9 +16,10 @@
 		expectIdentity("+");
 		expectIdentity("name");
 		expectIdentity("contact +");
-		expectIdentity("name;contact [name;photoUrl]");
+		expectIdentity("contact [name;photoUrl];name");
 		expectIdentity("name(notify=false) [name;photoUrl(notify=false)]");
-		expectIdentity("blogEntries(max=10) [title;description]");
+		expectIdentity("blogEntries(max=10) [description;title]");
+		expectIdentity("blogEntries(max=10,notify=false) [description;title]");
 		expectIdentity("a;b [a;b [a;b]]");
 		
 		expectSuccess("member()[]", "member");
@@ -30,4 +32,21 @@
 		expectFailure("+ name");
 		expectFailure("+ [name]");
 	}
+	
+	private void doMergeTest(String a, String b, String expected) throws ParseException {
+		FetchNode fetchA = FetchParser.parse(a);
+		FetchNode fetchB = FetchParser.parse(b);
+		
+		assertEquals(expected, fetchA.merge(fetchB).toString()); 
+		assertEquals(expected, fetchB.merge(fetchA).toString()); 
+	}
+	
+	public void testFetchMerge() throws Exception {
+		doMergeTest("", "", "");
+		doMergeTest("", "name", "name");
+		doMergeTest("contact", "name", "contact;name");
+		doMergeTest("blogEntries[description];contact", "blogEntries[title];name", "blogEntries [description;title];contact;name");
+		doMergeTest("blogEntries(max=10)", "blogEntries(max=15,notify=false)", "blogEntries(max=15)");
+		doMergeTest("blogEntries(max=10,notify=false)", "blogEntries(max=15,notify=false)", "blogEntries(max=15,notify=false)");
+	}
 }

Modified: dumbhippo/trunk/server/tests/feed-fetch-tests.xml
===================================================================
--- dumbhippo/trunk/server/tests/feed-fetch-tests.xml	2008-01-09 22:43:41 UTC (rev 7165)
+++ dumbhippo/trunk/server/tests/feed-fetch-tests.xml	2008-01-09 22:44:07 UTC (rev 7166)
@@ -9,7 +9,7 @@
 		<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:resource m:resourceId="user/$(bob)" m:fetch="blogEntries title">
 			<u:blogEntries m:resourceId="blogEntry/$(bob).2" m:ts="1163532712000"/>
 		</u:resource>
 	</fetchResult>
@@ -18,13 +18,13 @@
 		<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:resource m:resourceId="user/$(bob)" m:fetch="blogEntries(max=10) title">
 			<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 m:resourceId="user/$(bob)" m:fetch="blogEntries(max=10) title">
 		</u:resource>
 	</fetchResult>
 
@@ -32,7 +32,7 @@
 		<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:resource m:resourceId="user/$(bob)" m:fetch="blogEntries(max=10) title">
 			<u:blogEntries m:resourceId="blogEntry/$(bob).3" m:ts="1174657444000"/>
 		</u:resource>
 	</fetchResult>

Modified: dumbhippo/trunk/server/tests/fetch-tests.xml
===================================================================
--- dumbhippo/trunk/server/tests/fetch-tests.xml	2008-01-09 22:43:41 UTC (rev 7165)
+++ dumbhippo/trunk/server/tests/fetch-tests.xml	2008-01-09 22:44:07 UTC (rev 7166)
@@ -10,16 +10,16 @@
 		<u:resource m:resourceId="user/$(bob)" m:indirect="true" m:fetch="name">
 			<u:name>Bob</u:name>
 		</u:resource>
-		<gm:resource m:resourceId="groupMember/$(group).$(bob)" m:indirect="true" m:fetch="member">
+		<gm:resource m:resourceId="groupMember/$(group).$(bob)" m:indirect="true" m:fetch="member name">
 			<gm:member m:resourceId="user/$(bob)"/>
 		</gm:resource>
 		<u:resource m:resourceId="user/$(jane)" m:indirect="true" m:fetch="name">
 			<u:name>Jane</u:name>
 		</u:resource>
-		<gm:resource m:resourceId="groupMember/$(group).$(jane)" m:indirect="true" m:fetch="member">
+		<gm:resource m:resourceId="groupMember/$(group).$(jane)" m:indirect="true" m:fetch="member name">
 			<gm:member m:resourceId="user/$(jane)"/>
 		</gm:resource>
-		<g:resource m:resourceId="group/$(group)" m:fetch="name;members">
+		<g:resource m:resourceId="group/$(group)" m:fetch="name;members member name">
 			<g:name>BobAndJane</g:name>
 			<g:members m:resourceId="groupMember/$(group).$(bob)"/>
 			<g:members m:resourceId="groupMember/$(group).$(jane)"/>
@@ -27,19 +27,19 @@
 	</fetchResult>
 	
 	<fetchResult id="bobAndJaneDefault">
-		<u:resource m:resourceId="user/$(bob)" m:indirect="true" m:fetch="+;name">
+		<u:resource m:resourceId="user/$(bob)" m:indirect="true" m:fetch="+">
 			<u:name>Bob</u:name>
 		</u:resource>
-		<gm:resource m:resourceId="groupMember/$(group).$(bob)" m:indirect="true" m:fetch="+;member">
+		<gm:resource m:resourceId="groupMember/$(group).$(bob)" m:indirect="true" m:fetch="+">
 			<gm:member m:resourceId="user/$(bob)"/>
 		</gm:resource>
-		<u:resource m:resourceId="user/$(jane)" m:indirect="true" m:fetch="+;name">
+		<u:resource m:resourceId="user/$(jane)" m:indirect="true" m:fetch="+">
 			<u:name>Jane</u:name>
 		</u:resource>
-		<gm:resource m:resourceId="groupMember/$(group).$(jane)" m:indirect="true" m:fetch="+;member">
+		<gm:resource m:resourceId="groupMember/$(group).$(jane)" m:indirect="true" m:fetch="+">
 			<gm:member m:resourceId="user/$(jane)"/>
 		</gm:resource>
-		<g:resource m:resourceId="group/$(group)" m:fetch="+;name;members">
+		<g:resource m:resourceId="group/$(group)" m:fetch="+;members +">
 			<g:name>BobAndJane</g:name>
 			<g:members m:resourceId="groupMember/$(group).$(bob)"/>
 			<g:members m:resourceId="groupMember/$(group).$(jane)"/>
@@ -53,43 +53,43 @@
 	</fetchResult>
 	
 	<fetchResult id="bobAndJaneRemaining">
-		<u:resource m:resourceId="user/$(bob)" m:indirect="true" m:fetch="+;name">
+		<u:resource m:resourceId="user/$(bob)" m:indirect="true" m:fetch="+">
 			<u:name>Bob</u:name>
 		</u:resource>
-		<gm:resource m:resourceId="groupMember/$(group).$(bob)" m:indirect="true" m:fetch="+;member">
+		<gm:resource m:resourceId="groupMember/$(group).$(bob)" m:indirect="true" m:fetch="+">
 			<gm:member m:resourceId="user/$(bob)"/>
 		</gm:resource>
-		<u:resource m:resourceId="user/$(jane)" m:indirect="true" m:fetch="+;name">
+		<u:resource m:resourceId="user/$(jane)" m:indirect="true" m:fetch="+">
 			<u:name>Jane</u:name>
 		</u:resource>
-		<gm:resource m:resourceId="groupMember/$(group).$(jane)" m:indirect="true" m:fetch="+;member">
+		<gm:resource m:resourceId="groupMember/$(group).$(jane)" m:indirect="true" m:fetch="+">
 			<gm:member m:resourceId="user/$(jane)"/>
 		</gm:resource>
-		<g:resource m:resourceId="group/$(group)" m:fetch="+;name;members">
+		<g:resource m:resourceId="group/$(group)" m:fetch="+;members +">
 			<g:members m:resourceId="groupMember/$(group).$(bob)"/>
 			<g:members m:resourceId="groupMember/$(group).$(jane)"/>
 		</g:resource>
 	</fetchResult>
 	
 	<fetchResult id="bobAndJaneAddOn">
-		<gm:resource m:resourceId="groupMember/$(group).$(bob)" m:indirect="true" m:fetch="+;group;member">
+		<gm:resource m:resourceId="groupMember/$(group).$(bob)" m:indirect="true" m:fetch="group">
 			<gm:group m:resourceId="group/$(group)"/>
 		</gm:resource>
-		<gm:resource m:resourceId="groupMember/$(group).$(jane)" m:indirect="true" m:fetch="+;group;member">
+		<gm:resource m:resourceId="groupMember/$(group).$(jane)" m:indirect="true" m:fetch="group">
 			<gm:group m:resourceId="group/$(group)"/>
 		</gm:resource>
-		<g:resource m:resourceId="group/$(group)" m:fetch="+;name;members">
+		<g:resource m:resourceId="group/$(group)" m:fetch="members group">
 		</g:resource>
 	</fetchResult>
 
 	<fetchResult id="bobAndJaneLoop">
-		<gm:resource m:resourceId="groupMember/$(group).$(bob)" m:indirect="true" m:fetch="group">
+		<gm:resource m:resourceId="groupMember/$(group).$(bob)" m:indirect="true" m:fetch="group +">
 			<gm:group m:resourceId="group/$(group)"/>
 		</gm:resource>
-		<gm:resource m:resourceId="groupMember/$(group).$(jane)" m:indirect="true" m:fetch="group">
+		<gm:resource m:resourceId="groupMember/$(group).$(jane)" m:indirect="true" m:fetch="group +">
 			<gm:group m:resourceId="group/$(group)"/>
 		</gm:resource>
-		<g:resource m:resourceId="group/$(group)" m:fetch="+;name;members">
+		<g:resource m:resourceId="group/$(group)" m:fetch="+;members group +">
 			<g:name>BobAndJane</g:name>
 			<g:members m:resourceId="groupMember/$(group).$(bob)"/>
 			<g:members m:resourceId="groupMember/$(group).$(jane)"/>
@@ -100,10 +100,10 @@
 		<u:resource m:resourceId="user/$(victor)" m:indirect="true" m:fetch="name">
 			<u:name>Victor</u:name>
 		</u:resource>
-		<gm:resource m:resourceId="groupMember/$(group).$(victor)" m:indirect="true" m:fetch="member">
+		<gm:resource m:resourceId="groupMember/$(group).$(victor)" m:indirect="true" m:fetch="member name">
 			<gm:member m:resourceId="user/$(victor)"/>
 		</gm:resource>
-		<g:resource m:resourceId="group/$(group)" m:fetch="name;members">
+		<g:resource m:resourceId="group/$(group)" m:fetch="name;members member name">
 			<g:name>BobAndJaneAndVictor</g:name>
 			<g:members m:resourceId="groupMember/$(group).$(bob)"/>
 			<g:members m:resourceId="groupMember/$(group).$(jane)"/>



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]