Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git

Hide definition item, add unit test, split interpolation from YAML parsing

AuthorDaveJarvis <email>
Date2020-06-07 20:33:21 GMT-0700
Commitfbb8e605af0e313fbf1445a717efc5ab116c6846
Parent5d6dd09
Delta308 lines added, 393 lines removed, 85-line decrease
src/test/java/com/scrivenvar/definition/TreeItemInterpolatorTest.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivenvar.definition;
+
+import javafx.scene.control.TreeItem;
+import org.junit.jupiter.api.Test;
+
+import static java.lang.String.format;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TreeItemInterpolatorTest {
+
+ private final static String AUTHOR_FIRST = "FirstName";
+ private final static String AUTHOR_LAST = "LastName";
+ private final static String AUTHOR_ALL = "$root.name.first$ $root.name.last$";
+
+ /**
+ * Test that a hierarchical relationship of {@link TreeItem} instances can
+ * create a flat map with all string values containing key names interpolated.
+ */
+ @Test
+ public void test_Resolve_ReferencesInTree_InterpolatedMap() {
+ final var root = new TreeItem<>( "root" );
+ final var name = new TreeItem<>( "name" );
+ final var first = new TreeItem<>( "first" );
+ final var authorFirst = new TreeItem<>( AUTHOR_FIRST );
+ final var last = new TreeItem<>( "last" );
+ final var authorLast = new TreeItem<>( AUTHOR_LAST );
+ final var full = new TreeItem<>( "full" );
+ final var expr = new TreeItem<>( AUTHOR_ALL );
+
+ root.getChildren().add( name );
+ name.getChildren().add( first );
+ name.getChildren().add( last );
+ name.getChildren().add( full );
+
+ first.getChildren().add( authorFirst );
+ last.getChildren().add( authorLast );
+ full.getChildren().add( expr );
+
+ final var map = TreeItemInterpolator.toMap( root );
+
+ var actualAuthor = map.get( "$root.name.full$" );
+ var expectedAuthor = AUTHOR_ALL;
+ assertEquals( expectedAuthor, actualAuthor );
+
+ TreeItemInterpolator.interpolate( map );
+ actualAuthor = map.get( "$root.name.full$" );
+
+ expectedAuthor = format( "%s %s", AUTHOR_FIRST, AUTHOR_LAST );
+ assertEquals( expectedAuthor, actualAuthor );
+ }
+}
src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
package com.scrivenvar.definition.yaml;
-import com.fasterxml.jackson.core.ObjectCodec;
-import com.fasterxml.jackson.core.io.IOContext;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
-import com.fasterxml.jackson.databind.node.NullNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
-import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.scrivenvar.Messages;
-import com.scrivenvar.decorators.VariableDecorator;
-import com.scrivenvar.decorators.YamlVariableDecorator;
import com.scrivenvar.definition.DocumentParser;
-import org.yaml.snakeyaml.DumperOptions;
-import java.io.IOException;
import java.io.InputStream;
-import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import static com.scrivenvar.Constants.DEFAULT_MAP_SIZE;
import static com.scrivenvar.Constants.STATUS_BAR_OK;
/**
- * <p>
- * This program loads a YAML document into memory, scans for variable
- * declarations, then substitutes any self-referential values back into the
- * document. Its output is the given YAML document without any variables.
- * Variables in the YAML document are denoted using a bracketed dollar symbol
- * syntax. For example: $field.name$. Some nomenclature to keep from going
- * squirrely, consider:
- * </p>
- *
- * <pre>
- * root:
- * node:
- * name: $field.name$
- * field:
- * name: Alan Turing
- * </pre>
- * <p>
- * The various components of the given YAML are called:
- *
- * <ul>
- * <li><code>$field.name$</code> - delimited reference</li>
- * <li><code>field.name</code> - reference</li>
- * <li><code>name</code> - YAML field</li>
- * <li><code>Alan Turing</code> - (dereferenced) field value</li>
- * </ul>
+ * Responsible for reading a YAML document into an object hierarchy.
*
* @author White Magic Software, Ltd.
*/
public class YamlParser implements DocumentParser<JsonNode> {
-
- private final static VariableDecorator VARIABLE_DECORATOR
- = new YamlVariableDecorator();
-
- /**
- * Separates YAML variable nodes (e.g., the dots in
- * <code>$root.node.var$</code>).
- */
- public static final String SEPARATOR = ".";
-
- /**
- * Should be JsonPointer.SEPARATOR, but Jackson YAML uses magic values.
- */
- private final static char SEPARATOR_YAML = '/';
-
- private final static int GROUP_DELIMITED = 1;
- private final static int GROUP_REFERENCE = 2;
-
- /**
- * Compiled regular expression for matching delimited references.
- */
- private final static Pattern REGEX_PATTERN
- = Pattern.compile( YamlVariableDecorator.REGEX );
-
- /**
- * Flat map of references to dereferenced field values.
- */
- private final Map<String, String> mReferences =
- new HashMap<>( DEFAULT_MAP_SIZE );
/**
assert path != null;
mDocumentRoot = parse( path );
- interpolate( mDocumentRoot );
- }
-
- /**
- * Parses the given path containing YAML data into an object hierarchy.
- *
- * @param path {@link Path} to the YAML resource to parse.
- * @return The parsed contents, or an empty object hierarchy.
- */
- private JsonNode parse( final Path path ) {
- try( final InputStream in = Files.newInputStream( path ) ) {
- return parse( in );
- } catch( final Exception e ) {
- setError( Messages.get( "yaml.error.open" ) );
-
- // Ensure that a document root node exists by relying on the
- // default failure condition when processing. This is required
- // because the input stream could not be read.
- return new ObjectMapper().createObjectNode();
- }
}
public JsonNode getDocumentRoot() {
return mDocumentRoot;
- }
-
- /**
- * Returns the given string with all the delimited references swapped with
- * their recursively resolved values.
- *
- * @param text The text to parse with zero or more delimited references to
- * replace.
- * @return The substituted value.
- */
- public String substitute( String text ) {
- final Matcher matcher = patternMatch( text );
- final Map<String, String> map = getReferences();
-
- while( matcher.find() ) {
- final String key = matcher.group( GROUP_DELIMITED );
- final String value = map.getOrDefault( key, key );
-
- text = text.replace( key, value );
- }
-
- return text;
- }
-
- /**
- * Returns all the strings with their values resolved in a flat hierarchy.
- * This copies all the keys and resolved values into a new map.
- *
- * @return The new map created with all values having been resolved,
- * recursively.
- */
- public Map<String, String> createResolvedMap() {
- final Map<String, String> map = new HashMap<>( DEFAULT_MAP_SIZE );
-
- resolve( getDocumentRoot(), "", map );
-
- return map;
- }
-
- /**
- * Iterate over a given root node (at any level of the tree) and adapt each
- * leaf node.
- *
- * @param rootNode A JSON node (YAML node) to adapt.
- * @param map Container that associates definitions with values.
- */
- private void resolve(
- final JsonNode rootNode,
- final String path,
- final Map<String, String> map ) {
-
- if( rootNode != null ) {
- rootNode.fields().forEachRemaining(
- ( Entry<String, JsonNode> leaf ) -> resolve( leaf, path, map )
- );
- }
- }
-
- /**
- * Look up the value for a given node.
- *
- * @param root The node to resolve (contains a key to find).
- * @param path The path to the node.
- * @param map Flat map of existing key/value pairs.
- */
- private void resolve(
- final Entry<String, JsonNode> root,
- final String path,
- final Map<String, String> map ) {
- final JsonNode leaf = root.getValue();
- final String key = root.getKey();
-
- if( leaf.isValueNode() ) {
- map.put(
- VARIABLE_DECORATOR.decorate( path + key ),
- substitute(
- leaf instanceof NullNode ? "" : leaf.asText()
- )
- );
- }
- else if( leaf.isObject() ) {
- resolve( leaf, path + key + SEPARATOR, map );
- }
- }
-
- /**
- * Reads the first document from the given stream of YAML data and returns a
- * corresponding object that represents the YAML hierarchy. The calling class
- * is responsible for closing the stream. Calling classes should use
- * <code>JsonNode.fields()</code> to walk through the YAML tree of fields.
- *
- * @param in The input stream containing YAML content.
- */
- private JsonNode parse( final InputStream in ) throws IOException {
- setError( Messages.get( STATUS_BAR_OK ) );
-
- final YAMLFactory factory = new ResolverYamlFactory();
- final ObjectMapper mapper = new ObjectMapper( factory );
- return mapper.readTree( in );
- }
-
- /**
- * Iterate over a given root node (at any level of the tree) and process each
- * leaf node.
- *
- * @param root A node to process.
- */
- private void interpolate( final JsonNode root ) {
- root.fields().forEachRemaining( this::interpolate );
- }
-
- /**
- * Process the given field, which is a named node. This is where the
- * application does the up-front work of mapping references to their fully
- * recursively dereferenced values.
- *
- * @param field The named node.
- */
- private void interpolate( final Entry<String, JsonNode> field ) {
- final JsonNode node = field.getValue();
-
- if( node.isObject() ) {
- interpolate( node );
- }
- else {
- final JsonNode fieldValue = field.getValue();
-
- // Only basic data types can be parsed into variable values. For
- // node structures, YAML has a built-in mechanism.
- if( fieldValue.isValueNode() ) {
- try {
- resolve( fieldValue.asText() );
- } catch( final StackOverflowError e ) {
- final String msg = Messages.get(
- "yaml.error.unresolvable", node.textValue(), fieldValue );
- setError( msg );
- }
- }
- }
- }
-
- /**
- * Inserts the delimited references and field values into the cache. This will
- * overwrite existing references.
- *
- * @param fieldValue YAML field containing zero or more delimited references.
- * If it contains a delimited reference, the parameter is
- * modified with the
- * dereferenced value before it is returned.
- * @return fieldValue without delimited references.
- */
- private String resolve( String fieldValue ) {
- final Matcher matcher = patternMatch( fieldValue );
-
- while( matcher.find() ) {
- final String delimited = matcher.group( GROUP_DELIMITED );
- final String reference = matcher.group( GROUP_REFERENCE );
- final String dereference = resolve( lookup( reference ) );
-
- fieldValue = fieldValue.replace( delimited, dereference );
-
- // This will perform some superfluous calls by overwriting existing
- // items in the delimited reference map.
- put( delimited, dereference );
- }
-
- return fieldValue;
- }
-
- /**
- * Inserts a key/value pair into the references map. The map retains
- * references and dereferenced values found in the YAML. If the reference
- * already exists, this will overwrite with a new value.
- *
- * @param delimited The variable name.
- * @param dereferenced The resolved value.
- */
- private void put( final String delimited, final String dereferenced ) {
- if( dereferenced.isEmpty() ) {
- missing( delimited );
- }
- else {
- getReferences().put( delimited, dereferenced );
- }
- }
-
- /**
- * Called when a delimited reference is dereferenced to an empty string. This
- * should produce a warning for the user.
- *
- * @param delimited Delimited reference with no derived value.
- */
- private void missing( final String delimited ) {
- setError( Messages.get( "yaml.error.missing", delimited ) );
- }
-
- /**
- * Returns a REGEX_PATTERN matcher for the given text.
- *
- * @param text The text that contains zero or more instances of a
- * REGEX_PATTERN that can be found using the regular expression.
- */
- private Matcher patternMatch( final String text ) {
- return REGEX_PATTERN.matcher( text );
- }
-
- /**
- * Finds the YAML value for a reference.
- *
- * @param reference References a value in the YAML document.
- * @return The dereferenced value.
- */
- private String lookup( final String reference ) {
- return getDocumentRoot().at( asPath( reference ) ).asText();
}
/**
- * Converts a reference (not delimited) to a path that can be used to find a
- * value that should exist inside the YAML document.
+ * Parses the given path containing YAML data into an object hierarchy.
*
- * @param reference The reference to convert to a YAML document path.
- * @return The reference with a leading slash and its separator characters
- * converted to slashes.
- */
- private String asPath( final String reference ) {
- return SEPARATOR_YAML + reference.replace(
- getDelimitedSeparator(), SEPARATOR_YAML );
- }
-
- /**
- * @return The list of references mapped to dereferenced values.
+ * @param path {@link Path} to the YAML resource to parse.
+ * @return The parsed contents, or an empty object hierarchy.
*/
- private Map<String, String> getReferences() {
- return mReferences;
- }
-
- private final class ResolverYamlFactory extends YAMLFactory {
-
- private static final long serialVersionUID = 1L;
-
- @Override
- protected YAMLGenerator _createGenerator(
- final Writer out, final IOContext ctxt ) throws IOException {
-
- return new ResolverYamlGenerator(
- ctxt, _generatorFeatures, _yamlGeneratorFeatures, _objectCodec,
- out, _version );
- }
- }
-
- private class ResolverYamlGenerator extends YAMLGenerator {
+ private JsonNode parse( final Path path ) {
+ try( final InputStream in = Files.newInputStream( path ) ) {
+ setError( Messages.get( STATUS_BAR_OK ) );
- public ResolverYamlGenerator(
- final IOContext ctxt,
- final int jsonFeatures,
- final int yamlFeatures,
- final ObjectCodec codec,
- final Writer out,
- final DumperOptions.Version version ) throws IOException {
- super( ctxt, jsonFeatures, yamlFeatures, codec, out, version );
- }
+ return new ObjectMapper( new YAMLFactory() ).readTree( in );
+ } catch( final Exception e ) {
+ setError( Messages.get( "yaml.error.open" ) );
- @Override
- public void writeString( final String text ) throws IOException {
- super.writeString( substitute( text ) );
+ // Ensure that a document root node exists by relying on the
+ // default failure condition when processing. This is required
+ // because the input stream could not be read.
+ return new ObjectMapper().createObjectNode();
}
- }
-
- /**
- * Returns the character used to separate YAML paths within delimited
- * references. This will return only the first character of the command line
- * parameter, if the default is overridden.
- *
- * @return A period by default.
- */
- private char getDelimitedSeparator() {
- return SEPARATOR.charAt( 0 );
}
*/
public String getError() {
- return mError == null ? "" : mError;
+ return mError;
}
}
src/main/java/com/scrivenvar/definition/yaml/YamlTreeAdapter.java
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
+import com.scrivenvar.definition.RootTreeItem;
import com.scrivenvar.definition.TreeAdapter;
import com.scrivenvar.definition.VariableTreeItem;
import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
import java.io.IOException;
public TreeItem<String> adapt( final String root ) {
final JsonNode rootNode = getYamlParser().getDocumentRoot();
- final TreeItem<String> rootItem = createTreeItem( root );
+ final TreeItem<String> rootItem = createRootTreeItem( root );
rootItem.setExpanded( true );
/**
- * Creates a new tree item that can be added to the tree view.
+ * Creates a new {@link TreeItem} that can be added to the {@link TreeView}.
*
* @param value The node's value.
- * @return A new tree item node, never null.
+ * @return A new {@link TreeItem}, never {@code null}.
*/
private TreeItem<String> createTreeItem( final String value ) {
return new VariableTreeItem<>( value );
+ }
+
+ /**
+ * Creates a new {@link TreeItem} that is intended to be the root-level item
+ * added to the {@link TreeView}. This allows the root item to be
+ * distinguished from the other items so that reference keys do not include
+ * "Definition" as part of their name.
+ *
+ * @param value The node's value.
+ * @return A new {@link TreeItem}, never {@code null}.
+ */
+ private TreeItem<String> createRootTreeItem( final String value ) {
+ return new RootTreeItem<>( value );
}
src/main/java/com/scrivenvar/definition/TreeItemInterpolator.java
+/*
+ * Copyright 2020 White Magic Software, Ltd.
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * o Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * o Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.scrivenvar.definition;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.scrivenvar.decorators.YamlVariableDecorator;
+import com.scrivenvar.preview.HTMLPreviewPane;
+import javafx.scene.control.TreeItem;
+import javafx.scene.control.TreeView;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Stack;
+import java.util.regex.Matcher;
+
+import static com.scrivenvar.Constants.DEFAULT_MAP_SIZE;
+import static com.scrivenvar.decorators.YamlVariableDecorator.REGEX_PATTERN;
+
+/**
+ * Given a {@link TreeItem}, this will generate a flat map with all the
+ * values in the tree recursively interpolated. The application integrates
+ * definition files as follows:
+ * <ol>
+ * <li>Load YAML file into {@link JsonNode} hierarchy.</li>
+ * <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li>
+ * <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li>
+ * <li>Substitute flat map variables into document as required.</li>
+ * </ol>
+ *
+ * <p>
+ * This class is responsible for producing the interpolated flat map. This
+ * allows dynamic edits of the {@link TreeView} to be displayed in the
+ * {@link HTMLPreviewPane} without having to reload the definition file.
+ * Reloading the definition file would work, but has a number of drawbacks.
+ * </p>
+ *
+ * @author White Magic Software, Ltd.
+ */
+public class TreeItemInterpolator {
+ /**
+ * Separates YAML variable nodes (e.g., the dots in {@code $root.node.var$]).
+ */
+ public static final String SEPARATOR = ".";
+
+ private final static int GROUP_DELIMITED = 1;
+
+ /**
+ * Default buffer length for keys ({@link StringBuilder} has 16 character
+ * buffer) that should be large enough for most keys to avoid reallocating
+ * memory to increase the {@link StringBuilder}'s buffer.
+ */
+ public static final int DEFAULT_KEY_LENGTH = 64;
+
+ /**
+ * In-order traversal of a {@link TreeItem} hierarchy, exposing each item
+ * as a consecutive list.
+ */
+ private static final class TreeIterator
+ implements Iterator<TreeItem<String>> {
+ private final Stack<TreeItem<String>> mStack = new Stack<>();
+
+ public TreeIterator( final TreeItem<String> root ) {
+ if( root != null ) {
+ mStack.push( root );
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ return !mStack.isEmpty();
+ }
+
+ @Override
+ public TreeItem<String> next() {
+ final TreeItem<String> next = mStack.pop();
+ next.getChildren().forEach( mStack::push );
+
+ return next;
+ }
+ }
+
+ private TreeItemInterpolator() {
+ }
+
+ /**
+ * Iterate over a given root node (at any level of the tree) and process each
+ * leaf node into a flat map. Values must be interpolated separately.
+ */
+ public static Map<String, String> toMap( final TreeItem<String> root ) {
+ final Map<String, String> map = new HashMap<>( DEFAULT_MAP_SIZE );
+ final TreeIterator iterator = new TreeIterator( root );
+
+ iterator.forEachRemaining( item -> {
+ if( item.isLeaf() ) {
+ map.put( toPath( item.getParent() ), item.getValue() );
+ }
+ } );
+
+ return map;
+ }
+
+ /**
+ * Performs string interpolation on the values in the given map. This will
+ * change any value in the map that contains a variable that matches
+ * {@link YamlVariableDecorator#REGEX_PATTERN}.
+ *
+ * @param map Contains values that represent references to keys.
+ */
+ public static void interpolate( final Map<String, String> map ) {
+ map.replaceAll( ( k, v ) -> resolve( map, v ) );
+ }
+
+ /**
+ * Given a value with zero or more key references, this will resolve all
+ * the values, recursively. If a key cannot be dereferenced, the value will
+ * contain the key name.
+ *
+ * @param map Map to search for keys when resolving key references.
+ * @param value Value containing zero or more key references
+ * @return The given value with all embedded key references interpolated.
+ */
+ private static String resolve(
+ final Map<String, String> map, String value ) {
+ final Matcher matcher = REGEX_PATTERN.matcher( value );
+
+ while( matcher.find() ) {
+ final String keyName = matcher.group( GROUP_DELIMITED );
+
+ final String keyValue = resolve(
+ map, map.getOrDefault( keyName, keyName )
+ );
+
+ value = value.replace( keyName, keyValue );
+ }
+
+ return value;
+ }
+
+ /**
+ * For a given node, this will ascend the tree to generate a key name
+ * that is associated with the leaf node's value.
+ *
+ * @param node Ascendants represent the key to this node's value.
+ * @param <T> Data type that the {@link TreeItem} contains.
+ * @return The string representation of the node's unique key.
+ */
+ public static <T> String toPath( TreeItem<T> node ) {
+ assert node != null;
+
+ final StringBuilder key = new StringBuilder( DEFAULT_KEY_LENGTH );
+ final Stack<TreeItem<T>> stack = new Stack<>();
+
+ while( node != null && !(node instanceof RootTreeItem) ) {
+ stack.push( node );
+ node = node.getParent();
+ }
+
+ // Gets set at end of first iteration (to avoid an if condition).
+ String separator = "";
+
+ while( !stack.empty() ) {
+ final T subkey = stack.pop().getValue();
+ key.append( separator );
+ key.append( subkey );
+ separator = SEPARATOR;
+ }
+
+ return YamlVariableDecorator.entoken( key.toString() );
+ }
+}
src/main/java/com/scrivenvar/definition/VariableTreeItem.java
import static com.scrivenvar.definition.FindMode.CONTAINS;
import static com.scrivenvar.definition.FindMode.STARTS_WITH;
-import static com.scrivenvar.definition.yaml.YamlParser.SEPARATOR;
import static java.text.Normalizer.Form.NFD;
/**
* Provides behaviour afforded to variable names and their corresponding value.
*
* @param <T> The type of TreeItem (usually String).
* @author White Magic Software, Ltd.
*/
public class VariableTreeItem<T> extends TreeItem<T> {
- public static final int DEFAULT_MAX_VAR_LENGTH = 64;
/**
*/
public String toPath() {
- final Stack<TreeItem<T>> stack = new Stack<>();
- TreeItem<T> node = this;
-
- while( node.getParent() != null ) {
- stack.push( node );
- node = node.getParent();
- }
-
- final StringBuilder sb = new StringBuilder( DEFAULT_MAX_VAR_LENGTH );
-
- while( !stack.isEmpty() ) {
- node = stack.pop();
-
- if( !node.isLeaf() ) {
- sb.append( node.getValue() );
-
- // This will add a superfluous separator, but instead of peeking at
- // the stack all the time, the last separator will be removed outside
- // the loop (one operation executed once).
- sb.append( SEPARATOR );
- }
- }
-
- // Remove the trailing SEPARATOR.
- if( sb.length() > 0 ) {
- sb.setLength( sb.length() - 1 );
- }
-
- return sb.toString();
+ return TreeItemInterpolator.toPath( getParent() );
}
}