| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.sis.util.collection; |
| |
| import java.util.Arrays; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.TimeZone; |
| import java.util.Currency; |
| import java.util.ConcurrentModificationException; |
| import java.util.function.Predicate; |
| import java.io.IOException; |
| import java.text.Format; |
| import java.text.DecimalFormat; |
| import java.text.ParsePosition; |
| import java.text.ParseException; |
| import java.util.regex.Matcher; |
| import org.opengis.util.CodeList; |
| import java.nio.charset.Charset; |
| import org.opengis.util.Type; |
| import org.opengis.util.Record; |
| import org.opengis.util.GenericName; |
| import org.opengis.util.InternationalString; |
| import org.apache.sis.io.LineAppender; |
| import org.apache.sis.io.TableAppender; |
| import org.apache.sis.io.TabularFormat; |
| import org.apache.sis.io.CompoundFormat; |
| import org.apache.sis.measure.UnitFormat; |
| import org.apache.sis.util.Numbers; |
| import org.apache.sis.util.Workaround; |
| import org.apache.sis.util.CharSequences; |
| import org.apache.sis.util.ArgumentChecks; |
| import org.apache.sis.util.resources.Errors; |
| import org.apache.sis.util.resources.Vocabulary; |
| import org.apache.sis.math.DecimalFunctions; |
| import org.apache.sis.internal.util.Acyclic; |
| import org.apache.sis.internal.util.MetadataServices; |
| import org.apache.sis.internal.util.LocalizedParseException; |
| import org.apache.sis.internal.util.TreeFormatCustomization; |
| |
| import static org.apache.sis.util.Characters.NO_BREAK_SPACE; |
| |
| |
| /** |
| * A parser and formatter for {@link TreeTable} instances. |
| * This formatter is given an arbitrary number of {@link TableColumn}s |
| * to use during the formatting. The first column is taken as the node label. |
| * If a {@code TreeTable} is formatted with only that column, |
| * then the {@link String} result is like the following example: |
| * |
| * {@preformat text |
| * Node #1 |
| * ├─Node #2 |
| * │ └─Node #4 |
| * └─Node #3 |
| * } |
| * |
| * If the same {@code TreeTable} is formatted with two columns, |
| * then the {@link String} result is like the following example: |
| * |
| * {@preformat text |
| * Node #1……………………… More #1 |
| * ├─Node #2…………… More #2 |
| * │ └─Node #4… More #4 |
| * └─Node #3…………… More #3 |
| * } |
| * |
| * This representation can be printed to the {@linkplain java.io.Console#writer() console output} |
| * (for example) if the stream uses a monospaced font and supports Unicode characters. |
| * |
| * <h2>Customization</h2> |
| * Some formatting characteristics (indentation width, column where to draw the vertical line |
| * below nodes) can be modified by calls to the setter methods defined in this formatter. |
| * In particular, the dots joining the node labels to their values can be specified by the |
| * {@linkplain #setColumnSeparatorPattern(String) column separator pattern}. |
| * The default pattern is {@code "?……[…] "}, which means <cite>"If the next value is non-null, |
| * then insert the {@code "……"} string, repeat the {@code '…'} character as many time as needed |
| * (may be zero), and finally insert a space"</cite>. |
| * |
| * <h2>Safety against infinite recursivity</h2> |
| * Some {@code TreeTable} implementations generate the nodes dynamically as wrappers around Java objects. |
| * Such Java objects may contain cyclic associations (<var>A</var> contains <var>B</var> contains <var>C</var> |
| * contains <var>A</var>), which result in a tree of infinite depth. Some examples can been found in ISO 19115 |
| * metadata. This {@code TreeTableFormat} class contains a safety against such cycles. The algorithm is based |
| * on the assumption that for each node, the values and children are fully determined by the |
| * {@linkplain TreeTable.Node#getUserObject() user object}, if non-null. Consequently for each node <var>C</var> |
| * to be formatted, if the user object of that node is the same instance (in the sense of the {@code ==} operator) |
| * than the user object of a parent node <var>A</var>, then the children of the <var>C</var> node will not be formatted. |
| * |
| * @author Martin Desruisseaux (IRD, Geomatys) |
| * @version 1.0 |
| * @since 0.3 |
| * @module |
| */ |
| public class TreeTableFormat extends TabularFormat<TreeTable> { |
| /** |
| * For cross-version compatibility. |
| */ |
| private static final long serialVersionUID = 147992015470098561L; |
| |
| /** |
| * Shared {@code TreeTableFormat} instance for {@link DefaultTreeTable#toString()} implementation. |
| * Usage of this instance shall be done in a synchronized block. Note that metadata objects defined |
| * as {@link org.apache.sis.metadata.AbstractMetadata} subclasses use their own format instance. |
| */ |
| static final TreeTableFormat INSTANCE = new TreeTableFormat(null, null); |
| |
| /** |
| * The table columns to format, or {@code null} for formatting all of them. |
| * This map shall not be modified after creation, because it may be shared |
| * by many tables. |
| * |
| * @see #getColumns() |
| * @see #setColumns(TableColumn[]) |
| */ |
| private Map<TableColumn<?>,Integer> columnIndices; |
| |
| /** |
| * The number of characters to add on the left side for each indentation level. |
| * The default value is 4. |
| * |
| * @see #getIndentation() |
| * @see #setIndentation(int) |
| */ |
| private int indentation; |
| |
| /** |
| * The position of the vertical line, relative to the position of the label of the parent node. |
| * The default value is 2, which means that the vertical line is drawn below the third letter |
| * of the node label. |
| * |
| * @see #getVerticalLinePosition() |
| * @see #setVerticalLinePosition(int) |
| */ |
| private int verticalLinePosition; |
| |
| /** |
| * The tree symbols to write in the left margin, or {@code null} if not yet computed. |
| * The default symbols are as below: |
| * |
| * <ul> |
| * <li>{@code treeBlank} = {@code " "}</li> |
| * <li>{@code treeLine} = {@code " │ "}</li> |
| * <li>{@code treeCross} = {@code " ├─"}</li> |
| * <li>{@code treeEnd} = {@code " └─"}</li> |
| * </ul> |
| * |
| * @see #clearTreeSymbols() |
| * @see #createTreeSymbols() |
| */ |
| private transient String treeBlank, treeLine, treeCross, treeEnd; |
| |
| /** |
| * A filter for specifying whether a node should be formatted, or {@code null} if no filtering is applied. |
| * This is ignored at parsing time. |
| * |
| * @see #getNodeFilter() |
| * @see #setNodeFilter(Predicate) |
| */ |
| private Predicate<TreeTable.Node> nodeFilter; |
| |
| /** |
| * The set to be given to {@link Writer} constructor, |
| * created when first needed and reused for subsequent formatting. |
| */ |
| private transient Set<TreeTable.Node> recursivityGuard; |
| |
| /** |
| * A clone of the number format to be used with different settings (number of fraction digits, scientific notation). |
| * We use a clone for avoiding to change the setting of potentially user-supplied number format. This is used only |
| * for floating point numbers, not for integers. |
| */ |
| private transient DecimalFormat adaptableFormat; |
| |
| /** |
| * The default pattern used by {@link #adaptableFormat}. |
| * Used for switching back to default mode after scientific notation. |
| */ |
| private transient String defaultPattern; |
| |
| /** |
| * Whether {@link #adaptableFormat} is using scientific notation. |
| */ |
| private transient boolean usingScientificNotation; |
| |
| /** |
| * Creates a new tree table format. |
| * |
| * @param locale the locale to use for numbers, dates and angles formatting, |
| * or {@code null} for the {@linkplain Locale#ROOT root locale}. |
| * @param timezone the timezone, or {@code null} for UTC. |
| */ |
| public TreeTableFormat(final Locale locale, final TimeZone timezone) { |
| super(locale, timezone); |
| indentation = 4; |
| verticalLinePosition = 2; |
| beforeFill = "……"; |
| fillCharacter = '…'; |
| omitTrailingNulls = true; |
| } |
| |
| /** |
| * Clears the symbols used when writing the tree. |
| * They will be computed again when first needed. |
| * |
| * @see #createTreeSymbols() |
| */ |
| private void clearTreeSymbols() { |
| treeBlank = null; |
| treeLine = null; |
| treeCross = null; |
| treeEnd = null; |
| } |
| |
| /** |
| * Returns the type of objects formatted by this class. |
| * |
| * @return {@code TreeTable.class} |
| */ |
| @Override |
| public final Class<TreeTable> getValueType() { |
| return TreeTable.class; |
| } |
| |
| /** |
| * Returns the table columns to parse and format, or {@code null} for the default list of columns. |
| * The default is: |
| * |
| * <ul> |
| * <li>On parsing, a single column containing the node label as a {@link String}.</li> |
| * <li>On formatting, {@linkplain TreeTable#getColumns() all <code>TreeTable</code> columns}.</li> |
| * </ul> |
| * |
| * @return the table columns to parse and format, or {@code null} for the default. |
| */ |
| public TableColumn<?>[] getColumns() { |
| return (columnIndices != null) ? DefaultTreeTable.getColumns(columnIndices) : null; |
| } |
| |
| /** |
| * Sets the table columns to parse and format. A {@code null} value means to use the default |
| * list of columns, as defined in the {@link #getColumns()} method. |
| * |
| * @param columns the table columns to parse and format, or {@code null} for the default. |
| * @throws IllegalArgumentException if the given array is empty, contains a null element |
| * or a duplicated value. |
| */ |
| public void setColumns(final TableColumn<?>... columns) throws IllegalArgumentException { |
| if (columns == null) { |
| columnIndices = null; |
| } else { |
| ArgumentChecks.ensureNonEmpty("columns", columns); |
| columnIndices = DefaultTreeTable.createColumnIndices(columns); |
| } |
| } |
| |
| /** |
| * 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 2, which means that the vertical line is drawn below the third |
| * 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 filter that specify whether a node should be formatted or ignored. |
| * This is the predicate specified in the last call to {@link #setNodeFilter(Predicate)}. |
| * If no filter has been set, then this method returns {@code null}. |
| * |
| * @return a filter for specifying whether a node should be formatted, or {@code null} if no filtering is applied. |
| * |
| * @since 1.0 |
| */ |
| public Predicate<TreeTable.Node> getNodeFilter() { |
| return nodeFilter; |
| } |
| |
| /** |
| * Sets a filter specifying whether a node should be formatted or ignored. |
| * Filters are tested at formatting time for all children of the root node (but not for the root node itself). |
| * Filters are ignored at parsing time. |
| * |
| * @param filter filter for specifying whether a node should be formatted, or {@code null} for no filtering. |
| * |
| * @since 1.0 |
| */ |
| public void setNodeFilter(final Predicate<TreeTable.Node> filter) { |
| this.nodeFilter = filter; |
| } |
| |
| /** |
| * Returns the locale to use for code lists, international strings and exception messages. |
| */ |
| final Locale getDisplayLocale() { |
| return getLocale(Locale.Category.DISPLAY); |
| } |
| |
| /** |
| * Returns the formats to use for parsing and formatting the values of each column. |
| * The returned array may contain {@code null} elements, which means that the values |
| * in that column can be stored as {@code String}s. |
| * |
| * @param mandatory {@code true} if an exception shall be thrown for unrecognized types, or |
| * {@code false} for storing a {@code null} value in the array instead. |
| * @throws IllegalStateException if {@code mandatory} is {@code true} and a column |
| * contains values of an unsupported type. |
| */ |
| final Format[] getFormats(final TableColumn<?>[] columns, final boolean mandatory) throws IllegalStateException { |
| final Format[] formats = new Format[columns.length]; |
| for (int i=0; i<formats.length; i++) { |
| final Class<?> valueType = columns[i].getElementType(); |
| if ((formats[i] = getFormat(valueType)) == null) { |
| if (mandatory && !valueType.isAssignableFrom(String.class)) { |
| throw new IllegalStateException(Errors.format( |
| Errors.Keys.UnspecifiedFormatForClass_1, valueType)); |
| } |
| } |
| } |
| return formats; |
| } |
| |
| /** |
| * Creates a tree from the given character sequence, |
| * or returns {@code null} if the given text does not look like a tree for this method. |
| * This method can parse the trees created by the {@code format(…)} methods |
| * defined in this class. |
| * |
| * <h4>Parsing rules</h4> |
| * <ul> |
| * <li>Each node shall be represented by a single line made of two parts, in that order: |
| * <ol> |
| * <li>white spaces and tree drawing characters ({@code '│'}, {@code '├'}, {@code '└'} or {@code '─'});</li> |
| * <li>string representations of node values, separated by the |
| * {@linkplain #getColumnSeparatorPattern() colunm separator}.</li> |
| * </ol> |
| * </li> |
| * <li>The number of spaces and drawing characters before the node values determines the node |
| * indentation. This indentation does not need to be a factor of the {@link #getIndentation()} |
| * value, but must be consistent across all the parsed tree.</li> |
| * <li>The indentation determines the parent of each node.</li> |
| * <li>Parsing stops at first empty line (ignoring whitespaces), or at the end of the given text.</li> |
| * </ul> |
| * |
| * <h4>Error index</h4> |
| * If the given text does not seem to be a tree table, then this method returns {@code null}. |
| * Otherwise if parsing started but failed, then: |
| * |
| * <ul> |
| * <li>{@link ParsePosition#getErrorIndex()} will give the index at the beginning |
| * of line or beginning of cell where the error occurred, and</li> |
| * <li>{@link ParseException#getErrorOffset()} will give either the same value, |
| * or a slightly more accurate value inside the cell.</li> |
| * </ul> |
| * |
| * @param text the character sequence for the tree to parse. |
| * @param pos the position where to start the parsing. |
| * @return the parsed tree, or {@code null} if the given character sequence can not be parsed. |
| * @throws ParseException if an error occurred while parsing a node value. |
| */ |
| @Override |
| @SuppressWarnings("null") |
| public TreeTable parse(final CharSequence text, final ParsePosition pos) throws ParseException { |
| final Matcher matcher = getColumnSeparatorMatcher(text); |
| final int length = text.length(); |
| int indexOfLineStart = pos.getIndex(); |
| int indentationLevel = 0; // Current index in the 'indentations' array. |
| int[] indentations = new int[16]; // Number of spaces (ignoring drawing characters) for each level. |
| TreeTable.Node lastNode = null; // Last parsed node, having 'indentation[level]' characters before its content. |
| TreeTable.Node root = null; // First node found while parsing. |
| final DefaultTreeTable table = new DefaultTreeTable(columnIndices != null ? columnIndices : TableColumn.NAME_MAP); |
| final TableColumn<?>[] columns = DefaultTreeTable.getColumns(table.columnIndices); |
| final Format[] formats = getFormats(columns, true); |
| do { |
| final int startNextLine = CharSequences.indexOfLineStart(text, 1, indexOfLineStart); |
| int endOfLine = startNextLine; |
| while (endOfLine > indexOfLineStart) { |
| final int c = text.charAt(endOfLine-1); |
| if (c != '\r' && c != '\n') break; |
| endOfLine--; // Skip trailing '\r' and '\n'. |
| } |
| /* |
| * Skip leading spaces using Character.isSpaceChar(…) instead than isWhitespace(…) |
| * because we need to skip non-breaking spaces as well as ordinary space. We don't |
| * need to consider line feeds since they were handled by the lines just above. |
| */ |
| boolean hasChar = false; |
| int i; // The indentation of current line. |
| for (i=indexOfLineStart; i<endOfLine;) { |
| final int c = Character.codePointAt(text, i); |
| if (!Character.isSpaceChar(c)) { |
| hasChar = true; |
| if ("─│└├".indexOf(c) < 0) { |
| break; |
| } |
| } |
| i += Character.charCount(c); |
| } |
| if (!hasChar) { |
| break; // The line contains only whitespaces. |
| } |
| /* |
| * Go back to the fist non-space character (should be '─'). We do that in case the |
| * user puts some spaces in the text of the node label, since we don't want those |
| * user-spaces to interfer with the calculation of indentation. |
| */ |
| int indexOfValue = i; |
| i = CharSequences.skipTrailingWhitespaces(text, indexOfLineStart, i) - indexOfLineStart; |
| /* |
| * Found the first character which is not part of the indentation. Create a new root |
| * (without parent for now) and parse the values for each column. Columns with empty |
| * text are not parsed (the value is left to null). |
| */ |
| final TreeTable.Node node = new DefaultTreeTable.Node(table); |
| matcher.region(indexOfValue, endOfLine); |
| for (int ci=0; ci<columns.length; ci++) { |
| final boolean found = matcher.find(); |
| int endOfColumn = found ? matcher.start() : endOfLine; |
| indexOfValue = CharSequences.skipLeadingWhitespaces (text, indexOfValue, endOfColumn); |
| int endOfValue = CharSequences.skipTrailingWhitespaces(text, indexOfValue, endOfColumn); |
| if (endOfValue > indexOfValue) { |
| final String valueText = text.subSequence(indexOfValue, endOfValue).toString(); |
| try { |
| parseValue(node, columns[ci], formats[ci], valueText); |
| } catch (ParseException | ClassCastException e) { |
| pos.setErrorIndex(indexOfValue); // See method javadoc. |
| if (e instanceof ParseException) { |
| indexOfValue += ((ParseException) e).getErrorOffset(); |
| } |
| throw new LocalizedParseException(getDisplayLocale(), Errors.Keys.UnparsableStringForClass_2, |
| new Object[] {columns[ci].getElementType(), valueText}, indexOfValue).initCause(e); |
| } |
| } |
| if (!found) break; |
| /* |
| * The end of this column will be the beginning of the next column, |
| * after skipping the last character of the column separator. |
| */ |
| indexOfValue = matcher.end(); |
| } |
| /* |
| * If this is the first node created so far, it will be the root. |
| */ |
| if (root == null) { |
| indentations[0] = i; |
| root = node; |
| } else { |
| int p; |
| while (i < (p = indentations[indentationLevel])) { |
| /* |
| * Lower indentation level: go up in the tree until we find the new parent. |
| * Note that lastNode.getParent() should never return null, since only the |
| * node at 'indentationLevel == 0' has a null parent and we check that case. |
| */ |
| if (--indentationLevel < 0) { |
| pos.setErrorIndex(indexOfLineStart); |
| throw new LocalizedParseException(getDisplayLocale(), |
| Errors.Keys.NodeHasNoParent_1, new Object[] {node}, indexOfLineStart); |
| } |
| lastNode = lastNode.getParent(); |
| } |
| if (i == p) { |
| /* |
| * The node we just created is a sibling of the previous node. This is |
| * illegal if level==0, in which case we have no parent. Otherwise add |
| * the sibling to the common parent and let the indentation level unchanged. |
| */ |
| final TreeTable.Node parent = lastNode.getParent(); |
| if (parent == null) { |
| pos.setErrorIndex(indexOfLineStart); |
| throw new LocalizedParseException(getDisplayLocale(), |
| Errors.Keys.NodeHasNoParent_1, new Object[] {node}, indexOfLineStart); |
| } |
| parent.getChildren().add(node); |
| } else if (i > p) { |
| /* |
| * The node we just created is a child of the previous node. |
| * Add a new indentation level. |
| */ |
| lastNode.getChildren().add(node); |
| if (++indentationLevel == indentations.length) { |
| indentations = Arrays.copyOf(indentations, indentationLevel*2); |
| } |
| indentations[indentationLevel] = i; |
| } |
| } |
| lastNode = node; |
| indexOfLineStart = startNextLine; |
| } while (indexOfLineStart != length); |
| if (root == null) { |
| return null; |
| } |
| pos.setIndex(indexOfLineStart); |
| table.setRoot(root); |
| return table; |
| } |
| |
| /** |
| * Parses the given string using a format appropriate for the type of values in |
| * the given column, and stores the value in the given node. |
| * |
| * <p>This work is done in a separated method instead than inlined in the |
| * {@code parse(…)} method because of the {@code <V>} parametric value.</p> |
| * |
| * @param <V> the type of values in the given column. |
| * @param node the node in which to set the value. |
| * @param column the column in which to set the value. |
| * @param format the format to use for parsing the value, or {@code null}. |
| * @param text the textual representation of the value. |
| * @throws ParseException if an error occurred while parsing. |
| * @throws ClassCastException if the parsed value is not of the expected type. |
| */ |
| private <V> void parseValue(final TreeTable.Node node, final TableColumn<V> column, |
| final Format format, final String text) throws ParseException |
| { |
| final Object value; |
| if (format != null) { |
| value = format.parseObject(text); |
| } else { |
| value = text; |
| } |
| node.setValue(column, column.getElementType().cast(value)); |
| } |
| |
| /** |
| * Computes the {@code tree*} fields from the {@link #indentation} and |
| * {@link #verticalLinePosition} current values. |
| * |
| * @see #clearTreeSymbols() |
| */ |
| private void createTreeSymbols() { |
| 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 ? NO_BREAK_SPACE : '│'; |
| hc = NO_BREAK_SPACE; |
| } else { |
| // With a horizontal line |
| vc = (k & 1) == 0 ? '└' : '├'; |
| hc = '─'; |
| } |
| Arrays.fill(buffer, 0, verticalLinePosition, NO_BREAK_SPACE); |
| 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 the string to write before a node. |
| * |
| * @param isParent {@code true} for a parent node, or {@code false} for the actual node. |
| * @param isLast {@code true} if the node is the last children of its parent node. |
| */ |
| final String getTreeSymbols(final boolean isParent, final boolean isLast) { |
| return(isParent ? (isLast ? treeBlank : treeLine) |
| : (isLast ? treeEnd : treeCross)); |
| } |
| |
| /** |
| * Creates string representation of the node values. Tabulations are replaced by spaces, |
| * and line feeds are replaced by the Pilcrow character. This is necessary in order to |
| * avoid conflict with the characters expected by {@link TableAppender}. |
| * |
| * <p>Instances of {@link Writer} are created temporarily before to begin the formatting |
| * of a node, and discarded when the formatting is finished.</p> |
| */ |
| private final class Writer extends LineAppender { |
| /** |
| * Combination of {@link #nodeFilter} with other filter that may be specified by the tree table to format. |
| * The {@code TreeTable}-specific filter is specified by {@link TreeFormatCustomization}. |
| */ |
| private final Predicate<TreeTable.Node> filter; |
| |
| /** |
| * The columns to write. |
| */ |
| private final TableColumn<?>[] columns; |
| |
| /** |
| * The format to use for each column. |
| */ |
| private final Format[] formats; |
| |
| /** |
| * The node values to format. |
| */ |
| private final Object[] values; |
| |
| /** |
| * For each indentation level, {@code true} if the previous levels are writing the last node. |
| * This array will growth as needed. |
| */ |
| private boolean[] isLast; |
| |
| /** |
| * Whether to allows multi-lines cells instead than using Pilcrow character. |
| * This is currently supported only if the number of columns is less than 2. |
| */ |
| private final boolean multiLineCells; |
| |
| /** |
| * The node that have already been formatted. We use this map as a safety against infinite recursivity. |
| */ |
| private final Set<TreeTable.Node> recursivityGuard; |
| |
| /** |
| * The format for the column in process of being written. This is a format to use for the column as a whole. |
| * This field is updated for every new column to write. May be {@code null} if the format is unspecified. |
| */ |
| private transient Format columnFormat; |
| |
| /** |
| * Creates a new instance which will write to the given appendable. |
| * |
| * @param out where to format the tree. |
| * @param tree the tree table to format. |
| * @param columns the columns of the tree table to format. |
| * @param recursivityGuard an initially empty set. |
| */ |
| Writer(final Appendable out, final TreeTable tree, final TableColumn<?>[] columns, |
| final Set<TreeTable.Node> recursivityGuard) |
| { |
| super(columns.length >= 2 ? new TableAppender(out, "") : out); |
| multiLineCells = (super.out == out); |
| this.columns = columns; |
| this.formats = getFormats(columns, false); |
| this.values = new Object[columns.length]; |
| this.isLast = new boolean[8]; |
| this.recursivityGuard = recursivityGuard; |
| Predicate<TreeTable.Node> filter = nodeFilter; |
| if (tree instanceof TreeFormatCustomization) { |
| final TreeFormatCustomization custom = (TreeFormatCustomization) tree; |
| final Predicate<TreeTable.Node> more = custom.filter(); |
| if (more != null) { |
| filter = (filter != null) ? more.and(filter) : more; |
| } |
| } else { |
| } |
| this.filter = filter; |
| setTabulationExpanded(true); |
| setLineSeparator(multiLineCells ? TreeTableFormat.this.getLineSeparator() : " ¶ "); |
| } |
| |
| /** |
| * Localizes the given name in the display locale, or returns "(Unnamed)" if no localized value is found. |
| */ |
| private String toString(final GenericName name) { |
| final Locale locale = getDisplayLocale(); |
| if (name != null) { |
| final InternationalString i18n = name.toInternationalString(); |
| if (i18n != null) { |
| final String localized = i18n.toString(locale); |
| if (localized != null) { |
| return localized; |
| } |
| } |
| final String localized = name.toString(); |
| if (localized != null) { |
| return localized; |
| } |
| } |
| return '(' + Vocabulary.getResources(locale).getString(Vocabulary.Keys.Unnamed) + ')'; |
| } |
| |
| /** |
| * Appends a textual representation of the given value. |
| * |
| * @param value the value to format (may be {@code null}). |
| * @param recursive {@code true} if this method is invoking itself for writing collection values. |
| */ |
| private void formatValue(final Object value, final boolean recursive) throws IOException { |
| final CharSequence text; |
| if (value == null) { |
| text = " "; // String for missing value. |
| } else if (columnFormat != null) { |
| if (columnFormat instanceof CompoundFormat<?>) { |
| formatValue((CompoundFormat<?>) columnFormat, value); |
| return; |
| } |
| text = columnFormat.format(value); |
| } else if (value instanceof InternationalString) { |
| text = ((InternationalString) value).toString(getDisplayLocale()); |
| } else if (value instanceof CharSequence) { |
| text = value.toString(); |
| } else if (value instanceof CodeList<?>) { |
| text = MetadataServices.getInstance().getCodeTitle((CodeList<?>) value, getDisplayLocale()); |
| } else if (value instanceof Enum<?>) { |
| text = CharSequences.upperCaseToSentence(((Enum<?>) value).name()); |
| } else if (value instanceof Type) { |
| text = toString(((Type) value).getTypeName()); |
| } else if (value instanceof Locale) { |
| final Locale locale = getDisplayLocale(); |
| text = (locale != Locale.ROOT) ? ((Locale) value).getDisplayName(locale) : value.toString(); |
| } else if (value instanceof TimeZone) { |
| final Locale locale = getDisplayLocale(); |
| text = (locale != Locale.ROOT) ? ((TimeZone) value).getDisplayName(locale) : ((TimeZone) value).getID(); |
| } else if (value instanceof Charset) { |
| final Locale locale = getDisplayLocale(); |
| text = (locale != Locale.ROOT) ? ((Charset) value).displayName(locale) : ((Charset) value).name(); |
| } else if (value instanceof Currency) { |
| final Locale locale = getDisplayLocale(); |
| text = (locale != Locale.ROOT) ? ((Currency) value).getDisplayName(locale) : value.toString(); |
| } else if (value instanceof Record) { |
| formatCollection(((Record) value).getAttributes().values(), recursive); |
| return; |
| } else if (value instanceof Iterable<?>) { |
| formatCollection((Iterable<?>) value, recursive); |
| return; |
| } else if (value instanceof Object[]) { |
| formatCollection(Arrays.asList((Object[]) value), recursive); |
| return; |
| } else if (value instanceof Map.Entry<?,?>) { |
| final Map.Entry<?,?> entry = (Map.Entry<?,?>) value; |
| final Object k = entry.getKey(); |
| final Object v = entry.getValue(); |
| if (k == null) { |
| append(null); |
| } else { |
| formatValue(k, recursive); |
| } |
| if (v != null) { |
| append(" → "); |
| formatValue(v, recursive); |
| } |
| return; |
| } else { |
| /* |
| * Check for a value-by-value format only as last resort. If a column-wide format was specified by |
| * the 'columnFormat' field, that format should have been used by above code in order to produce a |
| * more uniform formatting. |
| */ |
| final Format format = getFormat(value.getClass()); |
| if (format instanceof DecimalFormat && Numbers.isFloat(value.getClass())) { |
| final double number = ((Number) value).doubleValue(); |
| if (number != (int) number) { // Cast to 'int' instead of 'long' as a way to limit to about 2E9. |
| /* |
| * The default floating point format uses only 3 fraction digits. We adjust that to the number |
| * of digits required by the number to format. We do that only if no NumberFormat was inferred |
| * for the whole column (in order to keep column format uniform). We use enough precision for |
| * all fraction digits except the last 2, in order to let DecimalFormat round the number. |
| */ |
| if (adaptableFormat == null) { |
| adaptableFormat = (DecimalFormat) format.clone(); |
| defaultPattern = adaptableFormat.toPattern(); |
| } |
| final int nf = DecimalFunctions.fractionDigitsForValue(number); |
| final boolean preferScientificNotation = (nf > 20 || nf < 7); // == (value < 1E-4 || value > 1E+9) |
| if (preferScientificNotation != usingScientificNotation) { |
| usingScientificNotation = preferScientificNotation; |
| adaptableFormat.applyPattern(preferScientificNotation ? "0.0############E0" : defaultPattern); |
| } |
| if (!preferScientificNotation) { |
| adaptableFormat.setMaximumFractionDigits(nf - 2); // All significand fraction digits except last two. |
| } |
| text = adaptableFormat.format(value); |
| } else { |
| text = format.format(value); |
| } |
| } else { |
| text = (format != null) ? format.format(value) : value.toString(); |
| } |
| } |
| append(text); |
| } |
| |
| /** |
| * Writes the values of the given collection. A maximum of 10 values will be written. |
| * If the collection contains other collections, the other collections will <strong>not</strong> |
| * be written recursively. |
| */ |
| private void formatCollection(final Iterable<?> values, final boolean recursive) throws IOException { |
| if (values != null) { |
| if (recursive) { |
| append('…'); // Do not format collections inside collections. |
| } else { |
| int count = 0; |
| for (final Object value : values) { |
| if (value != null) { |
| if (count != 0) append(", "); |
| formatValue(value, true); |
| if (++count == 10) { // Arbitrary limit. |
| append(", …"); |
| break; |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /** |
| * Work around for the inability to define the variable {@code <V>} locally. |
| */ |
| @Workaround(library="JDK", version="1.7") |
| private <V> void formatValue(final CompoundFormat<V> format, final Object value) throws IOException { |
| format.format(format.getValueType().cast(value), this); |
| } |
| |
| /** |
| * Appends the string representation of the given node and all its children. |
| * This method invokes itself recursively. |
| * |
| * @param node the node to format. |
| * @param level indentation level. The first level is 0. |
| */ |
| final void format(final TreeTable.Node node, final int level) throws IOException { |
| /* |
| * Draw the lines of the tree in the left margin for current row. |
| */ |
| for (int i=0; i<level; i++) { |
| out.append(getTreeSymbols(i != level-1, isLast[i])); |
| } |
| /* |
| * Fetch the values to write in current row, but do not write them now. We fetch values in advance in order |
| * to detect trailing null values, so we can avoid formatting trailing blank spaces. Note that a null value |
| * may be followed by a non-null value, which is why we need to check all of them before to know how many |
| * columns to omit. |
| */ |
| for (int i=0; i<columns.length; i++) { |
| values[i] = node.getValue(columns[i]); |
| } |
| int n = values.length - 1; |
| if (omitTrailingNulls) { |
| while (n > 0 && values[n] == null) n--; |
| } |
| /* |
| * Format the values that we fetched in above loop. |
| */ |
| for (int i=0; i<=n; i++) { |
| if (i != 0) { |
| // We have a TableAppender instance if and only if there is 2 or more columns. |
| writeColumnSeparator(i, (TableAppender) out); |
| } |
| columnFormat = formats[i]; |
| formatValue(values[i], false); |
| clear(); |
| } |
| out.append(lineSeparator); |
| if (level >= isLast.length) { |
| isLast = Arrays.copyOf(isLast, level*2); |
| } |
| /* |
| * Format the children only if we do not detect an infinite recursivity. Our recursivity detection |
| * algorithm assumes that the Node.equals(Object) method has been implemented as specified in its javadoc. |
| * In particular, the implementation may compare the values and children but shall not compare the parent. |
| * |
| * We skip the check for the particular case of DefaultTreeTable.Node implementation because it performs |
| * a real check of values and children, which is a little bit costly and known to be unnecessary in that |
| * particular case. |
| */ |
| final boolean omitCheck = node.getClass().isAnnotationPresent(Acyclic.class); |
| if (omitCheck || recursivityGuard.add(node)) { |
| boolean needLineSeparator = multiLineCells; |
| final String lineSeparator = needLineSeparator ? getLineSeparator() : null; |
| final Iterator<? extends TreeTable.Node> it = node.getChildren().iterator(); |
| TreeTable.Node next = next(it); |
| while (next != null) { |
| final TreeTable.Node child = next; |
| next = next(it); |
| needLineSeparator |= (isLast[level] = (next == null)); |
| if (needLineSeparator && lineSeparator != null) { |
| setLineSeparator(lineSeparator + getTreeSymbols(true, isLast[level])); |
| } |
| format(child, level+1); // 'isLast' must be set before to call this method. |
| } |
| if (lineSeparator != null) { |
| setLineSeparator(lineSeparator); // Restore previous state. |
| } |
| if (!omitCheck && !recursivityGuard.remove(node)) { |
| /* |
| * Assuming that Node.hashCode() and Node.equals(Object) implementation are not broken, |
| * this exception may happen only if the node content changed during this method execution. |
| */ |
| throw new ConcurrentModificationException(); |
| } |
| } else { |
| /* |
| * Detected a recursivity. Format "(cycle omitted)" just below the node. |
| */ |
| for (int i=0; i<level; i++) { |
| out.append(getTreeSymbols(true, isLast[i])); |
| } |
| final Locale locale = getDisplayLocale(); |
| out.append(treeBlank).append('(').append(Vocabulary.getResources(locale) |
| .getString(Vocabulary.Keys.CycleOmitted).toLowerCase(locale)) |
| .append(')').append(lineSeparator); |
| } |
| } |
| |
| /** |
| * Returns the next filtered element from the given iterator, or {@code null} if none. |
| * The filter applied by this method combines {@link #getNodeFilter()} with the filter |
| * returned by {@link TreeFormatCustomization#filter()}. |
| */ |
| private TreeTable.Node next(final Iterator<? extends TreeTable.Node> it) { |
| while (it.hasNext()) { |
| final TreeTable.Node next = it.next(); |
| if (next != null) { |
| if (filter == null || filter.test(next)) { |
| return next; |
| } |
| } |
| } |
| return null; |
| } |
| } |
| |
| /** |
| * Writes a graphical representation of the specified tree table in the given stream or buffer. |
| * This method iterates recursively over all {@linkplain TreeTable.Node#getChildren() children}. |
| * For each {@linkplain #getColumns() column to format} in each node, this method gets a textual |
| * representation of the {@linkplain TreeTable.Node#getValue(TableColumn) value in that column} |
| * using the formatter obtained by a call to {@link #getFormat(Class)}. |
| * |
| * @param tree the tree to format. |
| * @param toAppendTo where to format the tree. |
| * @throws IOException if an error occurred while writing to the given appendable. |
| * |
| * @see TreeTables#toString(TreeTable) |
| */ |
| @Override |
| public void format(final TreeTable tree, final Appendable toAppendTo) throws IOException { |
| ArgumentChecks.ensureNonNull("tree", tree); |
| if (treeBlank == null) { |
| createTreeSymbols(); |
| } |
| TableColumn<?>[] columns; |
| if (columnIndices != null) { |
| columns = DefaultTreeTable.getColumns(columnIndices); |
| } else { |
| final List<TableColumn<?>> c = tree.getColumns(); |
| columns = c.toArray(new TableColumn<?>[c.size()]); |
| } |
| if (recursivityGuard == null) { |
| recursivityGuard = new HashSet<>(); |
| } |
| try { |
| final Writer out = new Writer(toAppendTo, tree, columns, recursivityGuard); |
| out.format(tree.getRoot(), 0); |
| out.flush(); |
| } finally { |
| recursivityGuard.clear(); |
| } |
| } |
| |
| /** |
| * Creates a new format to use for parsing and formatting values of the given type. |
| * This method is invoked by the first time that a format is needed for the given type. |
| * Subclasses can override this method if they want to configure the way dates, numbers |
| * or other objects are formatted. |
| * See {@linkplain org.apache.sis.io.CompoundFormat#createFormat(Class) parent class documentation} |
| * for more information. |
| * |
| * <p>The implementation in {@code TreeTableFormat} differs from the default implementation |
| * in the following aspects:</p> |
| * <ul> |
| * <li>{@code UnitFormat} uses {@link UnitFormat.Style#NAME}.</li> |
| * </ul> |
| * |
| * @param valueType the base type of values to parse or format. |
| * @return the format to use for parsing of formatting values of the given type, or {@code null} if none. |
| */ |
| @Override |
| protected Format createFormat(final Class<?> valueType) { |
| final Format format = super.createFormat(valueType); |
| if (format instanceof UnitFormat) { |
| ((UnitFormat) format).setStyle(UnitFormat.Style.NAME); |
| } |
| return format; |
| } |
| |
| /** |
| * Writes characters between columns. The default implementation applies the configuration |
| * specified by {@link #setColumnSeparatorPattern(String)} as below: |
| * |
| * <blockquote><code> |
| * out.append({@linkplain #beforeFill beforeFill}); |
| * out.nextColumn({@linkplain #fillCharacter fillCharacter}); |
| * out.append({@linkplain #columnSeparator columnSeparator}); |
| * </code></blockquote> |
| * |
| * The output with default values is like below: |
| * |
| * {@preformat text |
| * root |
| * └─column0…… column1…… column2…… column3 |
| * } |
| * |
| * Subclasses can override this method if different column separators are desired. |
| * Note however that doing so may prevent the {@link #parse parse(…)} method to work. |
| * |
| * @param nextColumn zero-based index of the column to be written after the separator. |
| * @param out where to write the column separator. |
| * |
| * @see TableAppender#nextColumn(char) |
| * |
| * @since 1.0 |
| */ |
| protected void writeColumnSeparator(final int nextColumn, final TableAppender out) { |
| out.append(beforeFill); |
| out.nextColumn(fillCharacter); |
| out.append(columnSeparator); |
| } |
| |
| /** |
| * Returns a clone of this format. |
| * |
| * @return a clone of this format. |
| */ |
| @Override |
| public TreeTableFormat clone() { |
| final TreeTableFormat c = (TreeTableFormat) super.clone(); |
| c.recursivityGuard = null; |
| return c; |
| } |
| } |