/*
All programs in this directory and subdirectories are published under the
GNU General Public License as described below.
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc., 59
Temple Place, Suite 330, Boston, MA 02111-1307 USA
Further information about the GNU GPL is available at:
http://www.gnu.org/copyleft/gpl.ja.html
*/
package net.sf.jabref.groups;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Vector;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;
import javax.swing.undo.AbstractUndoableEdit;
import net.sf.jabref.BibtexDatabase;
import net.sf.jabref.BibtexEntry;
import net.sf.jabref.SearchRule;
/**
* A node in the groups tree that holds exactly one AbstractGroup.
*
* @author jzieren
*/
public class GroupTreeNode extends DefaultMutableTreeNode implements
Transferable {
public static final DataFlavor flavor;
public static final DataFlavor[] flavors;
static {
DataFlavor df = null;
try {
df = new DataFlavor(DataFlavor.javaJVMLocalObjectMimeType
+ ";class=net.sf.jabref.groups.GroupTreeNode");
} catch (ClassNotFoundException e) {
// never happens
}
flavor = df;
flavors = new DataFlavor[] { flavor };
}
/**
* Creates this node and associates the specified group with it.
*/
public GroupTreeNode(AbstractGroup group) {
setGroup(group);
}
/**
* @return The group associated with this node.
*/
public AbstractGroup getGroup() {
return (AbstractGroup) getUserObject();
}
/**
* Associates the specified group with this node.
*/
public void setGroup(AbstractGroup group) {
setUserObject(group);
}
/**
* Returns a textual representation of this node and its children. This
* representation contains both the tree structure and the textual
* representations of the group associated with each node. It thus allows a
* complete reconstruction of this object and its children.
*/
public String getTreeAsString() {
StringBuffer sb = new StringBuffer();
Enumeration<GroupTreeNode> e = preorderEnumeration();
GroupTreeNode cursor;
while (e.hasMoreElements()) {
cursor = e.nextElement();
sb.append(cursor.getLevel()).append(" ").append(cursor.getGroup().toString()).append("\n");
}
return sb.toString();
}
/**
* Creates a deep copy of this node and all of its children, including all
* groups.
*
* @return This object's deep copy.
*/
public GroupTreeNode deepCopy() {
GroupTreeNode copy = new GroupTreeNode(getGroup());
for (int i = 0; i < getChildCount(); ++i)
copy.add(((GroupTreeNode) getChildAt(i)).deepCopy());
return copy;
}
/**
* Update all groups, if necessary, to handle the situation where the group
* tree is applied to a different BibtexDatabase than it was created for. This
* is for instance used when updating the group tree due to an external change.
*
* @param db The database to refresh for.
*/
public void refreshGroupsForNewDatabase(BibtexDatabase db) {
for (int i = 0; i < getChildCount(); ++i) {
GroupTreeNode node = (GroupTreeNode)getChildAt(i);
node.getGroup().refreshForNewDatabase(db);
node.refreshGroupsForNewDatabase(db);
}
}
/**
* @return An indexed path from the root node to this node. The elements in
* the returned array represent the child index of each node in the
* path. If this node is the root node, the returned array has zero
* elements.
*/
public int[] getIndexedPath() {
TreeNode[] path = getPath();
int[] indexedPath = new int[path.length - 1];
for (int i = 1; i < path.length; ++i)
indexedPath[i - 1] = path[i - 1].getIndex(path[i]);
return indexedPath;
}
/**
* Returns the node indicated by the specified indexedPath, which contains
* child indices obtained e.g. by getIndexedPath().
*/
public GroupTreeNode getNode(int[] indexedPath) {
GroupTreeNode cursor = this;
for (int i = 0; i < indexedPath.length; ++i)
cursor = (GroupTreeNode) cursor.getChildAt(indexedPath[i]);
return cursor;
}
/**
* @param indexedPath
* A sequence of child indices that describe a path from this
* node to one of its desendants. Be aware that if <b>indexedPath
* </b> was obtained by getIndexedPath(), this node should
* usually be the root node.
* @return The descendant found by evaluating <b>indexedPath </b>. If the
* path could not be traversed completely (i.e. one of the child
* indices did not exist), null will be returned.
*/
public GroupTreeNode getDescendant(int[] indexedPath) {
GroupTreeNode cursor = this;
for (int i = 0; i < indexedPath.length && cursor != null; ++i)
cursor = (GroupTreeNode) cursor.getChildAt(indexedPath[i]);
return cursor;
}
/**
* A GroupTreeNode can create a SearchRule that finds elements contained in
* its own group, or the union of those elements in its own group and its
* children's groups (recursively), or the intersection of the elements in
* its own group and its parent's group. This setting is configured in the
* group contained in this node.
*
* @return A SearchRule that finds the desired elements.
*/
public SearchRule getSearchRule() {
return getSearchRule(getGroup().getHierarchicalContext());
}
protected SearchRule getSearchRule(int originalContext) {
final int context = getGroup().getHierarchicalContext();
if (context == AbstractGroup.INDEPENDENT)
return getGroup().getSearchRule();
AndOrSearchRuleSet searchRule = new AndOrSearchRuleSet(
context == AbstractGroup.REFINING, false);
searchRule.addRule(getGroup().getSearchRule());
if (context == AbstractGroup.INCLUDING
&& originalContext != AbstractGroup.REFINING) {
for (int i = 0; i < getChildCount(); ++i)
searchRule.addRule(((GroupTreeNode) getChildAt(i))
.getSearchRule(originalContext));
} else if (context == AbstractGroup.REFINING && !isRoot()
&& originalContext != AbstractGroup.INCLUDING) {
searchRule.addRule(((GroupTreeNode) getParent())
.getSearchRule(originalContext));
}
return searchRule;
}
@Override
@SuppressWarnings("unchecked")
public Enumeration<GroupTreeNode> preorderEnumeration(){
return super.preorderEnumeration();
}
@Override
@SuppressWarnings("unchecked")
public Enumeration<GroupTreeNode> depthFirstEnumeration(){
return super.depthFirstEnumeration();
}
@Override
@SuppressWarnings("unchecked")
public Enumeration<GroupTreeNode> breadthFirstEnumeration(){
return super.breadthFirstEnumeration();
}
@Override
@SuppressWarnings("unchecked")
public Enumeration<GroupTreeNode> children(){
return super.children();
}
/**
* Scans the subtree rooted at this node.
*
* @return All groups that contain the specified entry.
*/
public AbstractGroup[] getMatchingGroups(BibtexEntry entry) {
Vector<AbstractGroup> matchingGroups = new Vector<AbstractGroup>();
Enumeration<GroupTreeNode> e = preorderEnumeration();
AbstractGroup group;
while (e.hasMoreElements()) {
group = (e.nextElement()).getGroup();
if (group.contains(null, entry)) // first argument is never used
matchingGroups.add(group);
}
AbstractGroup[] matchingGroupsArray = new AbstractGroup[matchingGroups
.size()];
return matchingGroups.toArray(matchingGroupsArray);
}
public boolean canMoveUp() {
return getPreviousSibling() != null
&& !(getGroup() instanceof AllEntriesGroup);
}
public boolean canMoveDown() {
return getNextSibling() != null
&& !(getGroup() instanceof AllEntriesGroup);
}
public boolean canMoveLeft() {
return !(getGroup() instanceof AllEntriesGroup)
&& !(((GroupTreeNode) getParent()).getGroup() instanceof AllEntriesGroup);
}
public boolean canMoveRight() {
return getPreviousSibling() != null
&& !(getGroup() instanceof AllEntriesGroup);
}
public AbstractUndoableEdit moveUp(GroupSelector groupSelector) {
final GroupTreeNode myParent = (GroupTreeNode) getParent();
final int index = myParent.getIndex(this);
if (index > 0) {
UndoableMoveGroup undo = new UndoableMoveGroup(groupSelector,
groupSelector.getGroupTreeRoot(), this, myParent, index - 1);
myParent.insert(this, index - 1);
return undo;
}
return null;
}
public AbstractUndoableEdit moveDown(GroupSelector groupSelector) {
final GroupTreeNode myParent = (GroupTreeNode) getParent();
final int index = myParent.getIndex(this);
if (index < parent.getChildCount() - 1) {
UndoableMoveGroup undo = new UndoableMoveGroup(groupSelector,
groupSelector.getGroupTreeRoot(), this, myParent, index + 1);
myParent.insert(this, index + 1);
return undo;
}
return null;
}
public AbstractUndoableEdit moveLeft(GroupSelector groupSelector) {
final GroupTreeNode myParent = (GroupTreeNode) getParent();
final GroupTreeNode myGrandParent = (GroupTreeNode) myParent
.getParent();
// paranoia
if (myGrandParent == null)
return null;
final int index = myGrandParent.getIndex(myParent);
UndoableMoveGroup undo = new UndoableMoveGroup(groupSelector,
groupSelector.getGroupTreeRoot(), this, myGrandParent,
index + 1);
myGrandParent.insert(this, index + 1);
return undo;
}
public AbstractUndoableEdit moveRight(GroupSelector groupSelector) {
final GroupTreeNode myPreviousSibling = (GroupTreeNode) getPreviousSibling();
// paranoia
if (myPreviousSibling == null)
return null;
UndoableMoveGroup undo = new UndoableMoveGroup(groupSelector,
groupSelector.getGroupTreeRoot(), this, myPreviousSibling,
myPreviousSibling.getChildCount());
myPreviousSibling.add(this);
return undo;
}
/**
* @param path
* A sequence of child indices that designate a node relative to
* this node.
* @return The node designated by the specified path, or null if one or more
* indices in the path could not be resolved.
*/
public GroupTreeNode getChildAt(int[] path) {
GroupTreeNode cursor = this;
for (int i = 0; i < path.length && cursor != null; ++i)
cursor = (GroupTreeNode) cursor.getChildAt(path[i]);
return cursor;
}
/** Adds the selected entries to this node's group. */
public AbstractUndoableEdit addToGroup(BibtexEntry[] entries) {
if (getGroup() == null)
return null; // paranoia
AbstractUndoableEdit undo = getGroup().add(entries);
if (undo instanceof UndoableChangeAssignment)
((UndoableChangeAssignment) undo).setEditedNode(this);
return undo;
}
/** Removes the selected entries from this node's group. */
public AbstractUndoableEdit removeFromGroup(BibtexEntry[] entries) {
if (getGroup() == null)
return null; // paranoia
AbstractUndoableEdit undo = getGroup().remove(entries);
if (undo instanceof UndoableChangeAssignment)
((UndoableChangeAssignment) undo).setEditedNode(this);
return undo;
}
public DataFlavor[] getTransferDataFlavors() {
return flavors;
}
public boolean isDataFlavorSupported(DataFlavor someFlavor) {
return someFlavor.equals(GroupTreeNode.flavor);
}
public Object getTransferData(DataFlavor someFlavor)
throws UnsupportedFlavorException, IOException {
if (!isDataFlavorSupported(someFlavor))
throw new UnsupportedFlavorException(someFlavor);
return this;
}
/**
* Recursively compares this node's group and all subgroups.
*/
public boolean equals(Object other) {
if (!(other instanceof GroupTreeNode))
return false;
final GroupTreeNode otherNode = (GroupTreeNode) other;
if (getChildCount() != otherNode.getChildCount())
return false;
AbstractGroup g1 = getGroup();
AbstractGroup g2 = otherNode.getGroup();
if ((g1 == null && g2 != null) || (g1 != null && g2 == null))
return false;
if (g1 != null && g2 != null && !g1.equals(g2))
return false;
for (int i = 0; i < getChildCount(); ++i) {
if (!getChildAt(i).equals(otherNode.getChildAt(i)))
return false;
}
return true;
}
@Override
public int hashCode() {
return getGroup().getName().hashCode();
}
}