/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2001-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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.geotools.resources;
import java.io.IOException;
import java.util.Enumeration;
import java.lang.reflect.Constructor;
import javax.swing.JTree;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreeModel;
import javax.swing.tree.MutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.DefaultMutableTreeNode;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
/**
* Bridges to optional dependencies (especially {@code widget-swing} module).
*
* @todo Most methods of this class need to move as a {@code Trees} class in a {@code util} module.
*
* @since 2.0
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
public final class OptionalDependencies {
/**
* Constructor for {@link org.geotools.gui.swing.tree.NamedTreeNode}.
*/
private static Constructor treeNodeConstructor;
/**
* Set to {@code true} if {@link #treeNodeConstructor} can't be obtained.
*/
private static boolean noNamedTreeNode = false;
/**
* Interdit la création d'objets de cette classe.
*/
private OptionalDependencies() {
}
/**
* Creates an initially empty tree node.
*
* @param name The value to be returned by {@link TreeNode#toString}.
* @param object The user object to be returned by the tree node. May
* or may not be the same than {@code name}.
* @param allowsChildren if children are allowed.
*/
public static DefaultMutableTreeNode createTreeNode(final String name,
final Object object,
final boolean allowsChildren)
{
/*
* If the "modules/extension/swing-widgets" JAR is in the classpath, then create an
* instance of NamedTreeNode (see org.geotools.swing.tree javadoc for an explanation
* about why the NamedTreeNode workaround is needed). We use reflection because the
* swing-widgets module is optional, so we fallback on the standard Swing object if
* we can't create an instance of NamedTreeNode. We will attempt to use reflection
* only once in order to avoid a overhead if the swing-widgets module is not available.
*
* The swing-widgets module contains a "NamedTreeNodeTest" for making sure that the
* NamedTreeNode instances are properly created.
*
* Note: No need to sychronize; this is not a big deal if we make the attempt twice.
*/
if (!noNamedTreeNode) try {
if (treeNodeConstructor == null) {
treeNodeConstructor = Class.forName("org.geotools.gui.swing.tree.NamedTreeNode").
getConstructor(new Class[] {String.class, Object.class, Boolean.TYPE});
}
return (DefaultMutableTreeNode) treeNodeConstructor.newInstance(
new Object[] {name, object, Boolean.valueOf(allowsChildren)});
} catch (Exception e) {
/*
* There is a large amount of checked and unchecked exceptions that the above code
* may thrown. We catch all of them because a reasonable fallback exists (creation
* of the default Swing object below). Note that none of the unchecked exceptions
* (IllegalArgumentException, NullPointerException...) should occurs, except maybe
* SecurityException. Maybe we could let the unchecked exceptions propagate...
*/
noNamedTreeNode = true;
}
return new DefaultMutableTreeNode(name, allowsChildren);
}
/**
* 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.
* <p>
* This method should not be defined here, since this class is about optional dependencies.
* It should be defined in {@link org.geotools.gui.swing.tree.Trees} instead. However we put
* it here (for now) because it is used in some module that don't want to depend on widgets.
*/
public static MutableTreeNode xmlToSwing(final Node node) {
String label = node.getNodeName();
final String value = node.getNodeValue();
if (value != null) {
label += "=\"" + value + '"';
}
final DefaultMutableTreeNode root = createTreeNode(label, node, true);
final NamedNodeMap attributes = node.getAttributes();
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(createTreeNode(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.
*
* @param tree The tree to copy (may be {@code null}).
* @return A mutable copy of the given tree, or {@code null} if the tree was null.
* @todo Use {@code getUserObject} when we can.
*
* @since 2.5
*/
public static MutableTreeNode copy(final TreeNode node) {
if (node == null) {
return null;
}
final DefaultMutableTreeNode target = new DefaultMutableTreeNode(
node.toString(), node.getAllowsChildren());
final Enumeration children = node.children();
if (children != null) {
while (children.hasMoreElements()) {
final TreeNode child = (TreeNode) children.nextElement();
target.add(copy(child));
}
}
return target;
}
/**
* Construit une chaîne de caractères qui contiendra le
* noeud spécifié ainsi que tous les noeuds enfants.
*
* @param model Arborescence à écrire.
* @param node Noeud de l'arborescence à écrire.
* @param buffer Buffer dans lequel écrire le noeud.
* @param level Niveau d'indentation (à partir de 0).
* @param last Indique si les niveaux précédents sont en train d'écrire leurs derniers items.
* @return Le tableau {@code last}, qui peut éventuellement avoir été agrandit.
*/
private static boolean[] format(final TreeModel model, final Object node,
final Appendable buffer, final int level, boolean[] last,
final String lineSeparator) throws IOException
{
for (int i=0; i<level; i++) {
if (i != level-1) {
buffer.append(last[i] ? '\u00A0' : '\u2502').append("\u00A0\u00A0\u00A0");
} else {
buffer.append(last[i] ? '\u2514' : '\u251C').append("\u2500\u2500\u2500");
}
}
buffer.append(String.valueOf(node)).append(lineSeparator);
if (level >= last.length) {
last = XArray.resize(last, level*2);
}
final int count = model.getChildCount(node);
for (int i=0; i<count; i++) {
last[level] = (i == count-1);
last = format(model, model.getChild(node,i), buffer, level+1, last, lineSeparator);
}
return last;
}
/**
* Writes a graphical representation of the specified tree model in the given buffer.
* <p>
* This method should not be defined here, since this class is about optional dependencies.
* It should be defined in {@link org.geotools.gui.swing.tree.Trees} instead. However we put
* it here (for now) because it is used in some module that don't want to depend on widgets.
*
* @param tree The tree to format.
* @param buffer Where to format the tree.
* @param lineSeparator The line separator, or {@code null} for the system default.
* @throws IOException if an error occured while writting in the given buffer.
*
* @since 2.5
*/
public static void format(final TreeModel tree, final Appendable buffer, String lineSeparator)
throws IOException
{
final Object root = tree.getRoot();
if (root != null) {
if (lineSeparator == null) {
lineSeparator = System.getProperty("line.separator", "\n");
}
format(tree, root, buffer, 0, new boolean[64], lineSeparator);
}
}
/**
* Writes a graphical representation of the specified tree in the given buffer.
* <p>
* This method should not be defined here, since this class is about optional dependencies.
* It should be defined in {@link org.geotools.gui.swing.tree.Trees} instead. However we put
* it here (for now) because it is used in some module that don't want to depend on widgets.
*
* @param node The root node of the tree to format.
* @param buffer Where to format the tree.
* @param lineSeparator The line separator, or {@code null} for the system default.
* @throws IOException if an error occured while writting in the given buffer.
*
* @since 2.5
*/
public static void format(final TreeNode node, final Appendable buffer, String lineSeparator)
throws IOException
{
format(new DefaultTreeModel(node, true), buffer, lineSeparator);
}
/**
* 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.
* <p>
* This method should not be defined here, since this class is about optional dependencies.
* It should be defined in {@link org.geotools.gui.swing.tree.Trees} instead. However we put
* it here (for now) because it is used in some module that don't want to depend on widgets.
*
* @param tree The tree to format.
* @return A string representation of the tree, or {@code null} if it doesn't contain any node.
*/
public static String toString(final TreeModel tree) {
final Object root = tree.getRoot();
if (root == null) {
return null;
}
final StringBuilder buffer = new StringBuilder();
final String lineSeparator = System.getProperty("line.separator", "\n");
try {
format(tree, root, buffer, 0, new boolean[64], lineSeparator);
} catch (IOException e) {
// Should never happen when writting into a StringBuilder.
throw new AssertionError(e);
}
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.
* <p>
* This method should not be defined here, since this class is about optional dependencies.
* It should be defined in {@link org.geotools.gui.swing.tree.Trees} instead. However we put
* it here (for now) because it is used in some module that don't want to depend on widgets.
*
* @param node The root node of the tree to format.
* @return A string representation of the tree, or {@code null} if it doesn't contain any node.
*/
public static String toString(final TreeNode node) {
return toString(new DefaultTreeModel(node, true));
}
/**
* 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
*/
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
*/
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);
}
}