k-9/k9mail/src/main/java/com/fsck/k9/search/ConditionsTreeNode.java

434 lines
13 KiB
Java
Raw Normal View History

package com.fsck.k9.search;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Stack;
2014-02-15 17:48:35 -05:00
import java.util.Set;
import android.database.Cursor;
import android.os.Parcel;
import android.os.Parcelable;
2012-10-17 14:52:03 -04:00
import com.fsck.k9.search.SearchSpecification.Attribute;
import com.fsck.k9.search.SearchSpecification.SearchCondition;
2015-03-06 15:17:49 -05:00
import com.fsck.k9.search.SearchSpecification.SearchField;
/**
2012-10-16 16:42:51 -04:00
* This class stores search conditions. It's basically a boolean expression binary tree.
* The output will be SQL queries ( obtained by traversing inorder ).
2012-10-16 16:42:51 -04:00
*
* TODO removing conditions from the tree
* TODO implement NOT as a node again
*/
2012-10-17 14:52:03 -04:00
public class ConditionsTreeNode implements Parcelable {
2012-10-16 16:42:51 -04:00
2012-10-17 14:52:03 -04:00
public enum Operator {
2012-10-16 16:42:51 -04:00
AND, OR, CONDITION;
}
public ConditionsTreeNode mLeft;
public ConditionsTreeNode mRight;
public ConditionsTreeNode mParent;
/*
* If mValue isn't CONDITION then mCondition contains a real
* condition, otherwise it's null.
*/
2012-10-17 14:52:03 -04:00
public Operator mValue;
2012-10-16 16:42:51 -04:00
public SearchCondition mCondition;
/*
* Used for storing and retrieving the tree to/from the database.
* The algorithm is called "modified preorder tree traversal".
*/
public int mLeftMPTTMarker;
public int mRightMPTTMarker;
///////////////////////////////////////////////////////////////
// Static Helpers to restore a tree from a database cursor
///////////////////////////////////////////////////////////////
/**
* Builds a condition tree starting from a database cursor. The cursor
* should point to rows representing the nodes of the tree.
*
* @param cursor Cursor pointing to the first of a bunch or rows. Each rows
2014-09-14 05:18:57 -04:00
* should contains 1 tree node.
2012-10-16 16:42:51 -04:00
* @return A condition tree.
*/
public static ConditionsTreeNode buildTreeFromDB(Cursor cursor) {
Stack<ConditionsTreeNode> stack = new Stack<ConditionsTreeNode>();
ConditionsTreeNode tmp = null;
// root node
if (cursor.moveToFirst()) {
tmp = buildNodeFromRow(cursor);
stack.push(tmp);
}
// other nodes
while (cursor.moveToNext()) {
2012-10-16 16:42:51 -04:00
tmp = buildNodeFromRow(cursor);
2012-10-17 14:52:03 -04:00
if (tmp.mRightMPTTMarker < stack.peek().mRightMPTTMarker) {
2012-10-16 16:42:51 -04:00
stack.peek().mLeft = tmp;
stack.push(tmp);
} else {
while (stack.peek().mRightMPTTMarker < tmp.mRightMPTTMarker) {
stack.pop();
}
stack.peek().mRight = tmp;
}
}
return tmp;
2012-10-16 16:42:51 -04:00
}
/**
* Converts a single database row to a single condition node.
*
* @param cursor Cursor pointing to the row we want to convert.
* @return A single ConditionsTreeNode
*/
private static ConditionsTreeNode buildNodeFromRow(Cursor cursor) {
2012-10-16 16:42:51 -04:00
ConditionsTreeNode result = null;
SearchCondition condition = null;
2012-10-17 14:52:03 -04:00
Operator tmpValue = ConditionsTreeNode.Operator.valueOf(cursor.getString(5));
2012-10-16 16:42:51 -04:00
2012-10-17 14:52:03 -04:00
if (tmpValue == Operator.CONDITION) {
2015-03-06 15:17:49 -05:00
condition = new SearchCondition(SearchField.valueOf(cursor.getString(0)),
2012-10-17 14:52:03 -04:00
Attribute.valueOf(cursor.getString(2)), cursor.getString(1));
2012-10-16 16:42:51 -04:00
}
result = new ConditionsTreeNode(condition);
result.mValue = tmpValue;
result.mLeftMPTTMarker = cursor.getInt(3);
result.mRightMPTTMarker = cursor.getInt(4);
return result;
}
///////////////////////////////////////////////////////////////
// Constructors
///////////////////////////////////////////////////////////////
public ConditionsTreeNode(SearchCondition condition) {
mParent = null;
mCondition = condition;
2012-10-17 14:52:03 -04:00
mValue = Operator.CONDITION;
2012-10-16 16:42:51 -04:00
}
2012-10-17 14:52:03 -04:00
public ConditionsTreeNode(ConditionsTreeNode parent, Operator op) {
2012-10-16 16:42:51 -04:00
mParent = parent;
mValue = op;
mCondition = null;
}
/* package */ ConditionsTreeNode cloneTree() {
if (mParent != null) {
throw new IllegalStateException("Can't call cloneTree() for a non-root node");
}
ConditionsTreeNode copy = new ConditionsTreeNode(mCondition.clone());
copy.mLeftMPTTMarker = mLeftMPTTMarker;
copy.mRightMPTTMarker = mRightMPTTMarker;
copy.mLeft = (mLeft == null) ? null : mLeft.cloneNode(copy);
copy.mRight = (mRight == null) ? null : mRight.cloneNode(copy);
return copy;
}
private ConditionsTreeNode cloneNode(ConditionsTreeNode parent) {
ConditionsTreeNode copy = new ConditionsTreeNode(parent, mValue);
copy.mCondition = mCondition.clone();
copy.mLeftMPTTMarker = mLeftMPTTMarker;
copy.mRightMPTTMarker = mRightMPTTMarker;
copy.mLeft = (mLeft == null) ? null : mLeft.cloneNode(copy);
copy.mRight = (mRight == null) ? null : mRight.cloneNode(copy);
return copy;
}
2012-10-16 16:42:51 -04:00
///////////////////////////////////////////////////////////////
// Public modifiers
///////////////////////////////////////////////////////////////
/**
* Adds the expression as the second argument of an AND
* clause to this node.
*
* @param expr Expression to 'AND' with.
* @return New top AND node.
* @throws Exception
*/
public ConditionsTreeNode and(ConditionsTreeNode expr) throws Exception {
2012-10-17 14:52:03 -04:00
return add(expr, Operator.AND);
2012-10-16 16:42:51 -04:00
}
/**
* Convenience method.
* Adds the provided condition as the second argument of an AND
* clause to this node.
*
* @param condition Condition to 'AND' with.
* @return New top AND node, new root.
*/
public ConditionsTreeNode and(SearchCondition condition) {
try {
ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
return and(tmp);
} catch (Exception e) {
// impossible
return null;
}
}
/**
* Adds the expression as the second argument of an OR
* clause to this node.
*
* @param expr Expression to 'OR' with.
* @return New top OR node.
* @throws Exception
*/
public ConditionsTreeNode or(ConditionsTreeNode expr) throws Exception {
2012-10-17 14:52:03 -04:00
return add(expr, Operator.OR);
2012-10-16 16:42:51 -04:00
}
/**
* Convenience method.
* Adds the provided condition as the second argument of an OR
* clause to this node.
*
* @param condition Condition to 'OR' with.
* @return New top OR node, new root.
*/
public ConditionsTreeNode or(SearchCondition condition) {
try {
ConditionsTreeNode tmp = new ConditionsTreeNode(condition);
return or(tmp);
} catch (Exception e) {
// impossible
return null;
}
}
/**
* This applies the MPTT labeling to the subtree of which this node
* is the root node.
*
* For a description on MPTT see:
* http://www.sitepoint.com/hierarchical-data-database-2/
*/
public void applyMPTTLabel() {
applyMPTTLabel(1);
}
///////////////////////////////////////////////////////////////
// Public accessors
///////////////////////////////////////////////////////////////
/**
* Returns the condition stored in this node.
* @return Condition stored in the node.
*/
public SearchCondition getCondition() {
return mCondition;
}
/**
* Get a set of all the leaves in the tree.
* @return Set of all the leaves.
*/
2014-02-15 17:48:35 -05:00
public Set<ConditionsTreeNode> getLeafSet() {
Set<ConditionsTreeNode> leafSet = new HashSet<ConditionsTreeNode>();
2012-10-16 16:42:51 -04:00
return getLeafSet(leafSet);
}
/**
* Returns a list of all the nodes in the subtree of which this node
* is the root. The list contains the nodes in a pre traversal order.
*
* @return List of all nodes in subtree in preorder.
*/
public List<ConditionsTreeNode> preorder() {
2014-10-04 06:45:45 -04:00
List<ConditionsTreeNode> result = new ArrayList<ConditionsTreeNode>();
2012-10-16 16:42:51 -04:00
Stack<ConditionsTreeNode> stack = new Stack<ConditionsTreeNode>();
stack.push(this);
2012-10-17 14:52:03 -04:00
while (!stack.isEmpty()) {
ConditionsTreeNode current = stack.pop();
if (current.mLeft != null) {
stack.push(current.mLeft);
}
if (current.mRight != null) {
stack.push(current.mRight);
}
2012-10-16 16:42:51 -04:00
result.add(current);
}
return result;
}
///////////////////////////////////////////////////////////////
// Private class logic
///////////////////////////////////////////////////////////////
/**
* Adds two new ConditionTreeNodes, one for the operator and one for the
* new condition. The current node will end up on the same level as the
* one provided in the arguments, they will be siblings. Their common
* parent node will be one containing the operator provided in the arguments.
* The method will update all the required references so the tree ends up in
* a valid state.
*
* This method only supports node arguments with a null parent node.
*
* @param Node to add.
* @param Operator that will connect the new node with this one.
* @return New parent node, containing the operator.
* @throws Exception Throws when the provided new node does not have a null parent.
*/
2012-10-17 14:52:03 -04:00
private ConditionsTreeNode add(ConditionsTreeNode node, Operator op) throws Exception {
2012-10-16 16:42:51 -04:00
if (node.mParent != null) {
throw new Exception("Can only add new expressions from root node down.");
}
ConditionsTreeNode tmpNode = new ConditionsTreeNode(mParent, op);
tmpNode.mLeft = this;
tmpNode.mRight = node;
if (mParent != null) {
mParent.updateChild(this, tmpNode);
}
2012-10-17 14:52:03 -04:00
this.mParent = tmpNode;
2012-10-16 16:42:51 -04:00
node.mParent = tmpNode;
return tmpNode;
}
/**
* Helper method that replaces a child of the current node with a new node.
* If the provided old child node was the left one, left will be replaced with
* the new one. Same goes for the right one.
*
* @param oldChild Old child node to be replaced.
* @param newChild New child node.
*/
private void updateChild(ConditionsTreeNode oldChild, ConditionsTreeNode newChild) {
// we can compare objects id's because this is the desired behaviour in this case
if (mLeft == oldChild) {
mLeft = newChild;
} else if (mRight == oldChild) {
mRight = newChild;
}
}
/**
* Recursive function to gather all the leaves in the subtree of which
* this node is the root.
*
* @param leafSet Leafset that's being built.
* @return Set of leaves being completed.
*/
2014-02-15 17:48:35 -05:00
private Set<ConditionsTreeNode> getLeafSet(Set<ConditionsTreeNode> leafSet) {
2012-10-16 16:42:51 -04:00
if (mLeft == null && mRight == null) {
2012-10-17 14:52:03 -04:00
// if we ended up in a leaf, add ourself and return
2012-10-16 16:42:51 -04:00
leafSet.add(this);
return leafSet;
2012-10-17 14:52:03 -04:00
}
2012-10-16 16:42:51 -04:00
// we didn't end up in a leaf
2012-10-17 14:52:03 -04:00
if (mLeft != null) {
mLeft.getLeafSet(leafSet);
}
2012-10-16 16:42:51 -04:00
2012-10-17 14:52:03 -04:00
if (mRight != null) {
mRight.getLeafSet(leafSet);
2012-10-16 16:42:51 -04:00
}
2012-10-17 14:52:03 -04:00
return leafSet;
2012-10-16 16:42:51 -04:00
}
/**
* This applies the MPTT labeling to the subtree of which this node
* is the root node.
*
* For a description on MPTT see:
* http://www.sitepoint.com/hierarchical-data-database-2/
*/
private int applyMPTTLabel(int label) {
mLeftMPTTMarker = label;
2012-10-17 14:52:03 -04:00
if (mLeft != null) {
2012-10-16 16:42:51 -04:00
label = mLeft.applyMPTTLabel(label += 1);
}
2012-10-17 14:52:03 -04:00
if (mRight != null) {
2012-10-16 16:42:51 -04:00
label = mRight.applyMPTTLabel(label += 1);
}
2012-10-17 14:52:03 -04:00
2012-10-16 16:42:51 -04:00
++label;
mRightMPTTMarker = label;
return label;
}
///////////////////////////////////////////////////////////////
// Parcelable
//
// This whole class has to be parcelable because it's passed
// on through intents.
///////////////////////////////////////////////////////////////
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mValue.ordinal());
dest.writeParcelable(mCondition, flags);
dest.writeParcelable(mLeft, flags);
dest.writeParcelable(mRight, flags);
}
2012-10-17 14:52:03 -04:00
public static final Parcelable.Creator<ConditionsTreeNode> CREATOR =
new Parcelable.Creator<ConditionsTreeNode>() {
@Override
2012-10-16 16:42:51 -04:00
public ConditionsTreeNode createFromParcel(Parcel in) {
return new ConditionsTreeNode(in);
}
2012-10-17 14:52:03 -04:00
@Override
2012-10-16 16:42:51 -04:00
public ConditionsTreeNode[] newArray(int size) {
return new ConditionsTreeNode[size];
}
};
private ConditionsTreeNode(Parcel in) {
2012-10-17 14:52:03 -04:00
mValue = Operator.values()[in.readInt()];
2012-10-16 16:42:51 -04:00
mCondition = in.readParcelable(ConditionsTreeNode.class.getClassLoader());
mLeft = in.readParcelable(ConditionsTreeNode.class.getClassLoader());
mRight = in.readParcelable(ConditionsTreeNode.class.getClassLoader());
mParent = null;
2012-10-17 14:52:03 -04:00
2012-10-16 16:42:51 -04:00
if (mLeft != null) {
mLeft.mParent = this;
}
2012-10-17 14:52:03 -04:00
2012-10-16 16:42:51 -04:00
if (mRight != null) {
mRight.mParent = this;
}
}
}