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:27 UTC

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

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4d8b2e1a/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java b/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java
index 70b3a9f..11711fc 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/PrefetchTreeNode.java
@@ -38,535 +38,579 @@ import org.apache.cayenne.util.XMLSerializable;
  */
 public class PrefetchTreeNode implements Serializable, XMLSerializable {
 
-    public static final int UNDEFINED_SEMANTICS = 0;
-    public static final int JOINT_PREFETCH_SEMANTICS = 1;
-    public static final int DISJOINT_PREFETCH_SEMANTICS = 2;
-    public static final int DISJOINT_BY_ID_PREFETCH_SEMANTICS = 3;
-
-    protected String name;
-    protected boolean phantom;
-    protected int semantics;
-    protected String ejbqlPathEntityId;
-    protected String entityName;
-
-    // transient parent allows cloning parts of the tree via serialization
-    protected transient PrefetchTreeNode parent;
-
-    // Using Collection instead of Map for children storage (even though there cases of
-    // lookup by segment) is a reasonable tradeoff considering that
-    // each node has no more than a few children and lookup by name doesn't happen on
-    // traversal, only during creation.
-    protected Collection<PrefetchTreeNode> children;
-
-    /**
-     * Creates a root node of the prefetch tree. Children can be added to the parent by
-     * calling "addPath".
-     */
-    public PrefetchTreeNode() {
-        this(null, null);
-    }
-
-    /**
-     * Creates a phantom PrefetchTreeNode, initializing it with parent node and a name of
-     * a relationship segment connecting this node with the parent.
-     */
-    protected PrefetchTreeNode(PrefetchTreeNode parent, String name) {
-        this.parent = parent;
-        this.name = name;
-        this.phantom = true;
-        this.semantics = UNDEFINED_SEMANTICS;
-    }
-
-    public void encodeAsXML(XMLEncoder encoder) {
-        traverse(new XMLEncoderOperation(encoder));
-    }
-
-    /**
-     * Returns the root of the node tree. Root is the topmost parent node that itself has
-     * no parent set.
-     */
-    public PrefetchTreeNode getRoot() {
-        return (parent != null) ? parent.getRoot() : this;
-    }
-
-    /**
-     * Returns full prefetch path, that is a dot separated String of node names starting
-     * from root and up to and including this node. Note that root "name" is considered to
-     * be an empty string.
-     */
-    public String getPath() {
-        return getPath(null);
-    }
-
-    public String getPath(PrefetchTreeNode upTillParent) {
-        if (parent == null || upTillParent == this) {
-            return "";
-        }
-
-        StringBuilder path = new StringBuilder(getName());
-        PrefetchTreeNode node = this.getParent();
-
-        // root node has no path
-        while (node.getParent() != null && node != upTillParent) {
-            path.insert(0, node.getName() + ".");
-            node = node.getParent();
-        }
-
-        return path.toString();
-    }
-
-    /**
-     * Returns a subset of nodes with "joint" semantics that are to be prefetched in the
-     * same query as the current node. Result excludes this node, regardless of its
-     * semantics.
-     */
-    public Collection<PrefetchTreeNode> adjacentJointNodes() {
-        Collection<PrefetchTreeNode> c = new ArrayList<PrefetchTreeNode>();
-        traverse(new AdjacentJoinsOperation(c));
-        return c;
-    }
-
-    /**
-     * Returns a collection of PrefetchTreeNodes in this tree with joint semantics.
-     */
-    public Collection<PrefetchTreeNode> jointNodes() {
-        Collection<PrefetchTreeNode> c = new ArrayList<PrefetchTreeNode>();
-        traverse(new CollectionBuilderOperation(c, false, false, true, false, false));
-        return c;
-    }
-
-    /**
-     * Returns a collection of PrefetchTreeNodes with disjoint semantics.
-     */
-    public Collection<PrefetchTreeNode> disjointNodes() {
-        Collection<PrefetchTreeNode> c = new ArrayList<PrefetchTreeNode>();
-        traverse(new CollectionBuilderOperation(c, true, false, false, false, false));
-        return c;
-    }
-
-    /**
-     * Returns a collection of PrefetchTreeNodes with disjoint semantics
-     * @since 3.1
-     */
-    public Collection<PrefetchTreeNode> disjointByIdNodes() {
-        Collection<PrefetchTreeNode> c = new ArrayList<PrefetchTreeNode>();
-        traverse(new CollectionBuilderOperation(c, false, true, false, false, false));
-        return c;
-    }
-
-    /**
-     * Returns a collection of PrefetchTreeNodes that are not phantoms.
-     */
-    public Collection<PrefetchTreeNode> nonPhantomNodes() {
-        Collection<PrefetchTreeNode> c = new ArrayList<PrefetchTreeNode>();
-        traverse(new CollectionBuilderOperation(c, true, true, true, true, false));
-        return c;
-    }
-
-    /**
-     * Returns a clone of subtree that includes all joint children
-     * starting from this node itself and till the first occurrence of non-joint node
-     *
-     * @since 3.1
-     */
-    public PrefetchTreeNode cloneJointSubtree() {
-        return cloneJointSubtree(null);
-    }
-
-    private PrefetchTreeNode cloneJointSubtree(PrefetchTreeNode parent) {
-        PrefetchTreeNode cloned = new PrefetchTreeNode(parent, getName());
-        if (parent != null) {
-            cloned.setSemantics(getSemantics());
-            cloned.setPhantom(isPhantom());
-        }
-
-        if (children != null) {
-            for (PrefetchTreeNode child : children) {
-                if (child.isJointPrefetch()) {
-                    cloned.addChild(child.cloneJointSubtree(cloned));
-                }
-            }
-        }
-
-        return cloned;
-    }
-
-    /**
-     * Traverses the tree depth-first, invoking callback methods of the processor when
-     * passing through the nodes.
-     */
-    public void traverse(PrefetchProcessor processor) {
-
-        boolean result = false;
-
-        if (isPhantom()) {
-            result = processor.startPhantomPrefetch(this);
-        }
-        else if (isDisjointPrefetch()) {
-            result = processor.startDisjointPrefetch(this);
-        }
-        else if (isDisjointByIdPrefetch()) {
-            result = processor.startDisjointByIdPrefetch(this);
-        }
-        else if (isJointPrefetch()) {
-            result = processor.startJointPrefetch(this);
-        }
-        else {
-            result = processor.startUnknownPrefetch(this);
-        }
-
-        // process children unless processing is blocked...
-        if (result && children != null) {
-            for (PrefetchTreeNode child : children) {
-                child.traverse(processor);
-            }
-        }
-
-        // call finish regardless of whether children were processed
-        processor.finishPrefetch(this);
-    }
-
-    /**
-     * Looks up an existing node in the tree desribed by the dot-separated path. Will
-     * return null if no matching child exists.
-     */
-    public PrefetchTreeNode getNode(String path) {
-        if (Util.isEmptyString(path)) {
-            throw new IllegalArgumentException("Empty path: " + path);
-        }
-
-        PrefetchTreeNode node = this;
-        StringTokenizer toks = new StringTokenizer(path, Entity.PATH_SEPARATOR);
-        while (toks.hasMoreTokens() && node != null) {
-            String segment = toks.nextToken();
-            node = node.getChild(segment);
-        }
-
-        return node;
-    }
-
-    /**
-     * Adds a "path" with specified semantics to this prefetch node. All yet non-existent
-     * nodes in the created path will be marked as phantom.
-     *
-     * @return the last segment in the created path.
-     */
-    public PrefetchTreeNode addPath(String path) {
-        if (Util.isEmptyString(path)) {
-            throw new IllegalArgumentException("Empty path: " + path);
-        }
-
-        PrefetchTreeNode node = this;
-        StringTokenizer toks = new StringTokenizer(path, Entity.PATH_SEPARATOR);
-        while (toks.hasMoreTokens()) {
-            String segment = toks.nextToken();
-
-            PrefetchTreeNode child = node.getChild(segment);
-            if (child == null) {
-                child = new PrefetchTreeNode(node, segment);
-                node.addChild(child);
-            }
-
-            node = child;
-        }
-
-        return node;
-    }
-
-    /**
-     * Removes or makes phantom a node defined by this path. If the node for this path
-     * doesn't have any children, it is removed, otherwise it is made phantom.
-     */
-    public void removePath(String path) {
-
-        PrefetchTreeNode node = getNode(path);
-        while (node != null) {
-
-            if (node.children != null) {
-                node.setPhantom(true);
-                break;
-            }
-
-            String segment = node.getName();
-
-            node = node.getParent();
-
-            if (node != null) {
-                node.removeChild(segment);
-            }
-        }
-    }
-
-    public void addChild(PrefetchTreeNode child) {
-
-        if (Util.isEmptyString(child.getName())) {
-            throw new IllegalArgumentException("Child has no segmentPath: " + child);
-        }
-
-        if (child.getParent() != this) {
-            child.getParent().removeChild(child.getName());
-            child.parent = this;
-        }
-
-        if (children == null) {
-            children = new ArrayList<PrefetchTreeNode>(4);
-        }
-
-        children.add(child);
-    }
-
-    public void removeChild(PrefetchTreeNode child) {
-        if (children != null && child != null) {
-            children.remove(child);
-            child.parent = null;
-        }
-    }
-
-    protected void removeChild(String segment) {
-        if (children != null) {
-            PrefetchTreeNode child = getChild(segment);
-            if (child != null) {
-                children.remove(child);
-                child.parent = null;
-            }
-        }
-    }
-
-    protected PrefetchTreeNode getChild(String segment) {
-        if (children != null) {
-            for (PrefetchTreeNode child : children) {
-                if (segment.equals(child.getName())) {
-                    return child;
-                }
-            }
-        }
-
-        return null;
-    }
-
-    public PrefetchTreeNode getParent() {
-        return parent;
-    }
-
-    /**
-     * Returns an unmodifiable collection of children.
-     */
-    public Collection<PrefetchTreeNode> getChildren() {
-        return children == null ? Collections.EMPTY_SET : Collections
-                .unmodifiableCollection(children);
-    }
-
-    public boolean hasChildren() {
-        return children != null && !children.isEmpty();
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public boolean isPhantom() {
-        return phantom;
-    }
-
-    public void setPhantom(boolean phantom) {
-        this.phantom = phantom;
-    }
-
-    public int getSemantics() {
-        return semantics;
-    }
-
-    public void setSemantics(int semantics) {
-        this.semantics = semantics;
-    }
-
-    public boolean isJointPrefetch() {
-        return semantics == JOINT_PREFETCH_SEMANTICS;
-    }
-
-    public boolean isDisjointPrefetch() {
-        return semantics == DISJOINT_PREFETCH_SEMANTICS;
-    }
-
-    public boolean isDisjointByIdPrefetch() {
-        return semantics == DISJOINT_BY_ID_PREFETCH_SEMANTICS;
-    }
-
-    public String getEjbqlPathEntityId() {
-        return ejbqlPathEntityId;
-    }
-
-    public void setEjbqlPathEntityId(String ejbqlPathEntityId) {
-        this.ejbqlPathEntityId = ejbqlPathEntityId;
-    }
-
-    public String getEntityName() {
-        return entityName;
-    }
-
-    public void setEntityName(String entityName) {
-        this.entityName = entityName;
-    }
-
-
-    // **** custom serialization that supports serializing subtrees...
-
-    // implementing 'readResolve' instead of 'readObject' so that this would work with
-    // hessian
-    private Object readResolve() throws ObjectStreamException {
-
-        if (hasChildren()) {
-            for (PrefetchTreeNode child : children) {
-                child.parent = this;
-            }
-        }
-
-        return this;
-    }
-
-    // **** common tree operations
-
-    // An operation that encodes prefetch tree as XML.
-    class XMLEncoderOperation implements PrefetchProcessor {
-
-        XMLEncoder encoder;
-
-        XMLEncoderOperation(XMLEncoder encoder) {
-            this.encoder = encoder;
-        }
-
-        public boolean startPhantomPrefetch(PrefetchTreeNode node) {
-            // don't encode phantoms
-            return true;
-        }
-
-        public boolean startDisjointPrefetch(PrefetchTreeNode node) {
-            encoder.print("<prefetch type=\"disjoint\">");
-            encoder.print(node.getPath());
-            encoder.println("</prefetch>");
-            return true;
-        }
-
-        public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
-            encoder.print("<prefetch type=\"disjointById\">");
-            encoder.print(node.getPath());
-            encoder.println("</prefetch>");
-            return true;
-        }
-
-        public boolean startJointPrefetch(PrefetchTreeNode node) {
-            encoder.print("<prefetch type=\"joint\">");
-            encoder.print(node.getPath());
-            encoder.println("</prefetch>");
-            return true;
-        }
-
-        public boolean startUnknownPrefetch(PrefetchTreeNode node) {
-            encoder.print("<prefetch>");
-            encoder.print(node.getPath());
-            encoder.println("</prefetch>");
-
-            return true;
-        }
-
-        public void finishPrefetch(PrefetchTreeNode node) {
-            // noop
-        }
-    }
-
-    // An operation that collects all nodes in a single collection.
-    class CollectionBuilderOperation implements PrefetchProcessor {
-
-        Collection<PrefetchTreeNode> nodes;
-        boolean includePhantom;
-        boolean includeDisjoint;
-        boolean includeDisjointById;
-        boolean includeJoint;
-        boolean includeUnknown;
-
-        CollectionBuilderOperation(Collection<PrefetchTreeNode> nodes, boolean includeDisjoint,
-                boolean includeDisjointById, boolean includeJoint, boolean includeUnknown, boolean includePhantom) {
-            this.nodes = nodes;
-
-            this.includeDisjoint = includeDisjoint;
-            this.includeDisjointById = includeDisjointById;
-            this.includeJoint = includeJoint;
-            this.includeUnknown = includeUnknown;
-            this.includePhantom = includePhantom;
-        }
-
-        public boolean startPhantomPrefetch(PrefetchTreeNode node) {
-            if (includePhantom) {
-                nodes.add(node);
-            }
-
-            return true;
-        }
-
-        public boolean startDisjointPrefetch(PrefetchTreeNode node) {
-            if (includeDisjoint) {
-                nodes.add(node);
-            }
-            return true;
-        }
-
-        public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
-            if (includeDisjointById) {
-                nodes.add(node);
-            }
-            return true;
-        }
-
-        public boolean startJointPrefetch(PrefetchTreeNode node) {
-            if (includeJoint) {
-                nodes.add(node);
-            }
-            return true;
-        }
-
-        public boolean startUnknownPrefetch(PrefetchTreeNode node) {
-            if (includeUnknown) {
-                nodes.add(node);
-            }
-            return true;
-        }
-
-        public void finishPrefetch(PrefetchTreeNode node) {
-        }
-    }
-
-    class AdjacentJoinsOperation implements PrefetchProcessor {
-
-        Collection<PrefetchTreeNode> nodes;
-
-        AdjacentJoinsOperation(Collection<PrefetchTreeNode> nodes) {
-            this.nodes = nodes;
-        }
-
-        public boolean startPhantomPrefetch(PrefetchTreeNode node) {
-            return true;
-        }
-
-        public boolean startDisjointPrefetch(PrefetchTreeNode node) {
-            return node == PrefetchTreeNode.this;
-        }
-
-        public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
-            return startDisjointPrefetch(node);
-        }
-
-        public boolean startJointPrefetch(PrefetchTreeNode node) {
-            if (node != PrefetchTreeNode.this) {
-                nodes.add(node);
-            }
-            return true;
-        }
-
-        public boolean startUnknownPrefetch(PrefetchTreeNode node) {
-            return node == PrefetchTreeNode.this;
-        }
-
-        public void finishPrefetch(PrefetchTreeNode node) {
-        }
-    }
+	private static final long serialVersionUID = 1112629504025820837L;
+
+	public static final int UNDEFINED_SEMANTICS = 0;
+	public static final int JOINT_PREFETCH_SEMANTICS = 1;
+	public static final int DISJOINT_PREFETCH_SEMANTICS = 2;
+	public static final int DISJOINT_BY_ID_PREFETCH_SEMANTICS = 3;
+
+	protected String name;
+	protected boolean phantom;
+	protected int semantics;
+	protected String ejbqlPathEntityId;
+	protected String entityName;
+
+	// transient parent allows cloning parts of the tree via serialization
+	protected transient PrefetchTreeNode parent;
+
+	// Using Collection instead of Map for children storage (even though there
+	// cases of
+	// lookup by segment) is a reasonable tradeoff considering that
+	// each node has no more than a few children and lookup by name doesn't
+	// happen on
+	// traversal, only during creation.
+	protected Collection<PrefetchTreeNode> children;
+
+	/**
+	 * Creates a root node of the prefetch tree. Children can be added to the
+	 * parent by calling "addPath".
+	 */
+	public PrefetchTreeNode() {
+		this(null, null);
+	}
+
+	/**
+	 * Creates a phantom PrefetchTreeNode, initializing it with parent node and
+	 * a name of a relationship segment connecting this node with the parent.
+	 */
+	protected PrefetchTreeNode(PrefetchTreeNode parent, String name) {
+		this.parent = parent;
+		this.name = name;
+		this.phantom = true;
+		this.semantics = UNDEFINED_SEMANTICS;
+	}
+
+	public void encodeAsXML(XMLEncoder encoder) {
+		traverse(new XMLEncoderOperation(encoder));
+	}
+
+	/**
+	 * Returns the root of the node tree. Root is the topmost parent node that
+	 * itself has no parent set.
+	 */
+	public PrefetchTreeNode getRoot() {
+		return (parent != null) ? parent.getRoot() : this;
+	}
+
+	/**
+	 * Returns full prefetch path, that is a dot separated String of node names
+	 * starting from root and up to and including this node. Note that root
+	 * "name" is considered to be an empty string.
+	 */
+	public String getPath() {
+		return getPath(null);
+	}
+
+	public String getPath(PrefetchTreeNode upTillParent) {
+		if (parent == null || upTillParent == this) {
+			return "";
+		}
+
+		StringBuilder path = new StringBuilder(getName());
+		PrefetchTreeNode node = this.getParent();
+
+		// root node has no path
+		while (node.getParent() != null && node != upTillParent) {
+			path.insert(0, node.getName() + ".");
+			node = node.getParent();
+		}
+
+		return path.toString();
+	}
+
+	/**
+	 * Returns a subset of nodes with "joint" semantics that are to be
+	 * prefetched in the same query as the current node. Result excludes this
+	 * node, regardless of its semantics.
+	 */
+	public Collection<PrefetchTreeNode> adjacentJointNodes() {
+		Collection<PrefetchTreeNode> c = new ArrayList<PrefetchTreeNode>();
+		traverse(new AdjacentJoinsOperation(c));
+		return c;
+	}
+
+	/**
+	 * Returns a collection of PrefetchTreeNodes in this tree with joint
+	 * semantics.
+	 */
+	public Collection<PrefetchTreeNode> jointNodes() {
+		Collection<PrefetchTreeNode> c = new ArrayList<PrefetchTreeNode>();
+		traverse(new CollectionBuilderOperation(c, false, false, true, false, false));
+		return c;
+	}
+
+	/**
+	 * Returns a collection of PrefetchTreeNodes with disjoint semantics.
+	 */
+	public Collection<PrefetchTreeNode> disjointNodes() {
+		Collection<PrefetchTreeNode> c = new ArrayList<PrefetchTreeNode>();
+		traverse(new CollectionBuilderOperation(c, true, false, false, false, false));
+		return c;
+	}
+
+	/**
+	 * Returns a collection of PrefetchTreeNodes with disjoint semantics
+	 * 
+	 * @since 3.1
+	 */
+	public Collection<PrefetchTreeNode> disjointByIdNodes() {
+		Collection<PrefetchTreeNode> c = new ArrayList<PrefetchTreeNode>();
+		traverse(new CollectionBuilderOperation(c, false, true, false, false, false));
+		return c;
+	}
+
+	/**
+	 * Returns a collection of PrefetchTreeNodes that are not phantoms.
+	 */
+	public Collection<PrefetchTreeNode> nonPhantomNodes() {
+		Collection<PrefetchTreeNode> c = new ArrayList<PrefetchTreeNode>();
+		traverse(new CollectionBuilderOperation(c, true, true, true, true, false));
+		return c;
+	}
+
+	/**
+	 * Returns a clone of subtree that includes all joint children starting from
+	 * this node itself and till the first occurrence of non-joint node
+	 *
+	 * @since 3.1
+	 */
+	public PrefetchTreeNode cloneJointSubtree() {
+		return cloneJointSubtree(null);
+	}
+
+	private PrefetchTreeNode cloneJointSubtree(PrefetchTreeNode parent) {
+		PrefetchTreeNode cloned = new PrefetchTreeNode(parent, getName());
+		if (parent != null) {
+			cloned.setSemantics(getSemantics());
+			cloned.setPhantom(isPhantom());
+		}
+
+		if (children != null) {
+			for (PrefetchTreeNode child : children) {
+				if (child.isJointPrefetch()) {
+					cloned.addChild(child.cloneJointSubtree(cloned));
+				}
+			}
+		}
+
+		return cloned;
+	}
+
+	/**
+	 * Traverses the tree depth-first, invoking callback methods of the
+	 * processor when passing through the nodes.
+	 */
+	public void traverse(PrefetchProcessor processor) {
+
+		boolean result = false;
+
+		if (isPhantom()) {
+			result = processor.startPhantomPrefetch(this);
+		} else if (isDisjointPrefetch()) {
+			result = processor.startDisjointPrefetch(this);
+		} else if (isDisjointByIdPrefetch()) {
+			result = processor.startDisjointByIdPrefetch(this);
+		} else if (isJointPrefetch()) {
+			result = processor.startJointPrefetch(this);
+		} else {
+			result = processor.startUnknownPrefetch(this);
+		}
+
+		// process children unless processing is blocked...
+		if (result && children != null) {
+			for (PrefetchTreeNode child : children) {
+				child.traverse(processor);
+			}
+		}
+
+		// call finish regardless of whether children were processed
+		processor.finishPrefetch(this);
+	}
+
+	/**
+	 * Looks up an existing node in the tree desribed by the dot-separated path.
+	 * Will return null if no matching child exists.
+	 */
+	public PrefetchTreeNode getNode(String path) {
+		if (Util.isEmptyString(path)) {
+			throw new IllegalArgumentException("Empty path: " + path);
+		}
+
+		PrefetchTreeNode node = this;
+		StringTokenizer toks = new StringTokenizer(path, Entity.PATH_SEPARATOR);
+		while (toks.hasMoreTokens() && node != null) {
+			String segment = toks.nextToken();
+			node = node.getChild(segment);
+		}
+
+		return node;
+	}
+
+	/**
+	 * Adds a "path" with specified semantics to this prefetch node. All yet
+	 * non-existent nodes in the created path will be marked as phantom.
+	 *
+	 * @return the last segment in the created path.
+	 */
+	public PrefetchTreeNode addPath(String path) {
+		if (Util.isEmptyString(path)) {
+			throw new IllegalArgumentException("Empty path: " + path);
+		}
+
+		PrefetchTreeNode node = this;
+		StringTokenizer toks = new StringTokenizer(path, Entity.PATH_SEPARATOR);
+		while (toks.hasMoreTokens()) {
+			String segment = toks.nextToken();
+
+			PrefetchTreeNode child = node.getChild(segment);
+			if (child == null) {
+				child = new PrefetchTreeNode(node, segment);
+				node.addChild(child);
+			}
+
+			node = child;
+		}
+
+		return node;
+	}
+
+	/**
+	 * Merges {@link PrefetchTreeNode} into the current prefetch tree, cloning
+	 * the nodes added to this tree. Merged nodes semantics (if defined) and
+	 * non-phantom status are applied to the nodes of this tree.
+	 * 
+	 * @param node
+	 *            a root node of a tree to merge into this tree. The path of the
+	 *            merged node within the resulting tree is determined from its
+	 *            name.
+	 * 
+	 * @since 4.0
+	 */
+	public void merge(PrefetchTreeNode node) {
+		if (node == null) {
+			throw new NullPointerException("Null node");
+		}
+
+		PrefetchTreeNode start = node.getName() != null ? addPath(node.getName()) : this;
+		merge(start, node);
+	}
+
+	void merge(PrefetchTreeNode original, PrefetchTreeNode toMerge) {
+
+		if (toMerge.getSemantics() != UNDEFINED_SEMANTICS) {
+			original.setSemantics(toMerge.getSemantics());
+		}
+
+		if (!toMerge.isPhantom()) {
+			original.setPhantom(false);
+		}
+
+		for (PrefetchTreeNode childToMerge : toMerge.getChildren()) {
+
+			PrefetchTreeNode childOrigin = original.getChild(childToMerge.getName());
+			if (childOrigin == null) {
+				childOrigin = original.addPath(childToMerge.getName());
+			}
+
+			merge(childOrigin, childToMerge);
+		}
+	}
+
+	/**
+	 * Removes or makes phantom a node defined by this path. If the node for
+	 * this path doesn't have any children, it is removed, otherwise it is made
+	 * phantom.
+	 */
+	public void removePath(String path) {
+
+		PrefetchTreeNode node = getNode(path);
+		while (node != null) {
+
+			if (node.children != null) {
+				node.setPhantom(true);
+				break;
+			}
+
+			String segment = node.getName();
+
+			node = node.getParent();
+
+			if (node != null) {
+				node.removeChild(segment);
+			}
+		}
+	}
+
+	public void addChild(PrefetchTreeNode child) {
+
+		if (Util.isEmptyString(child.getName())) {
+			throw new IllegalArgumentException("Child has no segmentPath: " + child);
+		}
+
+		if (child.getParent() != this) {
+			child.getParent().removeChild(child.getName());
+			child.parent = this;
+		}
+
+		if (children == null) {
+			children = new ArrayList<PrefetchTreeNode>(4);
+		}
+
+		children.add(child);
+	}
+
+	public void removeChild(PrefetchTreeNode child) {
+		if (children != null && child != null) {
+			children.remove(child);
+			child.parent = null;
+		}
+	}
+
+	protected void removeChild(String segment) {
+		if (children != null) {
+			PrefetchTreeNode child = getChild(segment);
+			if (child != null) {
+				children.remove(child);
+				child.parent = null;
+			}
+		}
+	}
+
+	protected PrefetchTreeNode getChild(String segment) {
+		if (children != null) {
+			for (PrefetchTreeNode child : children) {
+				if (segment.equals(child.getName())) {
+					return child;
+				}
+			}
+		}
+
+		return null;
+	}
+
+	public PrefetchTreeNode getParent() {
+		return parent;
+	}
+
+	/**
+	 * Returns an unmodifiable collection of children.
+	 */
+	public Collection<PrefetchTreeNode> getChildren() {
+		return children == null ? Collections.<PrefetchTreeNode> emptySet() : children;
+	}
+
+	public boolean hasChildren() {
+		return children != null && !children.isEmpty();
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public boolean isPhantom() {
+		return phantom;
+	}
+
+	public void setPhantom(boolean phantom) {
+		this.phantom = phantom;
+	}
+
+	public int getSemantics() {
+		return semantics;
+	}
+
+	public void setSemantics(int semantics) {
+		this.semantics = semantics;
+	}
+
+	public boolean isJointPrefetch() {
+		return semantics == JOINT_PREFETCH_SEMANTICS;
+	}
+
+	public boolean isDisjointPrefetch() {
+		return semantics == DISJOINT_PREFETCH_SEMANTICS;
+	}
+
+	public boolean isDisjointByIdPrefetch() {
+		return semantics == DISJOINT_BY_ID_PREFETCH_SEMANTICS;
+	}
+
+	public String getEjbqlPathEntityId() {
+		return ejbqlPathEntityId;
+	}
+
+	public void setEjbqlPathEntityId(String ejbqlPathEntityId) {
+		this.ejbqlPathEntityId = ejbqlPathEntityId;
+	}
+
+	public String getEntityName() {
+		return entityName;
+	}
+
+	public void setEntityName(String entityName) {
+		this.entityName = entityName;
+	}
+
+	// **** custom serialization that supports serializing subtrees...
+
+	// implementing 'readResolve' instead of 'readObject' so that this would
+	// work with
+	// hessian
+	private Object readResolve() throws ObjectStreamException {
+
+		if (hasChildren()) {
+			for (PrefetchTreeNode child : children) {
+				child.parent = this;
+			}
+		}
+
+		return this;
+	}
+
+	// **** common tree operations
+
+	// An operation that encodes prefetch tree as XML.
+	class XMLEncoderOperation implements PrefetchProcessor {
+
+		XMLEncoder encoder;
+
+		XMLEncoderOperation(XMLEncoder encoder) {
+			this.encoder = encoder;
+		}
+
+		public boolean startPhantomPrefetch(PrefetchTreeNode node) {
+			// don't encode phantoms
+			return true;
+		}
+
+		public boolean startDisjointPrefetch(PrefetchTreeNode node) {
+			encoder.print("<prefetch type=\"disjoint\">");
+			encoder.print(node.getPath());
+			encoder.println("</prefetch>");
+			return true;
+		}
+
+		public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
+			encoder.print("<prefetch type=\"disjointById\">");
+			encoder.print(node.getPath());
+			encoder.println("</prefetch>");
+			return true;
+		}
+
+		public boolean startJointPrefetch(PrefetchTreeNode node) {
+			encoder.print("<prefetch type=\"joint\">");
+			encoder.print(node.getPath());
+			encoder.println("</prefetch>");
+			return true;
+		}
+
+		public boolean startUnknownPrefetch(PrefetchTreeNode node) {
+			encoder.print("<prefetch>");
+			encoder.print(node.getPath());
+			encoder.println("</prefetch>");
+
+			return true;
+		}
+
+		public void finishPrefetch(PrefetchTreeNode node) {
+			// noop
+		}
+	}
+
+	// An operation that collects all nodes in a single collection.
+	class CollectionBuilderOperation implements PrefetchProcessor {
+
+		Collection<PrefetchTreeNode> nodes;
+		boolean includePhantom;
+		boolean includeDisjoint;
+		boolean includeDisjointById;
+		boolean includeJoint;
+		boolean includeUnknown;
+
+		CollectionBuilderOperation(Collection<PrefetchTreeNode> nodes, boolean includeDisjoint,
+				boolean includeDisjointById, boolean includeJoint, boolean includeUnknown, boolean includePhantom) {
+			this.nodes = nodes;
+
+			this.includeDisjoint = includeDisjoint;
+			this.includeDisjointById = includeDisjointById;
+			this.includeJoint = includeJoint;
+			this.includeUnknown = includeUnknown;
+			this.includePhantom = includePhantom;
+		}
+
+		public boolean startPhantomPrefetch(PrefetchTreeNode node) {
+			if (includePhantom) {
+				nodes.add(node);
+			}
+
+			return true;
+		}
+
+		public boolean startDisjointPrefetch(PrefetchTreeNode node) {
+			if (includeDisjoint) {
+				nodes.add(node);
+			}
+			return true;
+		}
+
+		public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
+			if (includeDisjointById) {
+				nodes.add(node);
+			}
+			return true;
+		}
+
+		public boolean startJointPrefetch(PrefetchTreeNode node) {
+			if (includeJoint) {
+				nodes.add(node);
+			}
+			return true;
+		}
+
+		public boolean startUnknownPrefetch(PrefetchTreeNode node) {
+			if (includeUnknown) {
+				nodes.add(node);
+			}
+			return true;
+		}
+
+		public void finishPrefetch(PrefetchTreeNode node) {
+		}
+	}
+
+	class AdjacentJoinsOperation implements PrefetchProcessor {
+
+		Collection<PrefetchTreeNode> nodes;
+
+		AdjacentJoinsOperation(Collection<PrefetchTreeNode> nodes) {
+			this.nodes = nodes;
+		}
+
+		public boolean startPhantomPrefetch(PrefetchTreeNode node) {
+			return true;
+		}
+
+		public boolean startDisjointPrefetch(PrefetchTreeNode node) {
+			return node == PrefetchTreeNode.this;
+		}
+
+		public boolean startDisjointByIdPrefetch(PrefetchTreeNode node) {
+			return startDisjointPrefetch(node);
+		}
+
+		public boolean startJointPrefetch(PrefetchTreeNode node) {
+			if (node != PrefetchTreeNode.this) {
+				nodes.add(node);
+			}
+			return true;
+		}
+
+		public boolean startUnknownPrefetch(PrefetchTreeNode node) {
+			return node == PrefetchTreeNode.this;
+		}
+
+		public void finishPrefetch(PrefetchTreeNode node) {
+		}
+	}
 }

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4d8b2e1a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java
index 8046a8c..b7a4fdf 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/SelectQuery.java
@@ -588,11 +588,8 @@ public class SelectQuery<T> extends AbstractQuery implements ParameterizedQuery,
 	 * 
 	 * @since 4.0
 	 */
-	public PrefetchTreeNode addPrefetch(PrefetchTreeNode prefetchElement) {
-		String path = prefetchElement.getPath();
-		int semantics = prefetchElement.getSemantics();
-
-		return metaData.addPrefetch(path, semantics);
+	public void addPrefetch(PrefetchTreeNode prefetchElement) {
+		 metaData.mergePrefetch(prefetchElement);
 	}
 
 	/**

http://git-wip-us.apache.org/repos/asf/cayenne/blob/4d8b2e1a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextDisjointByIdPrefetch_ExtrasIT.java
----------------------------------------------------------------------
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextDisjointByIdPrefetch_ExtrasIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextDisjointByIdPrefetch_ExtrasIT.java
index 9b84973..daa9acd 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextDisjointByIdPrefetch_ExtrasIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextDisjointByIdPrefetch_ExtrasIT.java
@@ -347,6 +347,7 @@ public class DataContextDisjointByIdPrefetch_ExtrasIT extends ServerCase {
 
         queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() {
 
+        	@Override
             public void execute() {
                 assertFalse(result.isEmpty());