/*
* 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.Writer;
import java.io.Flushable;
import java.io.IOException;
import java.io.StringWriter;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Enumeration;
import java.text.FieldPosition;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreeModel;
import javax.swing.tree.DefaultTreeModel; // Really the Swing implementation, not the Geotk one.
import org.geotoolkit.io.LineWriter;
import org.geotoolkit.io.ExpandedTabWriter;
import org.geotoolkit.io.TableWriter;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.CharSequences;
import org.apache.sis.util.collection.BackingStoreException;
import org.geotoolkit.resources.Errors;
/**
* A formatter for a tree of nodes.
*
* @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
final class TreeFormat {
/**
* For cross-version compatibility.
*/
private static final long serialVersionUID = -4476366905386037025L;
/**
* The number of spaces to add on the left margin for each indentation level.
* The default value is 4.
*/
private int indentation;
/**
* The position of the vertical line, relative to the position of the root label.
* The default value is 0, which means that the vertical line is drawn below the
* first letter of the root label.
*/
private int verticalLinePosition;
/**
* The line separator to use for formatting the tree.
*/
private String lineSeparator;
/**
* The column separator to use if the table format is enabled.
*
* @since 3.19
*/
private char columnSeparator = '…';
/**
* {@code true} for enabling the formating of tree tables. The default value is {@code false}.
*
* @since 3.19
*/
private boolean isTableFormatEnabled;
/**
* The tree symbols to write in the left margin, or {@code null} if not yet computed.
* The default symbols are as below:
* <p>
* <ul>
* <li>{@link #treeBlank} = {@code " "}</li>
* <li>{@link #treeLine} = {@code "│ "}</li>
* <li>{@link #treeCross} = {@code "├───"}</li>
* <li>{@link #treeEnd} = {@code "└───"}</li>
* </ul>
*/
private transient String treeBlank, treeLine, treeCross, treeEnd;
/**
* The writer to use for formatting a possible multi-line text to a monoline text.
* Will be created only when first needed.
*
* @since 3.19
*/
private transient Writer lineWriter;
/**
* The buffer which is backing {@link #lineWriter}.
*
* @since 3.19
*/
private transient StringBuffer lineBuffer;
/**
* Creates a new format.
*/
public TreeFormat() {
indentation = 4;
lineSeparator = System.lineSeparator();
}
/**
* Clears the symbols used when writing the tree.
* They will be computed again when first needed.
*/
private void clearTreeSymbols() {
treeBlank = null;
treeLine = null;
treeCross = null;
treeEnd = null;
}
/**
* Returns the number of spaces to add on the left margin for each indentation level.
* The default value is 4.
*
* @return The current indentation.
*/
public int getIndentation() {
return indentation;
}
/**
* Sets the number of spaces to add on the left margin for each indentation level.
* If the new indentation is smaller than the {@linkplain #getVerticalLinePosition()
* vertical line position}, then the later is also set to the given indentation value.
*
* @param indentation The new indentation.
* @throws IllegalArgumentException If the given value is negative.
*/
public void setIndentation(final int indentation) throws IllegalArgumentException {
ArgumentChecks.ensurePositive("indentation", indentation);
this.indentation = indentation;
if (verticalLinePosition > indentation) {
verticalLinePosition = indentation;
}
clearTreeSymbols();
}
/**
* Returns the position of the vertical line, relative to the position of the root label.
* The default value is 0, which means that the vertical line is drawn below the first
* letter of the root label.
*
* @return The current vertical line position.
*/
public int getVerticalLinePosition() {
return verticalLinePosition;
}
/**
* Sets the position of the vertical line, relative to the position of the root label.
* The given value can not be greater than the {@linkplain #getIndentation() indentation}.
*
* @param verticalLinePosition The new vertical line position.
* @throws IllegalArgumentException If the given value is negative or greater than the indentation.
*/
public void setVerticalLinePosition(final int verticalLinePosition) throws IllegalArgumentException {
ArgumentChecks.ensureBetween("verticalLinePosition", 0, indentation, verticalLinePosition);
this.verticalLinePosition = verticalLinePosition;
clearTreeSymbols();
}
/**
* Returns the current line separator. The default value is system-dependent.
*
* @return The current line separator.
*/
public String getLineSeparator() {
return lineSeparator;
}
/**
* Sets the line separator.
*
* @param separator The new line separator.
*/
public void setLineSeparator(final String separator) {
ArgumentChecks.ensureNonNull("separator", separator);
lineSeparator = separator;
}
/**
* Returns the character used as column separator. This character will be inserted between the
* columns only if the {@linkplain #isTableFormatEnabled() table format is enabled} and the
* tree to format contains {@link TreeTableNode} elements.
* <p>
* The default value if <code>'…'</code>.
*
* @return The column separator.
*
* @since 3.19
*/
public char getColumnSeparator() {
return columnSeparator;
}
/**
* Sets the column character to insert between the columns in a {@link TreeTableNode}.
*
* @param separator The column separator.
*
* @since 3.19
*/
public void setColumnSeparator(final char separator) {
columnSeparator = separator;
}
/**
* Returns {@code true} if this {@code TreeFormat} is allowed to format the tree using many
* columns. The default value is {@code false}. Setting this property to {@code true} is
* useful only if the tree to format contains {@link TreeTableNode} elements.
*
* @return {@code true} if this {@code TreeFormat} object is allowed to format many columns.
*
* @since 3.19
*/
public boolean isTableFormatEnabled() {
return isTableFormatEnabled;
}
/**
* Sets whatever this {@code TreeFormat} is allowed to format the tree as a table.
* <p>
* <b>NOTE:</b> parsing of table format is not yet implemented.
*
* @param enabled {@code true} for enabling the table format.
*
* @since 3.19
*/
public void setTableFormatEnabled(final boolean enabled) {
isTableFormatEnabled = enabled;
}
/**
* Returns {@code true} if the given tree should be formatted as a tree table.
* The current implementation returns {@code true} if the given tree contains
* at least one node of kind {@link TreeTableNode}.
*
* @param node The root node to inspect.
* @return {@code false} if at least one tree table node was found.
*/
private static boolean hasTreeTableNode(final TreeNode node) {
if (node instanceof TreeTableNode) {
return true;
}
@SuppressWarnings("unchecked")
final Enumeration<TreeNode> e = node.children();
if (e != null) while (e.hasMoreElements()) {
if (hasTreeTableNode(e.nextElement())) {
return true;
}
}
return false;
}
/**
* Computes the {@code tree*} fields, if needed.
* This is done only when first needed.
*/
private void ensureInitialized() {
if (treeBlank == null) {
final int indentation = this.indentation;
final int verticalLinePosition = this.verticalLinePosition;
final char[] buffer = new char[indentation];
for (int k=0; k<4; k++) {
final char vc, hc;
if ((k & 2) == 0) {
// No horizontal line
vc = (k & 1) == 0 ? '\u00A0' : '\u2502';
hc = '\u00A0';
} else {
// With a horizontal line
vc = (k & 1) == 0 ? '\u2514' : '\u251C';
hc = '\u2500';
}
Arrays.fill(buffer, 0, verticalLinePosition, '\u00A0');
buffer[verticalLinePosition] = vc;
Arrays.fill(buffer, verticalLinePosition + 1, indentation, hc);
final String symbols = String.valueOf(buffer);
switch (k) {
case 0: treeBlank = symbols; break;
case 1: treeLine = symbols; break;
case 2: treeEnd = symbols; break;
case 3: treeCross = symbols; break;
default: throw new AssertionError(k);
}
}
}
}
/**
* Returns a string representation of the given value, or {@code null} if the value is
* null. Tabulations are replaced by a single space, and line feeds are replaced by the
* Unicode "carriage return" character. This is necessary in order to avoid conflict with
* the characters expected by {@link TableWriter}.
*/
private String toString(final Object value) throws IOException {
Writer writer = lineWriter;
if (writer == null) {
final StringWriter buffer = new StringWriter();
lineWriter = writer = new ExpandedTabWriter(new LineWriter(buffer, " \u00B6 "));
lineBuffer = buffer.getBuffer();
}
writer.write(String.valueOf(value));
writer.flush();
final String text = lineBuffer.toString();
writer.write('\n'); // Reset tabulation count and discart trailing spaces.
lineBuffer.setLength(0);
return text;
}
/**
* Appends to the given buffer the string representation of the given node and all
* its children. This method invokes itself recursively.
*
* @param model The tree to format.
* @param node The node of the tree to format.
* @param toAppendTo Where to write the string representation.
* @param level Indentation level. The first level is 0.
* @param last {@code true} if the previous levels are writing the last node.
* @return The {@code last} array, or a copy of that array if it was necessary
* to increase its length.
*/
private boolean[] format(final TreeModel model, final Object node,
final Appendable toAppendTo, final int level, boolean[] last) throws IOException
{
for (int i=0; i<level; i++) {
final boolean isLast = last[i];
toAppendTo.append((i != level-1)
? (isLast ? treeBlank : treeLine)
: (isLast ? treeEnd : treeCross));
}
if ((node instanceof TreeTableNode) && (toAppendTo instanceof TableWriter)) {
final TreeTableNode tableNode = (TreeTableNode) node;
final int columnCount = tableNode.getColumnCount();
for (int i=0; i<columnCount; i++) {
if (i != 0) {
((TableWriter) toAppendTo.append(columnSeparator)).nextColumn(columnSeparator);
}
final Object value = tableNode.getValueAt(i);
if (value != null) {
toAppendTo.append(toString(value));
}
}
} else {
toAppendTo.append(toString(node));
}
toAppendTo.append(lineSeparator);
if (level >= last.length) {
last = Arrays.copyOf(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), toAppendTo, level+1, last);
}
return last;
}
/**
* Writes a graphical representation of the specified tree model in the given buffer.
* This method iterates recursively over all children. Each children is fetched by a
* call to {@link TreeModel#getChild(Object, int)} and its string representation
* (expected to uses a single line) is created by a call to {@link String#valueOf(Object)}.
*
* @param tree The tree to format.
* @param toAppendTo Where to format the tree.
* @throws IOException If an error occurred while writing in the given appender.
*
* @see Trees#toString(TreeModel)
*/
public void format(final TreeModel tree, final Appendable toAppendTo) throws IOException {
final Object root = tree.getRoot();
if (root != null) {
Writer buffer = null;
Appendable out = toAppendTo;
if (isTableFormatEnabled && (root instanceof TreeNode) && hasTreeTableNode((TreeNode) root)) {
final Writer writer;
if (toAppendTo instanceof Writer) {
writer = (Writer) toAppendTo;
} else {
writer = buffer = new StringWriter();
}
out = new TableWriter(new LineWriter(writer, lineSeparator), " ");
}
ensureInitialized();
format(tree, root, out, 0, new boolean[64]);
if (out != toAppendTo) {
((Flushable) out).flush(); // Needed for writing the table content.
}
if (buffer != null) {
toAppendTo.append(buffer.toString());
}
}
}
/**
* Convenience method which delegate to the above {@link #format(TreeModel, Appendable)}
* method, but without throwing {@link IOException}. The I/O exception should never occur
* since we are writing in a {@link StringBuilder}.
*
* {@note Strictly speaking, an <code>IOException</code> could still occur in the user
* overrides the above <code>format</code> method and performs some I/O operation outside
* the given <code>StringBuilder</code>. However this is not the intended usage of this
* class and implementors should avoid such unexpected I/O operation.}
*
* @param tree The tree to format.
* @param toAppendTo Where to format the tree.
*/
public final void format(final TreeModel tree, final StringBuilder toAppendTo) {
try {
format(tree, (Appendable) toAppendTo);
} catch (IOException e) {
// Should never occur, unless the user overriden the above 'format'
// method in a weird way. This is why we don't use AssertionError.
throw new BackingStoreException(e);
}
}
/**
* Writes a graphical representation of the specified tree in the given buffer.
* The default implementation delegates to {@link #format(TreeModel, Appendable)}.
*
* @param node The root node of the tree to format.
* @param toAppendTo Where to format the tree.
* @throws IOException If an error occurred while writing in the given appender.
*
* @see Trees#toString(TreeNode)
*/
public void format(final TreeNode node, final Appendable toAppendTo) throws IOException {
// Use the Swing implementation in order to avoid recursivity
// in the debugguer if the tree model is formatted as a tree.
format(new DefaultTreeModel(node, true), toAppendTo);
}
/**
* Convenience method which delegate to the above {@link #format(TreeNode, Appendable)}
* method, but without throwing {@link IOException}. The I/O exception should never occur
* since we are writing in a {@link StringBuilder}.
*
* {@note Strictly speaking, an <code>IOException</code> could still occur in the user
* overrides the above <code>format</code> method and performs some I/O operation outside
* the given <code>StringBuilder</code>. However this is not the intended usage of this
* class and implementors should avoid such unexpected I/O operation.}
*
* @param node The root node of the tree to format.
* @param toAppendTo Where to format the tree.
*/
public final void format(final TreeNode node, final StringBuilder toAppendTo) {
try {
format(node, (Appendable) toAppendTo);
} catch (IOException e) {
// Should never occur, unless the user overriden the above 'format'
// method in a weird way. This is why we don't use AssertionError.
throw new BackingStoreException(e);
}
}
/**
* Writes a graphical representation of the specified elements in the given buffer. This method
* iterates over the given collection and invokes the {@link String#valueOf(Object)} method for
* each element. The {@code String} value can span multiple lines.
*
* {@section Root label}
* This method formats only the given child elements. It does not format anything for the
* root. It is up to the caller to format a root label on its own line before to invoke
* this method.
*
* {@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 format} method recursively if they wish (this method is safe for this purpose).
*
* @param nodes A collection of nodes to format.
* @param toAppendTo Where to format the tree.
* @throws IOException If an error occurred while writing in the given appender.
*
* @see Trees#toString(String, Iterable)
*/
public void format(final Iterable<?> nodes, final Appendable toAppendTo) throws IOException {
ensureInitialized();
final Iterator<?> it = nodes.iterator();
boolean hasNext = it.hasNext();
while (hasNext) {
final CharSequence[] lines = CharSequences.splitOnEOL(String.valueOf(it.next()));
hasNext = it.hasNext();
final String next;
String margin;
if (hasNext) {
margin = treeCross;
next = treeLine;
} else {
margin = treeEnd;
next = treeBlank;
}
for (final CharSequence line : lines) {
if (line.length() != 0) {
toAppendTo.append(margin).append(line).append(lineSeparator);
margin = next;
}
}
}
}
/**
* Convenience method which delegate to the above {@link #format(Iterable, Appendable)}
* method, but without throwing {@link IOException}. The I/O exception should never occur
* since we are writing in a {@link StringBuilder}.
*
* {@note Strictly speaking, an <code>IOException</code> could still occur in the user
* overrides the above <code>format</code> method and performs some I/O operation outside
* the given <code>StringBuilder</code>. However this is not the intended usage of this
* class and implementors should avoid such unexpected I/O operation.}
*
* @param nodes A collection of nodes to format.
* @param toAppendTo Where to format the tree.
*/
public final void format(final Iterable<?> nodes, final StringBuilder toAppendTo) {
try {
format(nodes, (Appendable) toAppendTo);
} catch (IOException e) {
// Should never occur, unless the user overriden the above 'format'
// method in a weird way. This is why we don't use AssertionError.
throw new BackingStoreException(e);
}
}
/**
* Writes a graphical representation of the specified tree in the given buffer.
* The default implementation delegates to one of the following method depending
* on the type of the given tree:
* <p>
* <ul>
* <li>{@link #format(TreeModel, Appendable)}</li>
* <li>{@link #format(TreeNode, Appendable)}</li>
* <li>{@link #format(Iterable, Appendable)}</li>
* </ul>
*
* @param tree The tree to format.
* @param toAppendTo Where to format the tree.
* @param pos Ignored in current implementation.
* @return The given buffer, returned for convenience.
*/
public StringBuffer format(final Object tree, final StringBuffer toAppendTo, final FieldPosition pos) {
ArgumentChecks.ensureNonNull("tree", tree);
try {
if (tree instanceof TreeModel) {
format((TreeModel) tree, toAppendTo);
} else if (tree instanceof TreeNode) {
format((TreeNode) tree, toAppendTo);
} else if (tree instanceof Iterable<?>) {
format((Iterable<?>) tree, toAppendTo);
} else {
throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentClass_3,
"tree", tree.getClass(), TreeModel.class));
}
} catch (IOException e) {
// Should never happen when writing into a StringBuffer.
throw new AssertionError(e);
}
return toAppendTo;
}
}