You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2014/11/14 18:47:28 UTC

[27/50] [abbrv] cayenne git commit: CAY-1959 Chainable API for SelectQuery

CAY-1959 Chainable API for SelectQuery

* merge method for PrefetchTreeNode for more comprehensive prefetch subtree merging
* using the new merge in SelectQuery as well

UPGRADE NOTES:

The most visible side effect of this change is that Property.xyzPrefetch() method will now return
a root of the prefetch tree that can merged into the query instead of a leaf that can't.

So in an unlikely case someone did MyEntity.MY_PROPERTY.disjoint().setSemantics(JOINT), the last part in this chain
is not longer going to work.


Project: http://git-wip-us.apache.org/repos/asf/cayenne/repo
Commit: http://git-wip-us.apache.org/repos/asf/cayenne/commit/4d8b2e1a
Tree: http://git-wip-us.apache.org/repos/asf/cayenne/tree/4d8b2e1a
Diff: http://git-wip-us.apache.org/repos/asf/cayenne/diff/4d8b2e1a

Branch: refs/heads/CAY-1946
Commit: 4d8b2e1a6a6db08bd68ffab227381eb981111893
Parents: fb8660e
Author: aadamchik <aa...@apache.org>
Authored: Sun Nov 9 21:20:21 2014 +0300
Committer: aadamchik <aa...@apache.org>
Committed: Sun Nov 9 21:24:50 2014 +0300

----------------------------------------------------------------------
 .../cayenne/query/PrefetchTreeNodeTest.java     |  169 ++-
 .../java/org/apache/cayenne/exp/Property.java   |  812 ++++++-------
 .../apache/cayenne/query/BaseQueryMetadata.java |  839 ++++++-------
 .../apache/cayenne/query/PrefetchTreeNode.java  | 1106 +++++++++---------
 .../org/apache/cayenne/query/SelectQuery.java   |    7 +-
 ...ataContextDisjointByIdPrefetch_ExtrasIT.java |    1 +
 6 files changed, 1546 insertions(+), 1388 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cayenne/blob/4d8b2e1a/cayenne-client/src/test/java/org/apache/cayenne/query/PrefetchTreeNodeTest.java
