/*****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
****************************************************************/
package org.apache.cayenne.query;
import org.apache.cayenne.map.Entity;
import org.apache.cayenne.util.Util;
import org.apache.cayenne.util.XMLEncoder;
import org.apache.cayenne.util.XMLSerializable;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.StringTokenizer;
/**
* Defines a node in a prefetch tree.
*
* @since 1.2
*/
public class PrefetchTreeNode implements Serializable, XMLSerializable {
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 and returns a prefetch tree spanning a single path. The tree is
* made of phantom nodes, up to the leaf node, which is non-phantom and has
* specified semantics.
*
* @since 4.0
*/
public static PrefetchTreeNode withPath(String path, int semantics) {
PrefetchTreeNode root = new PrefetchTreeNode();
PrefetchTreeNode node = root.addPath(path);
node.setPhantom(false);
node.setSemantics(semantics);
return root;
}
/**
* 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<>();
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<>();
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<>();
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<>();
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<>();
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<>(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
protected 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) {
}
}
}