Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
/*
 * 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 static com.scrivenvar.Constants.DEFAULT_MAP_SIZE;

/**
 * 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>
 */
public class TreeItemAdapter {
  /**
   * Separates YAML variable nodes (e.g., the dots in {@code $root.node.var$}).
   */
  public static final String SEPARATOR = ".";

  /**
   * 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 TreeItemAdapter() {
  }

  /**
   * 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;
  }


  /**
   * 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() );
  }
}