----------------------------------------------------------------------
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/query/PrefetchTreeNodeTest.java b/cayenne-client/src/test/java/org/apache/cayenne/query/PrefetchTreeNodeTest.java
index 90d4169..1dee8a9 100644
--- a/cayenne-client/src/test/java/org/apache/cayenne/query/PrefetchTreeNodeTest.java
+++ b/cayenne-client/src/test/java/org/apache/cayenne/query/PrefetchTreeNodeTest.java
@@ -18,51 +18,142 @@
  ****************************************************************/
 package org.apache.cayenne.query;
 
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.remote.hessian.service.HessianUtil;
-import org.junit.Test;
-
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNotSame;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.remote.hessian.service.HessianUtil;
+import org.junit.Test;
 
 public class PrefetchTreeNodeTest {
 
-    @Test
-    public void testTreeSerializationWithHessian() throws Exception {
-        PrefetchTreeNode n1 = new PrefetchTreeNode();
-        PrefetchTreeNode n2 = n1.addPath("abc");
-
-        PrefetchTreeNode nc1 = (PrefetchTreeNode) HessianUtil.cloneViaClientServerSerialization(n1,
-                new EntityResolver());
-        assertNotNull(nc1);
-
-        PrefetchTreeNode nc2 = nc1.getNode("abc");
-        assertNotNull(nc2);
-        assertNotSame(nc2, n2);
-        assertSame(nc1, nc2.getParent());
-        assertEquals("abc", nc2.getName());
-    }
-
-    @Test
-    public void testSubtreeSerializationWithHessian() throws Exception {
-        PrefetchTreeNode n1 = new PrefetchTreeNode();
-        PrefetchTreeNode n2 = n1.addPath("abc");
-        PrefetchTreeNode n3 = n2.addPath("xyz");
-
-        // test that substree was serialized as independent tree, instead of
-        // sucking
-        PrefetchTreeNode nc2 = (PrefetchTreeNode) HessianUtil.cloneViaClientServerSerialization(n2,
-                new EntityResolver());
-        assertNotNull(nc2);
-        assertNull(nc2.getParent());
-
-        PrefetchTreeNode nc3 = nc2.getNode("xyz");
-        assertNotNull(nc3);
-        assertNotSame(nc3, n3);
-        assertSame(nc2, nc3.getParent());
-        assertEquals("xyz", nc3.getName());
-    }
+	@Test
+	public void testTreeSerializationWithHessian() throws Exception {
+		PrefetchTreeNode n1 = new PrefetchTreeNode();
+		PrefetchTreeNode n2 = n1.addPath("abc");
+
+		PrefetchTreeNode nc1 = (PrefetchTreeNode) HessianUtil.cloneViaClientServerSerialization(n1,
+				new EntityResolver());
+		assertNotNull(nc1);
+
+		PrefetchTreeNode nc2 = nc1.getNode("abc");
+		assertNotNull(nc2);
+		assertNotSame(nc2, n2);
+		assertSame(nc1, nc2.getParent());
+		assertEquals("abc", nc2.getName());
+	}
+
+	@Test
+	public void testSubtreeSerializationWithHessian() throws Exception {
+		PrefetchTreeNode n1 = new PrefetchTreeNode();
+		PrefetchTreeNode n2 = n1.addPath("abc");
+		PrefetchTreeNode n3 = n2.addPath("xyz");
+
+		// test that substree was serialized as independent tree, instead of
+		// sucking
+		PrefetchTreeNode nc2 = (PrefetchTreeNode) HessianUtil.cloneViaClientServerSerialization(n2,
+				new EntityResolver());
+		assertNotNull(nc2);
+		assertNull(nc2.getParent());
+
+		PrefetchTreeNode nc3 = nc2.getNode("xyz");
+		assertNotNull(nc3);
+		assertNotSame(nc3, n3);
+		assertSame(nc2, nc3.getParent());
+		assertEquals("xyz", nc3.getName());
+	}
+
+	@Test
+	public void testMerge() {
+		PrefetchTreeNode original = new PrefetchTreeNode();
+		original.addPath("a").setPhantom(true);
+		original.addPath("a.b").setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+		original.addPath("a.b").setPhantom(false);
+		original.addPath("c").setSemantics(PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
+		original.addPath("c").setPhantom(false);
+		original.addPath("f").setSemantics(PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
+		original.addPath("f").setPhantom(false);
+
+		PrefetchTreeNode toMerge = new PrefetchTreeNode();
+		toMerge.addPath("a").setPhantom(false);
+		toMerge.addPath("a.b").setSemantics(PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS);
+		toMerge.addPath("d.e").setSemantics(PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
+		toMerge.addPath("d.e").setPhantom(false);
+		toMerge.addPath("c").setSemantics(PrefetchTreeNode.UNDEFINED_SEMANTICS);
+
+		original.merge(toMerge);
+
+		assertSame(original, original.getRoot());
+		assertEquals(4, original.getChildren().size());
+
+		PrefetchTreeNode mergedA = original.getChild("a");
+		assertEquals(1, mergedA.getChildren().size());
+		assertFalse("Phantom flag wasn't turned off", mergedA.isPhantom());
+		assertEquals(PrefetchTreeNode.UNDEFINED_SEMANTICS, mergedA.getSemantics());
+
+		PrefetchTreeNode mergedB = mergedA.getChild("b");
+		assertEquals(0, mergedB.getChildren().size());
+		assertFalse(mergedB.isPhantom());
+		assertEquals("Semantics was't merged", PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS,
+				mergedB.getSemantics());
+
+		PrefetchTreeNode mergedC = original.getChild("c");
+		assertEquals(0, mergedC.getChildren().size());
+		assertFalse(mergedC.isPhantom());
+		assertEquals("Semantics was overridden to undefined", PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS,
+				mergedC.getSemantics());
+
+		PrefetchTreeNode mergedD = original.getChild("d");
+		assertEquals(1, mergedD.getChildren().size());
+		assertTrue(mergedD.isPhantom());
+		assertEquals(PrefetchTreeNode.UNDEFINED_SEMANTICS, mergedD.getSemantics());
+		assertNotSame("Merged node wasn't cloned", toMerge.getChild("d"), mergedD);
+
+		PrefetchTreeNode mergedE = mergedD.getChild("e");
+		assertEquals(0, mergedE.getChildren().size());
+		assertFalse(mergedE.isPhantom());
+		assertEquals(PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS, mergedE.getSemantics());
+
+		PrefetchTreeNode mergedF = original.getChild("f");
+		assertEquals(0, mergedF.getChildren().size());
+		assertFalse(mergedF.isPhantom());
+		assertEquals(PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS, mergedF.getSemantics());
+	}
+
+	@Test
+	public void testMerge_NonRoot() {
+		PrefetchTreeNode original = new PrefetchTreeNode();
+		original.addPath("a").setPhantom(true);
+		original.addPath("a.b").setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+		original.addPath("a.b").setPhantom(false);
+
+		PrefetchTreeNode toMerge = new PrefetchTreeNode(null, "a.b.c");
+		toMerge.setPhantom(false);
+		toMerge.setSemantics(PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
+
+		original.merge(toMerge);
+
+		assertSame(original, original.getRoot());
+		assertEquals(1, original.getChildren().size());
+
+		PrefetchTreeNode mergedA = original.getChild("a");
+		assertEquals(1, mergedA.getChildren().size());
+		assertTrue(mergedA.isPhantom());
+		assertEquals(PrefetchTreeNode.UNDEFINED_SEMANTICS, mergedA.getSemantics());
+
+		PrefetchTreeNode mergedB = mergedA.getChild("b");
+		assertEquals(1, mergedB.getChildren().size());
+		assertFalse(mergedB.isPhantom());
+		assertEquals(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS, mergedB.getSemantics());
+
+		PrefetchTreeNode mergedC = mergedB.getChild("c");
+		assertEquals(0, mergedC.getChildren().size());
+		assertFalse(mergedC.isPhantom());
+		assertEquals(PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS, mergedC.getSemantics());
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4d8b2e1a/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java
index 999b346..48f1549 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/Property.java
@@ -48,405 +48,417 @@ import org.apache.cayenne.reflect.PropertyUtils;
  */
 public class Property<E> {
 
-    /**
-     * Name of the property in the object
-     */
-    private final String name;
-
-    /**
-     * Constructs a new property with the given name.
-     */
-    public Property(String name) {
-        this.name = name;
-    }
-
-    /**
-     * @return Name of the property in the object.
-     */
-    public String getName() {
-        return name;
-    }
-
-    @Override
-    public int hashCode() {
-        return getName().hashCode();
-    }
-
-    @Override
-    public boolean equals(Object obj) {
-        return obj instanceof Property && ((Property<?>) obj).getName().equals(getName());
-    }
-
-    /**
-     * @return Constructs a property path by appending the argument to the
-     *         existing property separated by a dot
-     */
-    public Property<Object> dot(String property) {
-        return new Property<Object>(getName() + "." + property);
-    }
-
-    /**
-     * @return Constructs a property path by appending the argument to the
-     *         existing property separated by a dot
-     */
-    public <T> Property<T> dot(Property<T> property) {
-        return new Property<T>(getName() + "." + property.getName());
-    }
-
-    /**
-     * Returns a version of this property that represents an OUTER join. It is
-     * up to caller to ensure that the property corresponds to a relationship,
-     * as "outer" attributes make no sense.
-     */
-    public Property<E> outer() {
-        return isOuter() ? this: new Property<E>(name + "+"); 
-    }
-    
-    private boolean isOuter() {
-        return name.endsWith("+");
-    }
-
-    /**
-     * @return An expression representing null.
-     */
-    public Expression isNull() {
-        return ExpressionFactory.matchExp(getName(), null);
-    }
-
-    /**
-     * @return An expression representing a non-null value.
-     */
-    public Expression isNotNull() {
-        return ExpressionFactory.matchExp(getName(), null).notExp();
-    }
-
-    /**
-     * @return An expression representing equality to TRUE.
-     */
-    public Expression isTrue() {
-        return ExpressionFactory.matchExp(getName(), Boolean.TRUE);
-    }
-
-    /**
-     * @return An expression representing equality to FALSE.
-     */
-    public Expression isFalse() {
-        return ExpressionFactory.matchExp(getName(), Boolean.FALSE);
-    }
-
-    /**
-     * @return An expression representing equality to a value.
-     */
-    public Expression eq(E value) {
-        return ExpressionFactory.matchExp(getName(), value);
-    }
-
-    /**
-     * @return An expression representing equality between two attributes
-     *         (columns).
-     */
-    public Expression eq(Property<?> value) {
-        return ExpressionFactory.matchExp(getName(), new ASTObjPath(value.getName()));
-    }
-
-    /**
-     * @return An expression representing inequality to a value.
-     */
-    public Expression ne(E value) {
-        return ExpressionFactory.noMatchExp(getName(), value);
-    }
-
-    /**
-     * @return An expression representing inequality between two attributes
-     *         (columns).
-     */
-    public Expression ne(Property<?> value) {
-        return ExpressionFactory.noMatchExp(getName(), new ASTObjPath(value.getName()));
-    }
-
-    /**
-     * @return An expression for a Database "Like" query.
-     */
-    public Expression like(E value) {
-        return ExpressionFactory.likeExp(getName(), value);
-    }
-
-    /**
-     * @return An expression for a case insensitive "Like" query.
-     */
-    public Expression likeInsensitive(E value) {
-        return ExpressionFactory.likeIgnoreCaseExp(getName(), value);
-    }
-
-    /**
-     * @return An expression for a Database "NOT LIKE" query.
-     */
-    public Expression nlike(E value) {
-        return ExpressionFactory.notLikeExp(getName(), value);
-    }
-
-    /**
-     * @return An expression for a case insensitive "NOT LIKE" query.
-     */
-    public Expression nlikeInsensitive(E value) {
-        return ExpressionFactory.notLikeIgnoreCaseExp(getName(), value);
-    }
-
-    /**
-     * @return An expression checking for objects between a lower and upper
-     *         bound inclusive
-     * 
-     * @param lower
-     *            The lower bound.
-     * @param upper
-     *            The upper bound.
-     */
-    public Expression between(E lower, E upper) {
-        return ExpressionFactory.betweenExp(getName(), lower, upper);
-    }
-
-    /**
-     * @return An expression for finding objects with values in the given set.
-     */
-    public Expression in(E firstValue, E... moreValues) {
-
-        int moreValuesLength = moreValues != null ? moreValues.length : 0;
-
-        Object[] values = new Object[moreValuesLength + 1];
-        values[0] = firstValue;
-
-        if (moreValuesLength > 0) {
-            System.arraycopy(moreValues, 0, values, 1, moreValuesLength);
-        }
-
-        return ExpressionFactory.inExp(getName(), values);
-    }
-
-    /**
-     * @return An expression for finding objects with values not in the given
-     *         set.
-     */
-    public Expression nin(E firstValue, E... moreValues) {
-
-        int moreValuesLength = moreValues != null ? moreValues.length : 0;
-
-        Object[] values = new Object[moreValuesLength + 1];
-        values[0] = firstValue;
-
-        if (moreValuesLength > 0) {
-            System.arraycopy(moreValues, 0, values, 1, moreValuesLength);
-        }
-
-        return ExpressionFactory.notInExp(getName(), values);
-    }
-
-    /**
-     * @return An expression for finding objects with values in the given set.
-     */
-    public Expression in(Collection<E> values) {
-        return ExpressionFactory.inExp(getName(), values);
-    }
-
-    /**
-     * @return An expression for finding objects with values not in the given
-     *         set.
-     */
-    public Expression nin(Collection<E> values) {
-        return ExpressionFactory.notInExp(getName(), values);
-    }
-
-    /**
-     * @return A greater than Expression.
-     */
-    public Expression gt(E value) {
-        return ExpressionFactory.greaterExp(getName(), value);
-    }
-
-    /**
-     * @return Represents a greater than relationship between two attributes
-     *         (columns).
-     */
-    public Expression gt(Property<?> value) {
-        return ExpressionFactory.greaterExp(getName(), new ASTObjPath(value.getName()));
-    }
-
-    /**
-     * @return A greater than or equal to Expression.
-     */
-    public Expression gte(E value) {
-        return ExpressionFactory.greaterOrEqualExp(getName(), value);
-    }
-
-    /**
-     * @return Represents a greater than or equal relationship between two
-     *         attributes (columns).
-     */
-    public Expression gte(Property<?> value) {
-        return ExpressionFactory.greaterOrEqualExp(getName(), new ASTObjPath(value.getName()));
-    }
-
-    /**
-     * @return A less than Expression.
-     */
-    public Expression lt(E value) {
-        return ExpressionFactory.lessExp(getName(), value);
-    }
-
-    /**
-     * @return Represents a less than relationship between two attributes
-     *         (columns).
-     */
-    public Expression lt(Property<?> value) {
-        return ExpressionFactory.lessExp(getName(), new ASTObjPath(value.getName()));
-    }
-
-    /**
-     * @return A less than or equal to Expression.
-     */
-    public Expression lte(E value) {
-        return ExpressionFactory.lessOrEqualExp(getName(), value);
-    }
-
-    /**
-     * @return Represents a less than or equal relationship between two
-     *         attributes (columns).
-     */
-    public Expression lte(Property<?> value) {
-        return ExpressionFactory.lessOrEqualExp(getName(), new ASTObjPath(value.getName()));
-    }
-
-    /**
-     * @return Ascending sort orderings on this property.
-     */
-    public Ordering asc() {
-        return new Ordering(getName(), SortOrder.ASCENDING);
-    }
-
-    /**
-     * @return Ascending sort orderings on this property.
-     */
-    public List<Ordering> ascs() {
-        List<Ordering> result = new ArrayList<Ordering>(1);
-        result.add(asc());
-        return result;
-    }
-
-    /**
-     * @return Ascending case insensitive sort orderings on this property.
-     */
-    public Ordering ascInsensitive() {
-        return new Ordering(getName(), SortOrder.ASCENDING_INSENSITIVE);
-    }
-
-    /**
-     * @return Ascending case insensitive sort orderings on this property.
-     */
-    public List<Ordering> ascInsensitives() {
-        List<Ordering> result = new ArrayList<Ordering>(1);
-        result.add(ascInsensitive());
-        return result;
-    }
-
-    /**
-     * @return Descending sort orderings on this property.
-     */
-    public Ordering desc() {
-        return new Ordering(getName(), SortOrder.DESCENDING);
-    }
-
-    /**
-     * @return Descending sort orderings on this property.
-     */
-    public List<Ordering> descs() {
-        List<Ordering> result = new ArrayList<Ordering>(1);
-        result.add(desc());
-        return result;
-    }
-
-    /**
-     * @return Descending case insensitive sort orderings on this property.
-     */
-    public Ordering descInsensitive() {
-        return new Ordering(getName(), SortOrder.DESCENDING_INSENSITIVE);
-    }
-
-    /**
-     * @return Descending case insensitive sort orderings on this property.
-     */
-    public List<Ordering> descInsensitives() {
-        List<Ordering> result = new ArrayList<Ordering>(1);
-        result.add(descInsensitive());
-        return result;
-    }
-
-    public PrefetchTreeNode joint() {
-        PrefetchTreeNode node = prefetch();
-        node.setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
-        return node;
-    }
-
-    public PrefetchTreeNode disjoint() {
-        PrefetchTreeNode node = prefetch();
-        node.setSemantics(PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
-        return node;
-    }
-
-    public PrefetchTreeNode disjointById() {
-        PrefetchTreeNode node = prefetch();
-        node.setSemantics(PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS);
-        return node;
-    }
-
-    PrefetchTreeNode prefetch() {
-
-        // TODO: not very efficient - we are creating a prefetch that
-        // SelectQuery would throw away and recreate...
-        PrefetchTreeNode root = new PrefetchTreeNode();
-        PrefetchTreeNode node = root.addPath(name);
-        node.setPhantom(false);
-        return node;
-    }
-
-    /**
-     * Extracts property value from an object using JavaBean-compatible
-     * introspection with one addition - a property can be a dot-separated
-     * property name path.
-     */
-    @SuppressWarnings("unchecked")
-    public E getFrom(Object bean) {
-        return (E) PropertyUtils.getProperty(bean, getName());
-    }
-
-    /**
-     * Extracts property value from a collection of objects using
-     * JavaBean-compatible introspection with one addition - a property can be a
-     * dot-separated property name path.
-     */
-    public List<E> getFromAll(Collection<?> beans) {
-        List<E> result = new ArrayList<E>(beans.size());
-        for (Object bean : beans) {
-            result.add(getFrom(bean));
-        }
-        return result;
-    }
-
-    /**
-     * Sets a property value in 'obj' using JavaBean-compatible introspection
-     * with one addition - a property can be a dot-separated property name path.
-     */
-    public void setIn(Object bean, E value) {
-        PropertyUtils.setProperty(bean, getName(), value);
-    }
-
-    /**
-     * Sets a property value in a collection of objects using
-     * JavaBean-compatible introspection with one addition - a property can be a
-     * dot-separated property name path.
-     */
-    public void setInAll(Collection<?> beans, E value) {
-        for (Object bean : beans) {
-            setIn(bean, value);
-        }
-    }
+	/**
+	 * Name of the property in the object
+	 */
+	private final String name;
+
+	/**
+	 * Constructs a new property with the given name.
+	 */
+	public Property(String name) {
+		this.name = name;
+	}
+
+	/**
+	 * @return Name of the property in the object.
+	 */
+	public String getName() {
+		return name;
+	}
+
+	@Override
+	public int hashCode() {
+		return getName().hashCode();
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		return obj instanceof Property && ((Property<?>) obj).getName().equals(getName());
+	}
+
+	/**
+	 * @return Constructs a property path by appending the argument to the
+	 *         existing property separated by a dot
+	 */
+	public Property<Object> dot(String property) {
+		return new Property<Object>(getName() + "." + property);
+	}
+
+	/**
+	 * @return Constructs a property path by appending the argument to the
+	 *         existing property separated by a dot
+	 */
+	public <T> Property<T> dot(Property<T> property) {
+		return new Property<T>(getName() + "." + property.getName());
+	}
+
+	/**
+	 * Returns a version of this property that represents an OUTER join. It is
+	 * up to caller to ensure that the property corresponds to a relationship,
+	 * as "outer" attributes make no sense.
+	 */
+	public Property<E> outer() {
+		return isOuter() ? this : new Property<E>(name + "+");
+	}
+
+	private boolean isOuter() {
+		return name.endsWith("+");
+	}
+
+	/**
+	 * @return An expression representing null.
+	 */
+	public Expression isNull() {
+		return ExpressionFactory.matchExp(getName(), null);
+	}
+
+	/**
+	 * @return An expression representing a non-null value.
+	 */
+	public Expression isNotNull() {
+		return ExpressionFactory.matchExp(getName(), null).notExp();
+	}
+
+	/**
+	 * @return An expression representing equality to TRUE.
+	 */
+	public Expression isTrue() {
+		return ExpressionFactory.matchExp(getName(), Boolean.TRUE);
+	}
+
+	/**
+	 * @return An expression representing equality to FALSE.
+	 */
+	public Expression isFalse() {
+		return ExpressionFactory.matchExp(getName(), Boolean.FALSE);
+	}
+
+	/**
+	 * @return An expression representing equality to a value.
+	 */
+	public Expression eq(E value) {
+		return ExpressionFactory.matchExp(getName(), value);
+	}
+
+	/**
+	 * @return An expression representing equality between two attributes
+	 *         (columns).
+	 */
+	public Expression eq(Property<?> value) {
+		return ExpressionFactory.matchExp(getName(), new ASTObjPath(value.getName()));
+	}
+
+	/**
+	 * @return An expression representing inequality to a value.
+	 */
+	public Expression ne(E value) {
+		return ExpressionFactory.noMatchExp(getName(), value);
+	}
+
+	/**
+	 * @return An expression representing inequality between two attributes
+	 *         (columns).
+	 */
+	public Expression ne(Property<?> value) {
+		return ExpressionFactory.noMatchExp(getName(), new ASTObjPath(value.getName()));
+	}
+
+	/**
+	 * @return An expression for a Database "Like" query.
+	 */
+	public Expression like(E value) {
+		return ExpressionFactory.likeExp(getName(), value);
+	}
+
+	/**
+	 * @return An expression for a case insensitive "Like" query.
+	 */
+	public Expression likeInsensitive(E value) {
+		return ExpressionFactory.likeIgnoreCaseExp(getName(), value);
+	}
+
+	/**
+	 * @return An expression for a Database "NOT LIKE" query.
+	 */
+	public Expression nlike(E value) {
+		return ExpressionFactory.notLikeExp(getName(), value);
+	}
+
+	/**
+	 * @return An expression for a case insensitive "NOT LIKE" query.
+	 */
+	public Expression nlikeInsensitive(E value) {
+		return ExpressionFactory.notLikeIgnoreCaseExp(getName(), value);
+	}
+
+	/**
+	 * @return An expression checking for objects between a lower and upper
+	 *         bound inclusive
+	 * 
+	 * @param lower
+	 *            The lower bound.
+	 * @param upper
+	 *            The upper bound.
+	 */
+	public Expression between(E lower, E upper) {
+		return ExpressionFactory.betweenExp(getName(), lower, upper);
+	}
+
+	/**
+	 * @return An expression for finding objects with values in the given set.
+	 */
+	public Expression in(E firstValue, E... moreValues) {
+
+		int moreValuesLength = moreValues != null ? moreValues.length : 0;
+
+		Object[] values = new Object[moreValuesLength + 1];
+		values[0] = firstValue;
+
+		if (moreValuesLength > 0) {
+			System.arraycopy(moreValues, 0, values, 1, moreValuesLength);
+		}
+
+		return ExpressionFactory.inExp(getName(), values);
+	}
+
+	/**
+	 * @return An expression for finding objects with values not in the given
+	 *         set.
+	 */
+	public Expression nin(E firstValue, E... moreValues) {
+
+		int moreValuesLength = moreValues != null ? moreValues.length : 0;
+
+		Object[] values = new Object[moreValuesLength + 1];
+		values[0] = firstValue;
+
+		if (moreValuesLength > 0) {
+			System.arraycopy(moreValues, 0, values, 1, moreValuesLength);
+		}
+
+		return ExpressionFactory.notInExp(getName(), values);
+	}
+
+	/**
+	 * @return An expression for finding objects with values in the given set.
+	 */
+	public Expression in(Collection<E> values) {
+		return ExpressionFactory.inExp(getName(), values);
+	}
+
+	/**
+	 * @return An expression for finding objects with values not in the given
+	 *         set.
+	 */
+	public Expression nin(Collection<E> values) {
+		return ExpressionFactory.notInExp(getName(), values);
+	}
+
+	/**
+	 * @return A greater than Expression.
+	 */
+	public Expression gt(E value) {
+		return ExpressionFactory.greaterExp(getName(), value);
+	}
+
+	/**
+	 * @return Represents a greater than relationship between two attributes
+	 *         (columns).
+	 */
+	public Expression gt(Property<?> value) {
+		return ExpressionFactory.greaterExp(getName(), new ASTObjPath(value.getName()));
+	}
+
+	/**
+	 * @return A greater than or equal to Expression.
+	 */
+	public Expression gte(E value) {
+		return ExpressionFactory.greaterOrEqualExp(getName(), value);
+	}
+
+	/**
+	 * @return Represents a greater than or equal relationship between two
+	 *         attributes (columns).
+	 */
+	public Expression gte(Property<?> value) {
+		return ExpressionFactory.greaterOrEqualExp(getName(), new ASTObjPath(value.getName()));
+	}
+
+	/**
+	 * @return A less than Expression.
+	 */
+	public Expression lt(E value) {
+		return ExpressionFactory.lessExp(getName(), value);
+	}
+
+	/**
+	 * @return Represents a less than relationship between two attributes
+	 *         (columns).
+	 */
+	public Expression lt(Property<?> value) {
+		return ExpressionFactory.lessExp(getName(), new ASTObjPath(value.getName()));
+	}
+
+	/**
+	 * @return A less than or equal to Expression.
+	 */
+	public Expression lte(E value) {
+		return ExpressionFactory.lessOrEqualExp(getName(), value);
+	}
+
+	/**
+	 * @return Represents a less than or equal relationship between two
+	 *         attributes (columns).
+	 */
+	public Expression lte(Property<?> value) {
+		return ExpressionFactory.lessOrEqualExp(getName(), new ASTObjPath(value.getName()));
+	}
+
+	/**
+	 * @return Ascending sort orderings on this property.
+	 */
+	public Ordering asc() {
+		return new Ordering(getName(), SortOrder.ASCENDING);
+	}
+
+	/**
+	 * @return Ascending sort orderings on this property.
+	 */
+	public List<Ordering> ascs() {
+		List<Ordering> result = new ArrayList<Ordering>(1);
+		result.add(asc());
+		return result;
+	}
+
+	/**
+	 * @return Ascending case insensitive sort orderings on this property.
+	 */
+	public Ordering ascInsensitive() {
+		return new Ordering(getName(), SortOrder.ASCENDING_INSENSITIVE);
+	}
+
+	/**
+	 * @return Ascending case insensitive sort orderings on this property.
+	 */
+	public List<Ordering> ascInsensitives() {
+		List<Ordering> result = new ArrayList<Ordering>(1);
+		result.add(ascInsensitive());
+		return result;
+	}
+
+	/**
+	 * @return Descending sort orderings on this property.
+	 */
+	public Ordering desc() {
+		return new Ordering(getName(), SortOrder.DESCENDING);
+	}
+
+	/**
+	 * @return Descending sort orderings on this property.
+	 */
+	public List<Ordering> descs() {
+		List<Ordering> result = new ArrayList<Ordering>(1);
+		result.add(desc());
+		return result;
+	}
+
+	/**
+	 * @return Descending case insensitive sort orderings on this property.
+	 */
+	public Ordering descInsensitive() {
+		return new Ordering(getName(), SortOrder.DESCENDING_INSENSITIVE);
+	}
+
+	/**
+	 * @return Descending case insensitive sort orderings on this property.
+	 */
+	public List<Ordering> descInsensitives() {
+		List<Ordering> result = new ArrayList<Ordering>(1);
+		result.add(descInsensitive());
+		return result;
+	}
+
+	/**
+	 * Returns a prefetch tree that follows this property path, potentially
+	 * spanning a number of phantom nodes, and having a single leaf with "joint"
+	 * prefetch semantics.
+	 */
+	public PrefetchTreeNode joint() {
+		PrefetchTreeNode node = prefetch();
+		node.setSemantics(PrefetchTreeNode.JOINT_PREFETCH_SEMANTICS);
+		return node.getRoot();
+	}
+
+	/**
+	 * Returns a prefetch tree that follows this property path, potentially
+	 * spanning a number of phantom nodes, and having a single leaf with
+	 * "disjoint" prefetch semantics.
+	 */
+	public PrefetchTreeNode disjoint() {
+		PrefetchTreeNode node = prefetch();
+		node.setSemantics(PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
+		return node.getRoot();
+	}
+
+	/**
+	 * Returns a prefetch tree that follows this property path, potentially
+	 * spanning a number of phantom nodes, and having a single leaf with
+	 * "disjoint by id" prefetch semantics.
+	 */
+	public PrefetchTreeNode disjointById() {
+		PrefetchTreeNode node = prefetch();
+		node.setSemantics(PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS);
+		return node.getRoot();
+	}
+
+	PrefetchTreeNode prefetch() {
+		PrefetchTreeNode root = new PrefetchTreeNode();
+		PrefetchTreeNode node = root.addPath(name);
+		node.setPhantom(false);
+		return node;
+	}
+
+	/**
+	 * Extracts property value from an object using JavaBean-compatible
+	 * introspection with one addition - a property can be a dot-separated
+	 * property name path.
+	 */
+	@SuppressWarnings("unchecked")
+	public E getFrom(Object bean) {
+		return (E) PropertyUtils.getProperty(bean, getName());
+	}
+
+	/**
+	 * Extracts property value from a collection of objects using
+	 * JavaBean-compatible introspection with one addition - a property can be a
+	 * dot-separated property name path.
+	 */
+	public List<E> getFromAll(Collection<?> beans) {
+		List<E> result = new ArrayList<E>(beans.size());
+		for (Object bean : beans) {
+			result.add(getFrom(bean));
+		}
+		return result;
+	}
+
+	/**
+	 * Sets a property value in 'obj' using JavaBean-compatible introspection
+	 * with one addition - a property can be a dot-separated property name path.
+	 */
+	public void setIn(Object bean, E value) {
+		PropertyUtils.setProperty(bean, getName(), value);
+	}
+
+	/**
+	 * Sets a property value in a collection of objects using
+	 * JavaBean-compatible introspection with one addition - a property can be a
+	 * dot-separated property name path.
+	 */
+	public void setInAll(Collection<?> beans, E value) {
+		for (Object bean : beans) {
+			setIn(bean, value);
+		}
+	}
 
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4d8b2e1a/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java b/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java
index d97e811..7f7c6dd 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/BaseQueryMetadata.java
@@ -43,417 +43,430 @@ import org.apache.cayenne.util.XMLSerializable;
  */
 class BaseQueryMetadata implements QueryMetadata, XMLSerializable, Serializable {
 
-    int fetchLimit = QueryMetadata.FETCH_LIMIT_DEFAULT;
-    int fetchOffset = QueryMetadata.FETCH_OFFSET_DEFAULT;
-
-    int statementFetchSize = QueryMetadata.FETCH_OFFSET_DEFAULT;
-
-    int pageSize = QueryMetadata.PAGE_SIZE_DEFAULT;
-    boolean fetchingDataRows = QueryMetadata.FETCHING_DATA_ROWS_DEFAULT;
-    QueryCacheStrategy cacheStrategy = QueryCacheStrategy.getDefaultStrategy();
-
-    PrefetchTreeNode prefetchTree;
-    String cacheKey;
-    String[] cacheGroups;
-
-    transient List<Object> resultSetMapping;
-    transient DbEntity dbEntity;
-    transient DataMap dataMap;
-    transient Object lastRoot;
-    transient ClassDescriptor classDescriptor;
-    transient EntityResolver lastEntityResolver;
-
-    /**
-     * Copies values of another QueryMetadata object to this object.
-     */
-    void copyFromInfo(QueryMetadata info) {
-        this.lastEntityResolver = null;
-        this.lastRoot = null;
-        this.classDescriptor = null;
-        this.dbEntity = null;
-        this.dataMap = null;
-
-        this.fetchingDataRows = info.isFetchingDataRows();
-        this.fetchLimit = info.getFetchLimit();
-        this.pageSize = info.getPageSize();
-        this.cacheStrategy = info.getCacheStrategy();
-        this.cacheKey = info.getCacheKey();
-        this.cacheGroups = info.getCacheGroups();
-        this.resultSetMapping = info.getResultSetMapping();
-
-        setPrefetchTree(info.getPrefetchTree());
-    }
-
-    boolean resolve(Object root, EntityResolver resolver, String cacheKey) {
-
-        if (lastRoot != root || lastEntityResolver != resolver) {
-
-            this.cacheKey = cacheKey;
-
-            this.classDescriptor = null;
-            this.dbEntity = null;
-            this.dataMap = null;
-
-            ObjEntity entity = null;
-
-            if (root != null) {
-                if (root instanceof Class<?>) {
-                    entity = resolver.getObjEntity((Class<?>) root);
-                    if (entity == null) { // entity not found, try to resolve it
-                                          // with
-                        // client resolver
-                        EntityResolver clientResolver = resolver.getClientEntityResolver();
-                        if (clientResolver != resolver) {
-                            ObjEntity clientEntity = clientResolver.getObjEntity((Class<?>) root);
-
-                            if (clientEntity != null) {
-                                entity = resolver.getObjEntity(clientEntity.getName());
-                            }
-                        }
-                    }
-
-                    if (entity != null) {
-                        this.dbEntity = entity.getDbEntity();
-                        this.dataMap = entity.getDataMap();
-                    }
-                } else if (root instanceof ObjEntity) {
-                    entity = (ObjEntity) root;
-                    this.dbEntity = entity.getDbEntity();
-                    this.dataMap = entity.getDataMap();
-                } else if (root instanceof String) {
-                    entity = resolver.getObjEntity((String) root);
-                    if (entity != null) {
-                        this.dbEntity = entity.getDbEntity();
-                        this.dataMap = entity.getDataMap();
-                    }
-                } else if (root instanceof DbEntity) {
-                    this.dbEntity = (DbEntity) root;
-                    this.dataMap = dbEntity.getDataMap();
-                } else if (root instanceof DataMap) {
-                    this.dataMap = (DataMap) root;
-                } else if (root instanceof Persistent) {
-                    entity = resolver.getObjEntity((Persistent) root);
-                    if (entity != null) {
-                        this.dbEntity = entity.getDbEntity();
-                        this.dataMap = entity.getDataMap();
-                    }
-                }
-            }
-
-            if (entity != null) {
-                this.classDescriptor = resolver.getClassDescriptor(entity.getName());
-            }
-
-            this.lastRoot = root;
-            this.lastEntityResolver = resolver;
-
-            return true;
-        }
-
-        return false;
-    }
-
-    void initWithProperties(Map<String, ?> properties) {
-        // must init defaults even if properties are empty
-        if (properties == null) {
-            properties = Collections.EMPTY_MAP;
-        }
-
-        Object fetchOffset = properties.get(QueryMetadata.FETCH_OFFSET_PROPERTY);
-        Object fetchLimit = properties.get(QueryMetadata.FETCH_LIMIT_PROPERTY);
-        Object pageSize = properties.get(QueryMetadata.PAGE_SIZE_PROPERTY);
-        Object statementFetchSize = properties.get(QueryMetadata.STATEMENT_FETCH_SIZE_PROPERTY);
-        Object fetchingDataRows = properties.get(QueryMetadata.FETCHING_DATA_ROWS_PROPERTY);
-
-        Object cacheStrategy = properties.get(QueryMetadata.CACHE_STRATEGY_PROPERTY);
-
-        Object cacheGroups = properties.get(QueryMetadata.CACHE_GROUPS_PROPERTY);
-
-        // init ivars from properties
-        this.fetchOffset = (fetchOffset != null) ? Integer.parseInt(fetchOffset.toString())
-                : QueryMetadata.FETCH_OFFSET_DEFAULT;
-
-        this.fetchLimit = (fetchLimit != null) ? Integer.parseInt(fetchLimit.toString())
-                : QueryMetadata.FETCH_LIMIT_DEFAULT;
-
-        this.pageSize = (pageSize != null) ? Integer.parseInt(pageSize.toString()) : QueryMetadata.PAGE_SIZE_DEFAULT;
-
-        this.statementFetchSize = (statementFetchSize != null) ? Integer.parseInt(statementFetchSize.toString())
-                : QueryMetadata.STATEMENT_FETCH_SIZE_DEFAULT;
-
-        this.fetchingDataRows = (fetchingDataRows != null) ? "true".equalsIgnoreCase(fetchingDataRows.toString())
-                : QueryMetadata.FETCHING_DATA_ROWS_DEFAULT;
-
-        this.cacheStrategy = (cacheStrategy != null) ? QueryCacheStrategy.safeValueOf(cacheStrategy.toString())
-                : QueryCacheStrategy.getDefaultStrategy();
-
-        this.cacheGroups = null;
-        if (cacheGroups instanceof String[]) {
-            this.cacheGroups = (String[]) cacheGroups;
-        } else if (cacheGroups instanceof String) {
-            StringTokenizer toks = new StringTokenizer(cacheGroups.toString(), ",");
-            this.cacheGroups = new String[toks.countTokens()];
-            for (int i = 0; i < this.cacheGroups.length; i++) {
-                this.cacheGroups[i] = toks.nextToken();
-            }
-        }
-    }
-
-    public void encodeAsXML(XMLEncoder encoder) {
-
-        if (fetchingDataRows != QueryMetadata.FETCHING_DATA_ROWS_DEFAULT) {
-            encoder.printProperty(QueryMetadata.FETCHING_DATA_ROWS_PROPERTY, fetchingDataRows);
-        }
-
-        if (fetchOffset != QueryMetadata.FETCH_OFFSET_DEFAULT) {
-            encoder.printProperty(QueryMetadata.FETCH_OFFSET_PROPERTY, fetchOffset);
-        }
-
-        if (fetchLimit != QueryMetadata.FETCH_LIMIT_DEFAULT) {
-            encoder.printProperty(QueryMetadata.FETCH_LIMIT_PROPERTY, fetchLimit);
-        }
-
-        if (pageSize != QueryMetadata.PAGE_SIZE_DEFAULT) {
-            encoder.printProperty(QueryMetadata.PAGE_SIZE_PROPERTY, pageSize);
-        }
-
-        if (cacheStrategy != null && QueryCacheStrategy.getDefaultStrategy() != cacheStrategy) {
-            encoder.printProperty(QueryMetadata.CACHE_STRATEGY_PROPERTY, cacheStrategy.name());
-        }
-
-        if (statementFetchSize != QueryMetadata.STATEMENT_FETCH_SIZE_DEFAULT) {
-            encoder.printProperty(QueryMetadata.STATEMENT_FETCH_SIZE_PROPERTY, statementFetchSize);
-        }
-
-        if (prefetchTree != null) {
-            prefetchTree.encodeAsXML(encoder);
-        }
-
-        if (cacheGroups != null && cacheGroups.length > 0) {
-            StringBuilder buffer = new StringBuilder(cacheGroups[0]);
-            for (int i = 1; i < cacheGroups.length; i++) {
-                buffer.append(',').append(cacheGroups[i]);
-            }
-            encoder.printProperty(QueryMetadata.CACHE_GROUPS_PROPERTY, buffer.toString());
-        }
-    }
-
-    /**
-     * @since 1.2
-     */
-    public String getCacheKey() {
-        return cacheKey;
-    }
-
-    /**
-     * @since 1.2
-     */
-    public DataMap getDataMap() {
-        return dataMap;
-    }
-
-    /**
-     * @since 1.2
-     */
-    public Procedure getProcedure() {
-        return null;
-    }
-
-    /**
-     * @since 3.0
-     */
-    public Map<String, String> getPathSplitAliases() {
-        return Collections.emptyMap();
-    }
-
-    /**
-     * @since 1.2
-     */
-    public DbEntity getDbEntity() {
-        return dbEntity;
-    }
-
-    /**
-     * @since 1.2
-     */
-    public ObjEntity getObjEntity() {
-        return classDescriptor != null ? classDescriptor.getEntity() : null;
-    }
-
-    /**
-     * @since 3.0
-     */
-    public ClassDescriptor getClassDescriptor() {
-        return classDescriptor;
-    }
-
-    /**
-     * @since 3.0
-     */
-    public List<Object> getResultSetMapping() {
-        return resultSetMapping;
-    }
-
-    /**
-     * @since 1.2
-     */
-    public PrefetchTreeNode getPrefetchTree() {
-        return prefetchTree;
-    }
-
-    void setPrefetchTree(PrefetchTreeNode prefetchTree) {
-        this.prefetchTree = prefetchTree != null ? deepClone(prefetchTree, null) : null;
-    }
-
-    private PrefetchTreeNode deepClone(PrefetchTreeNode source, PrefetchTreeNode targetParent) {
-
-        PrefetchTreeNode target = new PrefetchTreeNode(targetParent, source.getName());
-        target.setEjbqlPathEntityId(source.getEjbqlPathEntityId());
-        target.setEntityName(source.getEntityName());
-        target.setPhantom(source.isPhantom());
-        target.setSemantics(source.getSemantics());
-
-        for (PrefetchTreeNode child : source.getChildren()) {
-            target.addChild(deepClone(child, target));
-        }
-
-        return target;
-    }
-
-    /**
-     * @since 3.0
-     */
-    public QueryCacheStrategy getCacheStrategy() {
-        return cacheStrategy;
-    }
-
-    /**
-     * @since 3.0
-     */
-    void setCacheStrategy(QueryCacheStrategy cacheStrategy) {
-        this.cacheStrategy = cacheStrategy;
-    }
-
-    /**
-     * @since 3.0
-     */
-    public String[] getCacheGroups() {
-        return cacheGroups;
-    }
-
-    /**
-     * @since 3.0
-     */
-    void setCacheGroups(String... groups) {
-        this.cacheGroups = groups;
-    }
-
-    public boolean isFetchingDataRows() {
-        return fetchingDataRows;
-    }
-
-    public int getFetchLimit() {
-        return fetchLimit;
-    }
-
-    public int getPageSize() {
-        return pageSize;
-    }
-
-    public Query getOrginatingQuery() {
-        return null;
-    }
-
-    /**
-     * @since 3.0
-     */
-    public int getFetchOffset() {
-        return fetchOffset;
-    }
-
-    public boolean isRefreshingObjects() {
-        return true;
-    }
-
-    void setFetchingDataRows(boolean b) {
-        fetchingDataRows = b;
-    }
-
-    void setFetchLimit(int i) {
-        fetchLimit = i;
-    }
-
-    void setFetchOffset(int i) {
-        fetchOffset = i;
-    }
-
-    void setPageSize(int i) {
-        pageSize = i;
-    }
-
-    /**
-     * Sets statement's fetch size (0 for no default size)
-     * 
-     * @since 3.0
-     */
-    void setStatementFetchSize(int size) {
-        this.statementFetchSize = size;
-    }
-
-    /**
-     * @return statement's fetch size
-     * @since 3.0
-     */
-    public int getStatementFetchSize() {
-        return statementFetchSize;
-    }
-
-    /**
-     * Adds a joint prefetch.
-     * 
-     * @since 1.2
-     */
-    PrefetchTreeNode addPrefetch(String path, int semantics) {
-        if (prefetchTree == null) {
-            prefetchTree = new PrefetchTreeNode();
-        }
-
-        PrefetchTreeNode node = prefetchTree.addPath(path);
-        node.setSemantics(semantics);
-        node.setPhantom(false);
-        return node;
-    }
-
-    /**
-     * Adds all prefetches from a provided collection.
-     * 
-     * @since 1.2
-     */
-    void addPrefetches(Collection<String> prefetches, int semantics) {
-        if (prefetches != null) {
-            for (String prefetch : prefetches) {
-                addPrefetch(prefetch, semantics);
-            }
-        }
-    }
-
-    /**
-     * Clears all joint prefetches.
-     * 
-     * @since 1.2
-     */
-    void clearPrefetches() {
-        prefetchTree = null;
-    }
-
-    /**
-     * Removes joint prefetch.
-     * 
-     * @since 1.2
-     */
-    void removePrefetch(String prefetch) {
-        if (prefetchTree != null) {
-            prefetchTree.removePath(prefetch);
-        }
-    }
+	int fetchLimit = QueryMetadata.FETCH_LIMIT_DEFAULT;
+	int fetchOffset = QueryMetadata.FETCH_OFFSET_DEFAULT;
+
+	int statementFetchSize = QueryMetadata.FETCH_OFFSET_DEFAULT;
+
+	int pageSize = QueryMetadata.PAGE_SIZE_DEFAULT;
+	boolean fetchingDataRows = QueryMetadata.FETCHING_DATA_ROWS_DEFAULT;
+	QueryCacheStrategy cacheStrategy = QueryCacheStrategy.getDefaultStrategy();
+
+	PrefetchTreeNode prefetchTree;
+	String cacheKey;
+	String[] cacheGroups;
+
+	transient List<Object> resultSetMapping;
+	transient DbEntity dbEntity;
+	transient DataMap dataMap;
+	transient Object lastRoot;
+	transient ClassDescriptor classDescriptor;
+	transient EntityResolver lastEntityResolver;
+
+	/**
+	 * Copies values of another QueryMetadata object to this object.
+	 */
+	void copyFromInfo(QueryMetadata info) {
+		this.lastEntityResolver = null;
+		this.lastRoot = null;
+		this.classDescriptor = null;
+		this.dbEntity = null;
+		this.dataMap = null;
+
+		this.fetchingDataRows = info.isFetchingDataRows();
+		this.fetchLimit = info.getFetchLimit();
+		this.pageSize = info.getPageSize();
+		this.cacheStrategy = info.getCacheStrategy();
+		this.cacheKey = info.getCacheKey();
+		this.cacheGroups = info.getCacheGroups();
+		this.resultSetMapping = info.getResultSetMapping();
+
+		setPrefetchTree(info.getPrefetchTree());
+	}
+
+	boolean resolve(Object root, EntityResolver resolver, String cacheKey) {
+
+		if (lastRoot != root || lastEntityResolver != resolver) {
+
+			this.cacheKey = cacheKey;
+
+			this.classDescriptor = null;
+			this.dbEntity = null;
+			this.dataMap = null;
+
+			ObjEntity entity = null;
+
+			if (root != null) {
+				if (root instanceof Class<?>) {
+					entity = resolver.getObjEntity((Class<?>) root);
+					if (entity == null) { // entity not found, try to resolve it
+											// with
+						// client resolver
+						EntityResolver clientResolver = resolver.getClientEntityResolver();
+						if (clientResolver != resolver) {
+							ObjEntity clientEntity = clientResolver.getObjEntity((Class<?>) root);
+
+							if (clientEntity != null) {
+								entity = resolver.getObjEntity(clientEntity.getName());
+							}
+						}
+					}
+
+					if (entity != null) {
+						this.dbEntity = entity.getDbEntity();
+						this.dataMap = entity.getDataMap();
+					}
+				} else if (root instanceof ObjEntity) {
+					entity = (ObjEntity) root;
+					this.dbEntity = entity.getDbEntity();
+					this.dataMap = entity.getDataMap();
+				} else if (root instanceof String) {
+					entity = resolver.getObjEntity((String) root);
+					if (entity != null) {
+						this.dbEntity = entity.getDbEntity();
+						this.dataMap = entity.getDataMap();
+					}
+				} else if (root instanceof DbEntity) {
+					this.dbEntity = (DbEntity) root;
+					this.dataMap = dbEntity.getDataMap();
+				} else if (root instanceof DataMap) {
+					this.dataMap = (DataMap) root;
+				} else if (root instanceof Persistent) {
+					entity = resolver.getObjEntity((Persistent) root);
+					if (entity != null) {
+						this.dbEntity = entity.getDbEntity();
+						this.dataMap = entity.getDataMap();
+					}
+				}
+			}
+
+			if (entity != null) {
+				this.classDescriptor = resolver.getClassDescriptor(entity.getName());
+			}
+
+			this.lastRoot = root;
+			this.lastEntityResolver = resolver;
+
+			return true;
+		}
+
+		return false;
+	}
+
+	void initWithProperties(Map<String, ?> properties) {
+		// must init defaults even if properties are empty
+		if (properties == null) {
+			properties = Collections.EMPTY_MAP;
+		}
+
+		Object fetchOffset = properties.get(QueryMetadata.FETCH_OFFSET_PROPERTY);
+		Object fetchLimit = properties.get(QueryMetadata.FETCH_LIMIT_PROPERTY);
+		Object pageSize = properties.get(QueryMetadata.PAGE_SIZE_PROPERTY);
+		Object statementFetchSize = properties.get(QueryMetadata.STATEMENT_FETCH_SIZE_PROPERTY);
+		Object fetchingDataRows = properties.get(QueryMetadata.FETCHING_DATA_ROWS_PROPERTY);
+
+		Object cacheStrategy = properties.get(QueryMetadata.CACHE_STRATEGY_PROPERTY);
+
+		Object cacheGroups = properties.get(QueryMetadata.CACHE_GROUPS_PROPERTY);
+
+		// init ivars from properties
+		this.fetchOffset = (fetchOffset != null) ? Integer.parseInt(fetchOffset.toString())
+				: QueryMetadata.FETCH_OFFSET_DEFAULT;
+
+		this.fetchLimit = (fetchLimit != null) ? Integer.parseInt(fetchLimit.toString())
+				: QueryMetadata.FETCH_LIMIT_DEFAULT;
+
+		this.pageSize = (pageSize != null) ? Integer.parseInt(pageSize.toString()) : QueryMetadata.PAGE_SIZE_DEFAULT;
+
+		this.statementFetchSize = (statementFetchSize != null) ? Integer.parseInt(statementFetchSize.toString())
+				: QueryMetadata.STATEMENT_FETCH_SIZE_DEFAULT;
+
+		this.fetchingDataRows = (fetchingDataRows != null) ? "true".equalsIgnoreCase(fetchingDataRows.toString())
+				: QueryMetadata.FETCHING_DATA_ROWS_DEFAULT;
+
+		this.cacheStrategy = (cacheStrategy != null) ? QueryCacheStrategy.safeValueOf(cacheStrategy.toString())
+				: QueryCacheStrategy.getDefaultStrategy();
+
+		this.cacheGroups = null;
+		if (cacheGroups instanceof String[]) {
+			this.cacheGroups = (String[]) cacheGroups;
+		} else if (cacheGroups instanceof String) {
+			StringTokenizer toks = new StringTokenizer(cacheGroups.toString(), ",");
+			this.cacheGroups = new String[toks.countTokens()];
+			for (int i = 0; i < this.cacheGroups.length; i++) {
+				this.cacheGroups[i] = toks.nextToken();
+			}
+		}
+	}
+
+	public void encodeAsXML(XMLEncoder encoder) {
+
+		if (fetchingDataRows != QueryMetadata.FETCHING_DATA_ROWS_DEFAULT) {
+			encoder.printProperty(QueryMetadata.FETCHING_DATA_ROWS_PROPERTY, fetchingDataRows);
+		}
+
+		if (fetchOffset != QueryMetadata.FETCH_OFFSET_DEFAULT) {
+			encoder.printProperty(QueryMetadata.FETCH_OFFSET_PROPERTY, fetchOffset);
+		}
+
+		if (fetchLimit != QueryMetadata.FETCH_LIMIT_DEFAULT) {
+			encoder.printProperty(QueryMetadata.FETCH_LIMIT_PROPERTY, fetchLimit);
+		}
+
+		if (pageSize != QueryMetadata.PAGE_SIZE_DEFAULT) {
+			encoder.printProperty(QueryMetadata.PAGE_SIZE_PROPERTY, pageSize);
+		}
+
+		if (cacheStrategy != null && QueryCacheStrategy.getDefaultStrategy() != cacheStrategy) {
+			encoder.printProperty(QueryMetadata.CACHE_STRATEGY_PROPERTY, cacheStrategy.name());
+		}
+
+		if (statementFetchSize != QueryMetadata.STATEMENT_FETCH_SIZE_DEFAULT) {
+			encoder.printProperty(QueryMetadata.STATEMENT_FETCH_SIZE_PROPERTY, statementFetchSize);
+		}
+
+		if (prefetchTree != null) {
+			prefetchTree.encodeAsXML(encoder);
+		}
+
+		if (cacheGroups != null && cacheGroups.length > 0) {
+			StringBuilder buffer = new StringBuilder(cacheGroups[0]);
+			for (int i = 1; i < cacheGroups.length; i++) {
+				buffer.append(',').append(cacheGroups[i]);
+			}
+			encoder.printProperty(QueryMetadata.CACHE_GROUPS_PROPERTY, buffer.toString());
+		}
+	}
+
+	/**
+	 * @since 1.2
+	 */
+	public String getCacheKey() {
+		return cacheKey;
+	}
+
+	/**
+	 * @since 1.2
+	 */
+	public DataMap getDataMap() {
+		return dataMap;
+	}
+
+	/**
+	 * @since 1.2
+	 */
+	public Procedure getProcedure() {
+		return null;
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	public Map<String, String> getPathSplitAliases() {
+		return Collections.emptyMap();
+	}
+
+	/**
+	 * @since 1.2
+	 */
+	public DbEntity getDbEntity() {
+		return dbEntity;
+	}
+
+	/**
+	 * @since 1.2
+	 */
+	public ObjEntity getObjEntity() {
+		return classDescriptor != null ? classDescriptor.getEntity() : null;
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	public ClassDescriptor getClassDescriptor() {
+		return classDescriptor;
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	public List<Object> getResultSetMapping() {
+		return resultSetMapping;
+	}
+
+	/**
+	 * @since 1.2
+	 */
+	public PrefetchTreeNode getPrefetchTree() {
+		return prefetchTree;
+	}
+
+	void setPrefetchTree(PrefetchTreeNode prefetchTree) {
+		this.prefetchTree = prefetchTree != null ? deepClone(prefetchTree, null) : null;
+	}
+
+	private PrefetchTreeNode deepClone(PrefetchTreeNode source, PrefetchTreeNode targetParent) {
+
+		PrefetchTreeNode target = new PrefetchTreeNode(targetParent, source.getName());
+		target.setEjbqlPathEntityId(source.getEjbqlPathEntityId());
+		target.setEntityName(source.getEntityName());
+		target.setPhantom(source.isPhantom());
+		target.setSemantics(source.getSemantics());
+
+		for (PrefetchTreeNode child : source.getChildren()) {
+			target.addChild(deepClone(child, target));
+		}
+
+		return target;
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	public QueryCacheStrategy getCacheStrategy() {
+		return cacheStrategy;
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	void setCacheStrategy(QueryCacheStrategy cacheStrategy) {
+		this.cacheStrategy = cacheStrategy;
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	public String[] getCacheGroups() {
+		return cacheGroups;
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	void setCacheGroups(String... groups) {
+		this.cacheGroups = groups;
+	}
+
+	public boolean isFetchingDataRows() {
+		return fetchingDataRows;
+	}
+
+	public int getFetchLimit() {
+		return fetchLimit;
+	}
+
+	public int getPageSize() {
+		return pageSize;
+	}
+
+	public Query getOrginatingQuery() {
+		return null;
+	}
+
+	/**
+	 * @since 3.0
+	 */
+	public int getFetchOffset() {
+		return fetchOffset;
+	}
+
+	public boolean isRefreshingObjects() {
+		return true;
+	}
+
+	void setFetchingDataRows(boolean b) {
+		fetchingDataRows = b;
+	}
+
+	void setFetchLimit(int i) {
+		fetchLimit = i;
+	}
+
+	void setFetchOffset(int i) {
+		fetchOffset = i;
+	}
+
+	void setPageSize(int i) {
+		pageSize = i;
+	}
+
+	/**
+	 * Sets statement's fetch size (0 for no default size)
+	 * 
+	 * @since 3.0
+	 */
+	void setStatementFetchSize(int size) {
+		this.statementFetchSize = size;
+	}
+
+	/**
+	 * @return statement's fetch size
+	 * @since 3.0
+	 */
+	public int getStatementFetchSize() {
+		return statementFetchSize;
+	}
+
+	/**
+	 * Adds a joint prefetch.
+	 * 
+	 * @since 1.2
+	 */
+	PrefetchTreeNode addPrefetch(String path, int semantics) {
+		if (prefetchTree == null) {
+			prefetchTree = new PrefetchTreeNode();
+		}
+
+		PrefetchTreeNode node = prefetchTree.addPath(path);
+		node.setSemantics(semantics);
+		node.setPhantom(false);
+		return node;
+	}
+
+	/**
+	 * Adds a joint prefetch.
+	 * 
+	 * @since 4.0
+	 */
+	void mergePrefetch(PrefetchTreeNode node) {
+		if (prefetchTree == null) {
+			prefetchTree = new PrefetchTreeNode();
+		}
+
+		prefetchTree.merge(node);
+	}
+
+	/**
+	 * Adds all prefetches from a provided collection.
+	 * 
+	 * @since 1.2
+	 */
+	void addPrefetches(Collection<String> prefetches, int semantics) {
+		if (prefetches != null) {
+			for (String prefetch : prefetches) {
+				addPrefetch(prefetch, semantics);
+			}
+		}
+	}
+
+	/**
+	 * Clears all joint prefetches.
+	 * 
+	 * @since 1.2
+	 */
+	void clearPrefetches() {
+		prefetchTree = null;
+	}
+
+	/**
+	 * Removes joint prefetch.
+	 * 
+	 * @since 1.2
+	 */
+	void removePrefetch(String prefetch) {
+		if (prefetchTree != null) {
+			prefetchTree.removePath(prefetch);
+		}
+	}
 }