/*
* Geotoolkit.org - An Open Source Java GIS Toolkit
* http://www.geotoolkit.org
*
* (C) 2001-2012, Open Source Geospatial Foundation (OSGeo)
* (C) 2009-2012, Geomatys
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library 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
* Lesser General Public License for more details.
*/
package org.geotoolkit.gui.swing.tree;
import java.io.PrintWriter;
import java.util.Map;
import java.util.List;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.lang.reflect.Array;
import javax.swing.JTree;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import javax.swing.tree.TreeModel;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.geotoolkit.lang.Debug;
import org.geotoolkit.lang.Static;
import org.apache.sis.util.ArraysExt;
import org.apache.sis.util.Classes;
import org.geotoolkit.nio.IOUtilities;
/**
* Convenience static methods for trees operations. This class provides methods for performing
* a {@linkplain #copy copy} of a tree and for {@linkplain #xmlToSwing converting from XML}.
* It can also {@linkplain #print print} a tree in a format like the following example:
*
* {@preformat text
* Node #1
* ├───Node #2
* │ └───Node #4
* └───Node #3
* }
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.18
*
* @since 2.0
* @module
*
* @deprecated The {@linkplain org.apache.sis.util.collection.TreeTable tree model in Apache SIS}
* is no longer based on Swing tree interfaces. Swing dependencies will be phased out
* since Swing itself is likely to be replaced by JavaFX in future JDK versions.
*/
@Deprecated
public final class Trees extends Static {
/**
* Do not allows instantiation of this class.
*/
private Trees() {
}
/**
* Returns the user object from the given tree node. If the given node is an
* instance of Geotk {@link org.geotoolkit.gui.swing.tree.TreeNode}, then its
* {@link org.geotoolkit.gui.swing.tree.TreeNode#getUserObject() getUserObject()}
* method is invoked. Otherwise if the given node is an instance of Java
* {@link javax.swing.tree.DefaultMutableTreeNode}, then its {@link
* javax.swing.tree.DefaultMutableTreeNode#getUserObject() getUserObject()}
* method is invoked. Otherwise this method returns {@code null}.
*
* @param node The node for which to get the user object, or {@code null}.
* @return The user object, or {@code null} if none.
*
* @since 3.00
*/
public static Object getUserObject(final TreeNode node) {
if (node instanceof org.geotoolkit.gui.swing.tree.TreeNode) {
return ((org.geotoolkit.gui.swing.tree.TreeNode) node).getUserObject();
}
if (node instanceof javax.swing.tree.DefaultMutableTreeNode) {
return ((javax.swing.tree.DefaultMutableTreeNode) node).getUserObject();
}
return null;
}
/**
* Returns the path to the specified
* {@linkplain org.geotoolkit.gui.swing.tree.TreeNode#getUserObject user object}. For each tree
* node which are actually instance of Geotk {@link org.geotoolkit.gui.swing.tree.TreeNode},
* this method compares the specified {@code value} against the user object returned by the
* {@link org.geotoolkit.gui.swing.tree.TreeNode#getUserObject} method.
*
* @param model The tree model to inspect.
* @param value User object to compare to {@link org.geotoolkit.gui.swing.tree.TreeNode#getUserObject}.
* @return The paths to the specified value, or an empty array if none.
*/
public static TreePath[] getPathsToUserObject(final TreeModel model, final Object value) {
final List<TreePath> paths = new ArrayList<>(8);
final Object[] path = new Object[8];
path[0] = model.getRoot();
getPathsToUserObject(model, value, path, 1, paths);
return paths.toArray(new TreePath[paths.size()]);
}
/**
* Implementation of the path search. This method invokes itself recursively.
*
* @param model The tree model in which to search for a path.
* @param value The expected {@link org.geotoolkit.gui.swing.tree.TreeNode#getUserObject()}.
* @param path The path found up to date.
* @param length The number of valid elements in the {@code path} array.
* @param list Where to add new {@link TreePath} as they are found.
* @return {@code path}, or a new array if it was necessary to increase its size.
*/
private static Object[] getPathsToUserObject(final TreeModel model, final Object value,
Object[] path, final int length, final List<TreePath> list)
{
final Object parent = path[length-1];
if (parent instanceof org.geotoolkit.gui.swing.tree.TreeNode) {
final Object nodeValue = ((org.geotoolkit.gui.swing.tree.TreeNode) parent).getUserObject();
if (nodeValue == value || (value!=null && value.equals(nodeValue))) {
list.add(new TreePath(ArraysExt.resize(path, length)));
}
}
final int count = model.getChildCount(parent);
for (int i=0; i<count; i++) {
if (length >= path.length) {
path = Arrays.copyOf(path, length << 1);
}
path[length] = model.getChild(parent, i);
path = getPathsToUserObject(model, value, path, length+1, list);
}
return path;
}
/**
* Returns a tree representation of the given object.
* This method processes the following types especially:
* <p>
* <ul>
* <li>If the given object is an instance of {@link Node}, then this method delegates
* to {@link #xmlToSwing(Node)}.</li>
* <li>If the given object is an instance of {@link Iterable}, {@link Map} or an array,
* then a node is created with the name of the implemented interface (for example
* {@code "List"} or {@code "Set"}) and each iterated elements is added as a child
* of the above-cited node.</li>
* <li>If the given object is an instance of {@link java.util.Map.Entry}, then this
* method returns a {@link NamedTreeNode} which contain the entry key as the
* {@linkplain NamedTreeNode#getName() name} and the entry value as
* {@linkplain TreeNode#getUserObject() user object}.</li>
* <li>Otherwise this method returns a single {@link DefaultMutableTreeNode} which contain
* the given object as {@linkplain TreeNode#getUserObject() user object}.</li>
* </ul>
* <p>
* Together with {@link #toString(TreeNode)}, this method provides a convenient way to print
* the content of a XML document for debugging purpose.
*
* @param object The array, collection or single object to format.
* @return The given object as a Swing tree.
*
* @since 3.17
*/
public static MutableTreeNode objectToSwing(final Object object) {
final DefaultMutableTreeNode node;
Iterator<?> iterator = null;
Class<?> baseInterface = null;
if (object instanceof Iterable<?>) {
baseInterface = Iterable.class;
iterator = ((Iterable<?>) object).iterator();
} else if (object instanceof Map<?,?>) {
baseInterface = Map.class;
iterator = ((Map<?,?>) object).entrySet().iterator();
}
if (iterator != null) {
final Class<?>[] types = Classes.getLeafInterfaces(object.getClass(), baseInterface);
node = new DefaultMutableTreeNode(Classes.getShortName(types.length != 0 ? types[0] : null));
while (iterator.hasNext()) {
node.add(objectToSwing(iterator.next()));
}
} else {
if (object.getClass().isArray()) {
node = new DefaultMutableTreeNode("Array");
final int length = Array.getLength(object);
for (int i=0; i<length; i++) {
node.add(objectToSwing(Array.get(object, i)));
}
} else if (object instanceof Node) {
return xmlToSwing((Node) object);
} else {
if (object instanceof Map.Entry<?,?>) {
final Map.Entry<?,?> entry = (Map.Entry<?,?>) object;
node = new NamedTreeNode(String.valueOf(entry.getKey()), entry.getValue());
} else {
node = new DefaultMutableTreeNode(object);
}
node.setAllowsChildren(false);
}
}
return node;
}
/**
* Creates a Swing root tree node from a XML root tree node. Together with
* {@link #toString(TreeNode)}, this method provides a convenient way to print
* the content of a XML document for debugging purpose.
*
* @param node The XML root node.
* @return The given XML node as a Swing node.
*/
public static MutableTreeNode xmlToSwing(final Node node) {
String label = node.getNodeName();
final String value = node.getNodeValue();
if (value != null) {
label += "=\"" + value + '"';
}
final DefaultMutableTreeNode root = new NamedTreeNode(label, node);
final NamedNodeMap attributes = node.getAttributes();
if (attributes != null) {
final int length = attributes.getLength();
for (int i=0; i<length; i++) {
final Node attribute = attributes.item(i);
if (attribute != null) {
label = attribute.getNodeName() + "=\"" + attribute.getNodeValue() + '"';
root.add(new NamedTreeNode(label, attribute, false));
}
}
}
for (Node child=node.getFirstChild(); child!=null; child=child.getNextSibling()) {
root.add(xmlToSwing(child));
}
return root;
}
/**
* Returns a copy of the tree starting at the given node. The references to the
* {@linkplain org.geotoolkit.gui.swing.tree.TreeNode#getUserObject() user objects}
* (if any) and the {@linkplain org.geotoolkit.gui.swing.tree.TreeNode#toString()
* string representations} are copied verbalism.
*
* @param node The tree to copy (may be {@code null}).
* @return A mutable copy of the given tree, or {@code null} if the given tree was null.
*
* @since 2.5
*/
public static MutableTreeNode copy(final TreeNode node) {
return copy(node, null);
}
/**
* Returns a copy of a subset of the tree starting at the given node. The references to the
* {@linkplain org.geotoolkit.gui.swing.tree.TreeNode#getUserObject() user objects} in the
* copied nodes can be modified by the filter.
*
* @param node The tree to copy (may be {@code null}).
* @param filter An object filtering the node to copy, or {@code null} if none.
* @return A mutable copy of the given tree, or {@code null} if the given tree was null or if
* the given node is not {@linkplain TreeNodeFilter#accept accepted} by the filter.
*
* @since 3.04
*/
public static MutableTreeNode copy(final TreeNode node, final TreeNodeFilter filter) {
if (node == null || (filter != null && !filter.accept(node))) {
return null;
}
final String label = node.toString();
Object userObject = getUserObject(node);
if (filter != null) {
userObject = filter.convertUserObject(node, userObject);
}
final boolean allowsChildren = node.getAllowsChildren();
final DefaultMutableTreeNode target;
if (userObject == null || userObject == label) {
target = new DefaultMutableTreeNode(label, allowsChildren);
} else {
target = new NamedTreeNode(label, userObject, allowsChildren);
}
@SuppressWarnings("unchecked")
final Enumeration<? extends TreeNode> children = node.children();
if (children != null) {
while (children.hasMoreElements()) {
final MutableTreeNode child = copy(children.nextElement(), filter);
if (child != null) {
target.add(child);
}
}
}
return target;
}
/**
* Returns a graphical representation of the specified tree model. This representation can
* be printed to the {@linkplain System#out standard output stream} (for example) if it uses
* a monospaced font and supports unicode.
*
* @param tree The tree to format.
* @return A string representation of the tree.
*/
public static String toString(final TreeModel tree) {
final TreeFormat tf = new TreeFormat();
tf.setTableFormatEnabled(true);
final StringBuilder buffer = new StringBuilder();
tf.format(tree, buffer);
return buffer.toString();
}
/**
* Returns a graphical representation of the specified tree. This representation can be
* printed to the {@linkplain System#out standard output stream} (for example) if it uses
* a monospaced font and supports unicode.
*
* @param node The root node of the tree to format.
* @return A string representation of the tree.
*/
public static String toString(final TreeNode node) {
final TreeFormat tf = new TreeFormat();
tf.setTableFormatEnabled(true);
final StringBuilder buffer = new StringBuilder();
tf.format(node, buffer);
return buffer.toString();
}
/**
* Returns a graphical representation of the specified nodes. This representation can be
* printed to the {@linkplain System#out standard output stream} (for example) if it uses
* a monospaced font and supports unicode.
*
* {@section Recursivity}
* This method does not perform any check on the element types. In particular, elements of type
* {@link TreeModel}, {@link TreeNode} or inner {@link Iterable} are not processed recursively.
* It is up to the {@code toString()} implementation of each element to invoke this
* {@code toString} method recursively if they wish (this method is safe for this purpose).
*
* @param root The root name of the tree to format.
* @param nodes The nodes to format.
* @return A string representation of the tree.
*
* @since 3.18
*/
public static String toString(final String root, final Iterable<?> nodes) {
final TreeFormat tf = new TreeFormat();
tf.setTableFormatEnabled(true);
final StringBuilder buffer = new StringBuilder(root).append(tf.getLineSeparator());
tf.format(nodes, buffer);
return buffer.toString();
}
/**
* Prints the specified tree model to the {@linkplain System#out standard output stream}.
* This method is mostly a convenience for debugging purpose.
*
* @param tree The tree to print.
*
* @since 2.4
*/
@Debug
public static void print(final TreeModel tree) {
print(toString(tree));
}
/**
* Prints the specified tree to the {@linkplain System#out standard output stream}.
* This method is mostly a convenience for debugging purpose.
*
* @param node The root node of the tree to print.
*
* @since 2.4
*/
@Debug
public static void print(final TreeNode node) {
print(toString(node));
}
/**
* Prints the given text to the console.
*/
@Debug
private static void print(final String text) {
final PrintWriter out = IOUtilities.standardPrintWriter();
out.println(text);
out.flush();
}
/**
* Display the given tree in a Swing frame. This is a convenience
* method for debugging purpose only.
*
* @param node The root of the tree to display in a Swing frame.
* @param title The frame title, or {@code null} if none.
*
* @since 2.5
*/
@Debug
public static void show(final TreeNode node, final String title) {
show(new DefaultTreeModel(node, true), title);
}
/**
* Display the given tree in a Swing frame. This is a convenience
* method for debugging purpose only.
*
* @param tree The tree to display in a Swing frame.
* @param title The frame title, or {@code null} if none.
*
* @since 2.5
*/
@Debug
public static void show(final TreeModel tree, final String title) {
final JFrame frame = new JFrame(title);
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.add(new JScrollPane(new JTree(tree)));
frame.pack();
frame.setVisible(true);
}
}