Package org.uispec4j

Source Code of org.uispec4j.Tree

package org.uispec4j;

import org.uispec4j.assertion.Assertion;
import org.uispec4j.assertion.testlibrairies.AssertAdapter;
import org.uispec4j.utils.ArrayUtils;
import org.uispec4j.utils.ColorUtils;
import org.uispec4j.utils.Utils;

import javax.accessibility.AccessibleComponent;
import javax.accessibility.AccessibleContext;
import javax.swing.*;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.util.*;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Wrapper for JTree components.<p>
* The nodes of a tree are identified using stringified paths. For instance, for the tree
* below:
* <pre><code>
* root
*   |
*   +- child
*   |  |
*   |  +- subChild
*   |
*   +- otherChild
*      |
*      +- otherSubChild
* </code></pre>
* the "subChild" element is identified with the following path:
* <pre><code>
*   child/subChild
* </code></pre>
* Note that when defining paths the root element name is always omitted. The root
* node path itself is denoted by an empty string ("").<p>
* The default path separator is "/". However, this separator can be customized as follows:
* <ul>
* <li>By setting it on a given Tree instance using {@link Tree#setSeparator(String)}</li>
* <li>By setting it on all new Tree instances using {@link Tree#setDefaultSeparator(String)}</li>
* <li>By setting it on all new Tree instances using the <code>uispec4j.tree.separator</code>
* property.</li>
* </ul>
* When using paths, it is also possible to use substrings from the displayed node names.
* For instance, instead of writing:
* <pre><code>
*   otherChild/otherSubChild
* </code></pre>
* one can write:
* <pre><code>
*   other/sub
* </code></pre>
* <p/>
* The contents of the tree can be checked with {@link #contentEquals(String)},
* which is used as follows:
* <pre><code>
* assertTrue(jTree.contentEquals("root\n" +
*                                "  child1\n" +
*                                "    child1_1\n" +
*                                "  child2"));
* </code></pre>
* <p/>
* The conversion between the values (Strings) given in the tests and the values
* actually displayed by the JTree renderer is performed by a dedicated
* {@link TreeCellValueConverter}, which retrieves the graphical component that draws
* the tree nodes and determines the displayed value accordingly.
* A {@link DefaultTreeCellValueConverter} is used by default by the Tree component.
*/
public class Tree extends AbstractSwingUIComponent {

  public static final String TYPE_NAME = "tree";
  public static final Class[] SWING_CLASSES = {JTree.class};
  static final String SEPARATOR_PROPERTY = "uispec4j.tree.separator";

  private JTree jTree;
  static String defaultSeparator = "/";
  private String separator;
  private TreeCellValueConverter cellValueConverter = new DefaultTreeCellValueConverter();

  private static final Pattern COLOR_PROPERTY_PATTERN =
    Pattern.compile(" #\\(.*color=([\\w]+)\\)");

  public Tree(JTree jTree) {
    this.jTree = jTree;
    this.separator = initSeparator();
  }

  private static String initSeparator() {
    String property = System.getProperty(Tree.SEPARATOR_PROPERTY);
    if ((property != null) && (property.length() > 0)) {
      return property;
    }
    return Tree.defaultSeparator;
  }

  public String getDescriptionTypeName() {
    return TYPE_NAME;
  }

  public JTree getAwtComponent() {
    return jTree;
  }

  /**
   * Returns the JTree wrapped by this component.
   */
  public JTree getJTree() {
    return jTree;
  }

  /**
   * Sets the separator to be used for specifying node paths in this jTree instance.
   */
  public void setSeparator(String separator) {
    checkSeparator(separator);
    this.separator = separator;
  }

  private static void checkSeparator(String separator) {
    if (separator == null) {
      throw new IllegalArgumentException("Separator must not be null");
    }
    else if (separator.length() == 0) {
      throw new IllegalArgumentException("Separator must not be empty");
    }
  }

  /**
   * Returns the separator currently used for specifying node paths in this jTree instance.
   */
  public String getSeparator() {
    return separator;
  }

  /**
   * Sets the separator to be used for specifying node paths in new jTree instances.
   */
  public static void setDefaultSeparator(String separator) {
    checkSeparator(separator);
    defaultSeparator = separator;
  }

  /**
   * Sets a new converter for retrieving the text displayed on the tree cells.
   */
  public void setCellValueConverter(TreeCellValueConverter converter) {
    this.cellValueConverter = converter;
  }

  /**
   * Checks the nodes structure displayed by the jTree.<p>
   * The expected contents is a newline (\n) separated string where nodes are
   * indented with two-space steps.
   * For instance:
   * <code><pre>
   * assertTrue(jTree.contentEquals("root\n" +
   *                                "  child1\n" +
   *                                "    child1_1\n" +
   *                                "  child2"));
   * </pre></code>
   * Text display properties such as boldness and color can be checked using a "#(...)"
   * specifier.
   * For instance:
   * <code><pre>
   * assertTrue(jTree.contentEquals("root\n" +
   *                                "  child1 #(bold)\n" +
   *                                "    child1_1 #(bold,color=red)\n" +
   *                                "  child2"));
   * </pre></code>
   * The properties are defined as follows:
   * <ul>
   * <li>The "bold" property must be present if and only if the node text is bold</li>
   * <li>The "color" property value can be numeric ("0000ee") or approximative ("blue")
   * (see the <a href="http://www.uispec4j.org/colors">Using colors</a> page
   * for more information)</li>
   * <li>The "bold" property, if present, must be placed before the "color" property</li>
   * </ul>
   */
  public Assertion contentEquals(final String expectedContents) {
    return new Assertion() {
      public void check() {
        String trimmedExpected = expectedContents.trim();
        AssertAdapter.assertTrue("Expected tree description should not be empty",
                                 (trimmedExpected != null) && (trimmedExpected.length() > 0));
        checkContents(trimmedExpected);
      }
    };
  }

  /**
   * Checks that a node identified by the given path is present in the jTree.
   */
  public Assertion contains(final String path) {
    return new Assertion() {
      public void check() {
        getTreePath(path);
      }
    };
  }

  /**
   * Selects the root node of the jTree.
   */
  public void selectRoot() {
    jTree.setSelectionPath(new TreePath(jTree.getModel().getRoot()));
  }

  /**
   * Expands the current jTree selection with a given node.
   */
  public void addToSelection(String path) {
    jTree.addSelectionPath(getTreePath(path));
  }

  /**
   * Removes the given node from the current jTree selection.
   */
  public void removeFromSelection(String path) {
    TreePath jTreePath = getTreePath(path);
    jTree.removeSelectionPath(jTreePath);
  }

  /**
   * Expands the current jTree selection with a node identified by its position in its parent node.
   * <p>This method is preferred over {@link #addToSelection(String)} when there are several nodes
   * with the same name under a given parent.
   */
  public void addToSelection(String parentPath, int childIndex) {
    jTree.addSelectionPath(computeChildTreePath(parentPath, childIndex));
  }

  /**
   * Removes the current selection.
   */
  public void clearSelection() {
    jTree.clearSelection();
  }

  /**
   * Sets the selection on the given node.
   */
  public void select(String path) {
    jTree.clearSelection();
    jTree.setSelectionPath(getTreePath(path));
  }

  /**
   * Sets the jTree selection on a node identified by its position in its parent node.
   * <p>This method is preferred over {@link #select(String)} when there are several nodes
   * with the same name under a given parent.
   */
  public void select(String parentPath, int childIndex) {
    int childCount = getChildCount(parentPath);
    if (childIndex < 0 || childCount <= childIndex) {
      throw new RuntimeException("No child found under '" +
                                 parentPath +
                                 "' for index '" + childIndex + "'");
    }
    jTree.clearSelection();
    jTree.addSelectionPath(computeChildTreePath(parentPath, childIndex));
  }

  /**
   * Selects under a given parent all the nodes whose name contains a given substring.
   * This method will throw an exception if no the parent path was invalid or if no children
   * were found.
   */
  public void select(String parentPath, String childSubstring) {
    TreePath jTreePath = getTreePath(parentPath);
    TreeModel model = jTree.getModel();
    Object node = jTreePath.getLastPathComponent();
    List<TreePath> subPaths = new ArrayList<TreePath>();
    for (int i = 0, max = model.getChildCount(node); i < max; i++) {
      Object child = model.getChild(node, i);
      String text = getShownText(child);
      if (text.indexOf(childSubstring) >= 0) {
        subPaths.add(jTreePath.pathByAddingChild(child));
      }
    }
    if (subPaths.isEmpty()) {
      AssertAdapter.fail("No children found");
    }

    TreePath[] result = subPaths.toArray(new TreePath[subPaths.size()]);
    jTree.setSelectionPaths(result);
  }

  public void select(String[] paths) {
    jTree.clearSelection();
    for (String path : paths) {
      jTree.addSelectionPath(getTreePath(path));
    }
  }

  /**
   * Simulates a user left-click on a given node.
   */
  public void click(String path) {
    clickOnPath(getTreePath(path), false);
  }

  /**
   * Simulates a user right-click on a given node.
   */
  public void rightClick(String path) {
    jTree.clearSelection();
    clickOnPath(getTreePath(path), true);
  }

  public void doubleClick(String path) {
    doubleClickOnTreePath(path);
  }

  /**
   * Right-clicks on the first selected node.
   */
  public void rightClickInSelection() {
    TreePath selectionPath = jTree.getSelectionPath();
    AssertAdapter.assertNotNull("There is no current selection", selectionPath);
    clickOnPath(selectionPath, true);
  }

  public Trigger triggerClick(final String path) {
    return new Trigger() {
      public void run() throws Exception {
        click(path);
      }
    };
  }

  public Trigger triggerRightClick(final String path) {
    return new Trigger() {
      public void run() throws Exception {
        rightClick(path);
      }
    };
  }

  public Trigger triggerRightClickInSelection() {
    return new Trigger() {
      public void run() throws Exception {
        rightClickInSelection();
      }
    };
  }

  public Trigger triggerDoubleClick(final String path) {
    return new Trigger() {
      public void run() throws Exception {
        doubleClick(path);
      }
    };
  }

  /**
   * Returns the number of children of a given node.
   */
  public int getChildCount(String path) {
    TreePath jTreePath = getTreePath(path);
    return jTree.getModel().getChildCount(jTreePath.getLastPathComponent());
  }

  /**
   * Checks that a given node is selected, and that is is the only selection.
   */
  public Assertion selectionEquals(final String path) {
    return new Assertion() {
      public void check() {
        TreePath selectionPath = jTree.getSelectionPath();
        AssertAdapter.assertNotNull("The current tree selection is null.", selectionPath);
        TreePath expectedPath = getTreePath(path);
        AssertAdapter.assertNotNull("The expected path is not valid.", expectedPath);
        AssertAdapter.assertEquals(path, pathToString(selectionPath, separator));
      }
    };
  }

  /**
   * Checks the selection contents.
   */
  public Assertion selectionEquals(final String[] paths) {
    return new Assertion() {
      public void check() {
        String[] expectedPaths = paths.clone();
        TreePath[] selectionPaths = jTree.getSelectionPaths();
        if (selectionPaths == null) {
          selectionPaths = new TreePath[0];
        }
        String[] actual = new String[selectionPaths.length];
        for (int i = 0; i < selectionPaths.length; i++) {
          TreePath selectionPath = selectionPaths[i];
          AssertAdapter.assertNotNull("The tree has an unexpectedly null selection path.", selectionPath);
          TreePath expectedPath = getTreePath(paths[i]);
          actual[i] = pathToString(expectedPath, separator);
        }
        Arrays.sort(actual);
        ArrayUtils.assertEquals(expectedPaths, actual);
      }
    };
  }

  /**
   * Checks that the selection is empty.
   */
  public Assertion selectionIsEmpty() {
    return new Assertion() {
      public void check() {
        AssertAdapter.assertEquals(0, jTree.getSelectionCount());
      }
    };
  }

  /**
   * Checks the font color used on a given node.
   */
  public Assertion foregroundEquals(final String path, final String color) {
    return new Assertion() {
      public void check() {
        Object userObject = getTreePath(path).getLastPathComponent();
        ColorUtils.assertEquals(color, getShownColor(userObject));
      }
    };
  }

  /**
   * Checks that the a given node of the jTree is expanded - i.e. that its children are made visible.
   *
   * @param path a String identifying the path to be expanded or collapsed
   */
  public Assertion pathIsExpanded(final String path) {
    return new Assertion() {
      public void check() {
        AssertAdapter.assertTrue(jTree.isExpanded(getTreePath(path)));
      }
    };
  }

  /**
   * Expands or collapses a given node.
   *
   * @param path   a String identifying the path to be expanded or collapsed
   * @param expand if true, expand the node, and collapse it otherwise
   */
  public void expand(String path, boolean expand) {
    TreePath jTreePath = getTreePath(path);
    if (expand) {
      jTree.expandPath(jTreePath);
    }
    else {
      jTree.collapsePath(jTreePath);
    }
  }

  /**
   * Expands all the nodes of the jTree.
   */
  public void expandAll() {
    expandSubTree(new TreePath(jTree.getModel().getRoot()));
  }

  public String toString() {
    return getContent();
  }

  private TreePath computeChildTreePath(String parentPath, int childIndex) {
    TreePath jTreePath = getTreePath(parentPath);
    Object child = jTree.getModel().getChild(jTreePath.getLastPathComponent(), childIndex);
    return jTreePath.pathByAddingChild(child);
  }

  private void doubleClickOnTreePath(String path) {
    Rectangle rect = setVisible(getTreePath(path));
    if (rect != null) {
      Mouse.doDoubleClickInRectangle(jTree, rect);
    }
  }

  private void clickOnPath(TreePath treePath, boolean useRightClick) {
    Rectangle rect = setVisible(treePath);
    if (rect != null) {
      Mouse.doClickInRectangle(jTree, rect, useRightClick, Key.Modifier.NONE);
    }
  }

  private Rectangle setVisible(TreePath treePath) {
    jTree.expandPath(treePath.getParentPath());
    if (!jTree.isPathSelected(treePath)) {
      jTree.setSelectionPath(treePath);
    }
    return getRectangle(treePath);
  }

  private Rectangle getRectangle(TreePath treePath) {
    return jTree.getRowBounds(jTree.getRowForPath(treePath));
  }

  private String getShownText(Object object) {
    return cellValueConverter.getValue(getRenderedComponent(object), object);
  }

  private Color getShownColor(Object object) {
    return cellValueConverter.getForeground(getRenderedComponent(object), object);
  }

  private boolean isBold(Object object) {
    return cellValueConverter.isBold(getRenderedComponent(object), object);
  }

  private Component getRenderedComponent(Object object) {
    TreeCellRenderer renderer = jTree.getCellRenderer();
    return renderer.getTreeCellRendererComponent(jTree, object,
                                                 false, false, false, 0, false);
  }

  private TreePath getTreePath(String path) {
    TreePath jTreePath = findTreePath(path);
    if (jTreePath == null) {
      AssertAdapter.fail(badTreePath(path));
    }
    return jTreePath;
  }

  private TreePath findTreePath(String path) {
    String[] pathArray = toArray(path, separator);
    TreeModel model = jTree.getModel();
    Object[] objects = new Object[pathArray.length + 1];
    Object node = model.getRoot();
    objects[0] = node;
    for (int i = 0; i < pathArray.length; i++) {
      Object exactMatch = null;
      Object substringMatch = null;
      boolean substringAmbiguity = false;
      for (int j = 0; (j < model.getChildCount(node)); j++) {
        Object child = model.getChild(node, j);
        String shownText = getShownText(child);
        if (pathArray[i].equals(shownText)) {
          if (exactMatch != null) {
            AssertAdapter.fail("Naming ambiguity: there are several '" +
                               pathArray[i] + "' under '" +
                               getShownText(node) + "'");
          }
          exactMatch = child;
        }
        else if (shownText.indexOf(pathArray[i]) >= 0) {
          if (substringMatch != null) {
            substringAmbiguity = true;
          }
          substringMatch = child;
        }
      }
      Object result = null;
      if (exactMatch != null) {
        result = exactMatch;
      }
      else if (substringAmbiguity) {
        AssertAdapter.fail("Naming ambiguity: there are several '" +
                           pathArray[i] + "' under '" +
                           getShownText(node) + "'");
      }
      else {
        result = substringMatch;
      }
      if (result == null) {
        return null;
      }
      objects[i + 1] = result;
      node = result;
    }
    return new TreePath(objects);
  }

  private static String[] toArray(String path, String separator) {
    List<String> result = new ArrayList<String>();
    for (int index = 0; index < path.length();) {
      int nextSeparatorPosition = path.indexOf(separator, index);
      if (nextSeparatorPosition == -1) {
        nextSeparatorPosition = path.length();
      }
      result.add(path.substring(index, nextSeparatorPosition));
      index = nextSeparatorPosition + separator.length();
    }
    return result.toArray(new String[result.size()]);
  }

  private void expandSubTree(TreePath path) {
    TreeModel jTreeModel = jTree.getModel();
    Object node = path.getLastPathComponent();
    for (int i = 0; i < jTreeModel.getChildCount(node); i++) {
      Object child = jTreeModel.getChild(node, i);
      TreePath childPath = path.pathByAddingChild(child);
      if (!isLeaf(childPath)) {
        expandSubTree(childPath);
      }
    }
    jTree.expandPath(path);
  }

  private boolean isLeaf(TreePath path) {
    return jTree.getModel().isLeaf(path.getLastPathComponent());
  }

  private String pathToString(TreePath jTreePath, String separator) {
    Object[] path = jTreePath.getPath();
    StringBuffer buffer = new StringBuffer();
    for (int i = 1; i < path.length; i++) {
      buffer.append(getShownText(path[i]));
      if (i < path.length - 1) {
        buffer.append(separator);
      }
    }
    return buffer.toString();
  }

  private void checkContents(String trimmedExpected) {
    compareContents(trimmedExpected, getContent());
  }

  private String getContent() {
    TreeModel model = jTree.getModel();
    Object root = model.getRoot();
    StringBuffer buffer = new StringBuffer();
    fillBuffer(root, model, buffer, "");
    return buffer.toString();
  }

  private void compareContents(String expected, String actual) {
    if (expected.equals(actual)) {
      return;
    }
    if (!areLinesEqual(toLines(expected), toLines(actual))) {
      AssertAdapter.assertEquals(expected, actual);
    }
  }

  private boolean areLinesEqual(List expected, List actual) {
    if (expected.size() != actual.size()) {
      return false;
    }
    for (Iterator expectedIter = expected.iterator(), actualIter = actual.iterator();
         expectedIter.hasNext() && actualIter.hasNext();) {
      if (!areLinesEqual((String)expectedIter.next(),
                         (String)actualIter.next())) {
        return false;
      }
    }
    return true;
  }

  private boolean areLinesEqual(String expected, String actual) {
    if (expected.equals(actual)) {
      return true;
    }
    Matcher expectedMatcher = COLOR_PROPERTY_PATTERN.matcher(expected);
    Matcher actualMatcher = COLOR_PROPERTY_PATTERN.matcher(actual);
    String expectedWithoutColor = expectedMatcher.replaceFirst("C");
    String actualWithoutColor = actualMatcher.replaceFirst("C");
    if (!expectedWithoutColor.equals(actualWithoutColor)) {
      return false;
    }
    String expectedColor = expectedMatcher.group(1);
    String actualColor = actualMatcher.group(1);
    return ColorUtils.equals(expectedColor, ColorUtils.getColor(actualColor));
  }

  private List<String> toLines(String text) {
    StringTokenizer tokenizer = new StringTokenizer(text, "\n");
    List<String> result = new ArrayList<String>();
    while (tokenizer.hasMoreTokens()) {
      result.add(tokenizer.nextToken());
    }
    return result;
  }

  private void fillBuffer(Object obj,
                          TreeModel model,
                          StringBuffer buffer,
                          String indent) {
    String text = getShownText(obj);
    buffer.append(indent).append(text);
    boolean bold = isBold(obj);
    fillNodeProperties(bold, getShownColor(obj), buffer);
    buffer.append('\n');
    for (int i = 0, max = model.getChildCount(obj); i < max; i++) {
      Object child = model.getChild(obj, i);
      fillBuffer(child, model, buffer, indent + "  ");
    }
  }

  private void fillNodeProperties(boolean bold, Color shownColor, StringBuffer buffer) {
    String shownColorDescription = getShownColorDescription(shownColor);
    if (bold || (shownColorDescription != null)) {
      buffer.append(" #(");
      if (bold) {
        buffer.append("bold");
        if (shownColorDescription != null) {
          buffer.append(',');
        }
      }
      if (shownColorDescription != null) {
        buffer.append("color=").append(shownColorDescription);
      }
      buffer.append(")");
    }
  }

  private String getShownColorDescription(Color shownColor) {
    return isDefaultColor(shownColor) ? null : ColorUtils.getColorDescription(shownColor);
  }

  private boolean isDefaultColor(Color color) {
    return Color.BLACK.equals(color) || Utils.equals(color, getDefaultForegroundColor());
  }

  private Color getDefaultForegroundColor() {
    AccessibleContext context = jTree.getAccessibleContext();
    if (context instanceof AccessibleComponent) {
      return ((AccessibleComponent)context).getForeground();
    }
    return null;
  }

  static String badTreePath(String path) {
    return "Could not find element '" + path + "'";
  }
}
TOP

Related Classes of org.uispec4j.Tree

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.