001: package org.uispec4j;
002:
003: import junit.framework.Assert;
004: import junit.framework.AssertionFailedError;
005: import org.uispec4j.assertion.Assertion;
006: import org.uispec4j.utils.ArrayUtils;
007: import org.uispec4j.utils.ColorUtils;
008: import org.uispec4j.utils.Utils;
009:
010: import javax.accessibility.AccessibleComponent;
011: import javax.accessibility.AccessibleContext;
012: import javax.swing.*;
013: import javax.swing.tree.TreeCellRenderer;
014: import javax.swing.tree.TreeModel;
015: import javax.swing.tree.TreePath;
016: import java.awt.*;
017: import java.util.*;
018: import java.util.List;
019: import java.util.regex.Matcher;
020: import java.util.regex.Pattern;
021:
022: /**
023: * Wrapper for JTree components.<p>
024: * The nodes of a tree are identified using stringified paths. For instance, for the tree
025: * below:
026: * <pre><code>
027: * root
028: * |
029: * +- child
030: * | |
031: * | +- subChild
032: * |
033: * +- otherChild
034: * |
035: * +- otherSubChild
036: * </code></pre>
037: * the "subChild" element is identified with the following path:
038: * <pre><code>
039: * child/subChild
040: * </code></pre>
041: * Note that when defining paths the root element name is always omitted. The root
042: * node path itself is denoted by an empty string ("").<p>
043: * The default path separator is "/". However, this separator can be customized as follows:
044: * <ul>
045: * <li>By setting it on a given Tree instance using {@link Tree#setSeparator(String)}</li>
046: * <li>By setting it on all new Tree instances using {@link Tree#setDefaultSeparator(String)}</li>
047: * <li>By setting it on all new Tree instances using the <code>uispec4j.tree.separator</code>
048: * property.</li>
049: * </ul>
050: * When using paths, it is also possible to use substrings from the displayed node names.
051: * For instance, instead of writing:
052: * <pre><code>
053: * otherChild/otherSubChild
054: * </code></pre>
055: * one can write:
056: * <pre><code>
057: * other/sub
058: * </code></pre>
059: * <p/>
060: * The contents of the tree can be checked with {@link #contentEquals(String)},
061: * which is used as follows:
062: * <pre><code>
063: * assertTrue(jTree.contentEquals("root\n" +
064: * " child1\n" +
065: * " child1_1\n" +
066: * " child2"));
067: * </code></pre>
068: * <p/>
069: * The conversion between the values (Strings) given in the tests and the values
070: * actually displayed by the JTree renderer is performed by a dedicated
071: * {@link TreeCellValueConverter}, which retrieves the graphical component that draws
072: * the tree nodes and determines the displayed value accordingly.
073: * A {@link DefaultTreeCellValueConverter} is used by default by the Tree component.
074: */
075: public class Tree extends AbstractUIComponent {
076:
077: public static final String TYPE_NAME = "tree";
078: public static final Class[] SWING_CLASSES = { JTree.class };
079: static final String SEPARATOR_PROPERTY = "uispec4j.tree.separator";
080:
081: private JTree jTree;
082: static String defaultSeparator = "/";
083: private String separator;
084: private TreeCellValueConverter cellValueConverter = new DefaultTreeCellValueConverter();
085:
086: private static final Pattern COLOR_PROPERTY_PATTERN = Pattern
087: .compile(" #\\(.*color=([\\w]+)\\)");
088:
089: public Tree(JTree jTree) {
090: this .jTree = jTree;
091: this .separator = initSeparator();
092: }
093:
094: private static String initSeparator() {
095: String property = System.getProperty(Tree.SEPARATOR_PROPERTY);
096: if ((property != null) && (property.length() > 0)) {
097: return property;
098: }
099: return Tree.defaultSeparator;
100: }
101:
102: public String getDescriptionTypeName() {
103: return TYPE_NAME;
104: }
105:
106: public Component getAwtComponent() {
107: return jTree;
108: }
109:
110: /**
111: * Returns the JTree wrapped by this component.
112: */
113: public JTree getJTree() {
114: return jTree;
115: }
116:
117: /**
118: * Sets the separator to be used for specifying node paths in this jTree instance.
119: */
120: public void setSeparator(String separator) {
121: checkSeparator(separator);
122: this .separator = separator;
123: }
124:
125: private static void checkSeparator(String separator) {
126: if (separator == null) {
127: throw new IllegalArgumentException(
128: "Separator must not be null");
129: } else if (separator.length() == 0) {
130: throw new IllegalArgumentException(
131: "Separator must not be empty");
132: }
133: }
134:
135: /**
136: * Returns the separator currently used for specifying node paths in this jTree instance.
137: */
138: public String getSeparator() {
139: return separator;
140: }
141:
142: /**
143: * Sets the separator to be used for specifying node paths in new jTree instances.
144: */
145: public static void setDefaultSeparator(String separator) {
146: checkSeparator(separator);
147: defaultSeparator = separator;
148: }
149:
150: /**
151: * Sets a new converter for retrieving the text displayed on the tree cells.
152: */
153: public void setCellValueConverter(TreeCellValueConverter converter) {
154: this .cellValueConverter = converter;
155: }
156:
157: /**
158: * Checks the nodes structure displayed by the jTree.<p>
159: * The expected contents is a newline (\n) separated string where nodes are
160: * indented with two-space steps.
161: * For instance:
162: * <code><pre>
163: * assertTrue(jTree.contentEquals("root\n" +
164: * " child1\n" +
165: * " child1_1\n" +
166: * " child2"));
167: * </pre></code>
168: * Text display properties such as boldness and color can be checked using a "#(...)"
169: * specifier.
170: * For instance:
171: * <code><pre>
172: * assertTrue(jTree.contentEquals("root\n" +
173: * " child1 #(bold)\n" +
174: * " child1_1 #(bold,color=red)\n" +
175: * " child2"));
176: * </pre></code>
177: * The properties are defined as follows:
178: * <ul>
179: * <li>The "bold" property must be present if and only if the node text is bold</li>
180: * <li>The "color" property value can be numeric ("0000ee") or approximative ("blue")
181: * (see the <a href="http://www.uispec4j.org/usingcolors.html">Using colors</a> page
182: * for more information)</li>
183: * <li>The "bold" property, if present, must be placed before the "color" property</li>
184: * </ul>
185: */
186: public Assertion contentEquals(final String expectedContents) {
187: return new Assertion() {
188: public void check() {
189: String trimmedExpected = expectedContents.trim();
190: Assert
191: .assertTrue(
192: "Expected tree description should not be empty",
193: (trimmedExpected != null)
194: && (trimmedExpected.length() > 0));
195: checkContents(trimmedExpected);
196: }
197: };
198: }
199:
200: /**
201: * Checks that a node identified by the given path is present in the jTree.
202: */
203: public Assertion contains(final String path) {
204: return new Assertion() {
205: public void check() {
206: getTreePath(path);
207: }
208: };
209: }
210:
211: /**
212: * Selects the root node of the jTree.
213: */
214: public void selectRoot() {
215: jTree
216: .setSelectionPath(new TreePath(jTree.getModel()
217: .getRoot()));
218: }
219:
220: /**
221: * Expands the current jTree selection with a given node.
222: */
223: public void addToSelection(String path) {
224: jTree.addSelectionPath(getTreePath(path));
225: }
226:
227: /**
228: * Removes the given node from the current jTree selection.
229: */
230: public void removeFromSelection(String path) {
231: TreePath jTreePath = getTreePath(path);
232: jTree.removeSelectionPath(jTreePath);
233: }
234:
235: /**
236: * Expands the current jTree selection with a node identified by its position in its parent node.
237: * <p>This method is preferred over {@link #addToSelection(String)} when there are several nodes
238: * with the same name under a given parent.
239: */
240: public void addToSelection(String parentPath, int childIndex) {
241: jTree.addSelectionPath(computeChildTreePath(parentPath,
242: childIndex));
243: }
244:
245: /**
246: * Removes the current selection.
247: */
248: public void clearSelection() {
249: jTree.clearSelection();
250: }
251:
252: /**
253: * Sets the selection on the given node.
254: */
255: public void select(String path) {
256: jTree.clearSelection();
257: jTree.setSelectionPath(getTreePath(path));
258: }
259:
260: /**
261: * Sets the jTree selection on a node identified by its position in its parent node.
262: * <p>This method is preferred over {@link #select(String)} when there are several nodes
263: * with the same name under a given parent.
264: */
265: public void select(String parentPath, int childIndex) {
266: int childCount = getChildCount(parentPath);
267: if (childIndex < 0 || childCount <= childIndex) {
268: throw new RuntimeException("No child found under '"
269: + parentPath + "' for index '" + childIndex + "'");
270: }
271: jTree.clearSelection();
272: jTree.addSelectionPath(computeChildTreePath(parentPath,
273: childIndex));
274: }
275:
276: /**
277: * Selects under a given parent all the nodes whose name contains a given substring.
278: * This method will throw an exception if no the parent path was invalid or if no children
279: * were found.
280: */
281: public void select(String parentPath, String childSubstring) {
282: TreePath jTreePath = getTreePath(parentPath);
283: TreeModel model = jTree.getModel();
284: Object node = jTreePath.getLastPathComponent();
285: List subPaths = new ArrayList();
286: for (int i = 0, max = model.getChildCount(node); i < max; i++) {
287: Object child = model.getChild(node, i);
288: String text = getShownText(child);
289: if (text.indexOf(childSubstring) >= 0) {
290: subPaths.add(jTreePath.pathByAddingChild(child));
291: }
292: }
293: if (subPaths.isEmpty()) {
294: Assert.fail("No children found");
295: }
296:
297: TreePath[] result = (TreePath[]) subPaths
298: .toArray(new TreePath[subPaths.size()]);
299: jTree.setSelectionPaths(result);
300: }
301:
302: public void select(String[] paths) {
303: jTree.clearSelection();
304: for (int i = 0; i < paths.length; i++) {
305: String path = paths[i];
306: jTree.addSelectionPath(getTreePath(path));
307: }
308: }
309:
310: /**
311: * Simulates a user left-click on a given node.
312: */
313: public void click(String path) {
314: TreePath jTreePath = getTreePath(path);
315: jTree.setSelectionPath(jTreePath);
316: clickOnTreePath(getTreePath(path), false, Key.Modifier.NONE);
317: }
318:
319: /**
320: * Simulates a user right-click on a given node.
321: */
322: public void rightClick(String path) {
323: TreePath jTreePath = getTreePath(path);
324: jTree.setSelectionPath(jTreePath);
325: clickOnTreePath(jTreePath, true, Key.Modifier.NONE);
326: }
327:
328: /**
329: * Right-clicks on the first selected node.
330: */
331: public void rightClickInSelection() {
332: TreePath selectionPath = jTree.getSelectionPath();
333: Assert.assertNotNull("There is no current selection",
334: selectionPath);
335: clickOnTreePath(selectionPath, true, Key.Modifier.NONE);
336: }
337:
338: public Trigger triggerClick(final String path) {
339: return new Trigger() {
340: public void run() throws Exception {
341: click(path);
342: }
343: };
344: }
345:
346: public Trigger triggerRightClick(final String path) {
347: return new Trigger() {
348: public void run() throws Exception {
349: rightClick(path);
350: }
351: };
352: }
353:
354: public Trigger triggerRightClickInSelection() {
355: return new Trigger() {
356: public void run() throws Exception {
357: rightClickInSelection();
358: }
359: };
360: }
361:
362: /**
363: * Returns the number of children of a given node.
364: */
365: public int getChildCount(String path) {
366: TreePath jTreePath = getTreePath(path);
367: return jTree.getModel().getChildCount(
368: jTreePath.getLastPathComponent());
369: }
370:
371: /**
372: * Checks that a given node is selected, and that is is the only selection.
373: */
374: public Assertion selectionEquals(final String path) {
375: return new Assertion() {
376: public void check() {
377: TreePath selectionPath = jTree.getSelectionPath();
378: Assert.assertNotNull(selectionPath);
379: TreePath expectedPath = getTreePath(path);
380: Assert.assertNotNull(expectedPath);
381: Assert.assertEquals(path, pathToString(selectionPath,
382: separator));
383: }
384: };
385: }
386:
387: /**
388: * Checks the selection contents.
389: */
390: public Assertion selectionEquals(final String[] paths) {
391: return new Assertion() {
392: public void check() {
393: String[] expectedPaths = (String[]) paths.clone();
394: TreePath[] selectionPaths = jTree.getSelectionPaths();
395: if (selectionPaths == null) {
396: selectionPaths = new TreePath[0];
397: }
398: String[] actual = new String[selectionPaths.length];
399: for (int i = 0; i < selectionPaths.length; i++) {
400: TreePath selectionPath = selectionPaths[i];
401: Assert.assertNotNull(selectionPath);
402: TreePath expectedPath = getTreePath(paths[i]);
403: actual[i] = pathToString(expectedPath, separator);
404: }
405: Arrays.sort(actual);
406: ArrayUtils.assertEquals(expectedPaths, actual);
407: }
408: };
409: }
410:
411: /**
412: * Checks that the selection is empty.
413: */
414: public Assertion selectionIsEmpty() {
415: return new Assertion() {
416: public void check() {
417: Assert.assertEquals(0, jTree.getSelectionCount());
418: }
419: };
420: }
421:
422: /**
423: * Checks the font color used on a given node.
424: */
425: public Assertion foregroundEquals(final String path,
426: final String color) {
427: return new Assertion() {
428: public void check() {
429: Object userObject = getTreePath(path)
430: .getLastPathComponent();
431: ColorUtils.assertEquals(color,
432: getShownColor(userObject));
433: }
434: };
435: }
436:
437: /**
438: * Checks that the a given node of the jTree is expanded - i.e. that its children are made visible.
439: *
440: * @param path a String identifying the path to be expanded or collapsed
441: */
442: public Assertion pathIsExpanded(final String path) {
443: return new Assertion() {
444: public void check() {
445: Assert.assertTrue(jTree.isExpanded(getTreePath(path)));
446: }
447: };
448: }
449:
450: /**
451: * Expands or collapses a given node.
452: *
453: * @param path a String identifying the path to be expanded or collapsed
454: * @param expand if true, expand the node, and collapse it otherwise
455: */
456: public void expand(String path, boolean expand) {
457: TreePath jTreePath = getTreePath(path);
458: if (expand) {
459: jTree.expandPath(jTreePath);
460: } else {
461: jTree.collapsePath(jTreePath);
462: }
463: }
464:
465: /**
466: * Expands all the nodes of the jTree.
467: */
468: public void expandAll() {
469: expandSubTree(new TreePath(jTree.getModel().getRoot()));
470: }
471:
472: public String toString() {
473: return getContent();
474: }
475:
476: private TreePath computeChildTreePath(String parentPath,
477: int childIndex) {
478: TreePath jTreePath = getTreePath(parentPath);
479: Object child = jTree.getModel().getChild(
480: jTreePath.getLastPathComponent(), childIndex);
481: return jTreePath.pathByAddingChild(child);
482: }
483:
484: private void clickOnTreePath(TreePath path, boolean useRightClick,
485: Key.Modifier keyModifier) {
486: jTree.expandPath(path.getParentPath());
487: Rectangle rect = jTree.getRowBounds(jTree.getRowForPath(path));
488: if (rect != null) {
489: Mouse.doClickInRectangle(jTree, rect, useRightClick,
490: keyModifier);
491: }
492: }
493:
494: private String getShownText(Object object) {
495: return cellValueConverter.getValue(
496: getRenderedComponent(object), object);
497: }
498:
499: private Color getShownColor(Object object) {
500: return cellValueConverter.getForeground(
501: getRenderedComponent(object), object);
502: }
503:
504: private boolean isBold(Object object) {
505: return cellValueConverter.isBold(getRenderedComponent(object),
506: object);
507: }
508:
509: private Component getRenderedComponent(Object object) {
510: TreeCellRenderer renderer = jTree.getCellRenderer();
511: return renderer.getTreeCellRendererComponent(jTree, object,
512: false, false, false, 0, false);
513: }
514:
515: private TreePath getTreePath(String path) {
516: TreePath jTreePath = findTreePath(path);
517: if (jTreePath == null) {
518: Assert.fail(badTreePath(path));
519: }
520: return jTreePath;
521: }
522:
523: private TreePath findTreePath(String path) {
524: String[] pathArray = toArray(path, separator);
525: TreeModel model = jTree.getModel();
526: Object[] objects = new Object[pathArray.length + 1];
527: Object node = model.getRoot();
528: objects[0] = node;
529: for (int i = 0; i < pathArray.length; i++) {
530: Object exactMatch = null;
531: Object substringMatch = null;
532: boolean substringAmbiguity = false;
533: for (int j = 0; (j < model.getChildCount(node)); j++) {
534: Object child = model.getChild(node, j);
535: String shownText = getShownText(child);
536: if (pathArray[i].equals(shownText)) {
537: if (exactMatch != null) {
538: throw new AssertionFailedError(
539: "Naming ambiguity: there are several '"
540: + pathArray[i] + "' under '"
541: + getShownText(node) + "'");
542: }
543: exactMatch = child;
544: } else if (shownText.indexOf(pathArray[i]) >= 0) {
545: if (substringMatch != null) {
546: substringAmbiguity = true;
547: }
548: substringMatch = child;
549: }
550: }
551: Object result;
552: if (exactMatch != null) {
553: result = exactMatch;
554: } else if (substringAmbiguity) {
555: throw new AssertionFailedError(
556: "Naming ambiguity: there are several '"
557: + pathArray[i] + "' under '"
558: + getShownText(node) + "'");
559: } else {
560: result = substringMatch;
561: }
562: if (result == null) {
563: return null;
564: }
565: objects[i + 1] = result;
566: node = result;
567: }
568: return new TreePath(objects);
569: }
570:
571: private static String[] toArray(String path, String separator) {
572: List result = new ArrayList();
573: for (int index = 0; index < path.length();) {
574: int nextSeparatorPosition = path.indexOf(separator, index);
575: if (nextSeparatorPosition == -1) {
576: nextSeparatorPosition = path.length();
577: }
578: result.add(path.substring(index, nextSeparatorPosition));
579: index = nextSeparatorPosition + separator.length();
580: }
581: return (String[]) (result.toArray(new String[result.size()]));
582: }
583:
584: private void expandSubTree(TreePath path) {
585: TreeModel jTreeModel = jTree.getModel();
586: Object node = path.getLastPathComponent();
587: for (int i = 0; i < jTreeModel.getChildCount(node); i++) {
588: Object child = jTreeModel.getChild(node, i);
589: TreePath childPath = path.pathByAddingChild(child);
590: if (!isLeaf(childPath)) {
591: expandSubTree(childPath);
592: }
593: }
594: jTree.expandPath(path);
595: }
596:
597: private boolean isLeaf(TreePath path) {
598: return jTree.getModel().isLeaf(path.getLastPathComponent());
599: }
600:
601: private String pathToString(TreePath jTreePath, String separator) {
602: Object[] path = jTreePath.getPath();
603: StringBuffer buffer = new StringBuffer();
604: for (int i = 1; i < path.length; i++) {
605: buffer.append(getShownText(path[i]));
606: if (i < path.length - 1) {
607: buffer.append(separator);
608: }
609: }
610: return buffer.toString();
611: }
612:
613: private void checkContents(String trimmedExpected) {
614: compareContents(trimmedExpected, getContent());
615: }
616:
617: private String getContent() {
618: TreeModel model = jTree.getModel();
619: Object root = model.getRoot();
620: StringBuffer buffer = new StringBuffer();
621: fillBuffer(root, model, buffer, "");
622: return buffer.toString();
623: }
624:
625: private void compareContents(String expected, String actual) {
626: if (expected.equals(actual)) {
627: return;
628: }
629: if (!areLinesEqual(toLines(expected), toLines(actual))) {
630: Assert.assertEquals(expected, actual);
631: }
632: }
633:
634: private boolean areLinesEqual(List expected, List actual) {
635: if (expected.size() != actual.size()) {
636: return false;
637: }
638: for (Iterator expectedIter = expected.iterator(), actualIter = actual
639: .iterator(); expectedIter.hasNext()
640: && actualIter.hasNext();) {
641: if (!areLinesEqual((String) expectedIter.next(),
642: (String) actualIter.next())) {
643: return false;
644: }
645: }
646: return true;
647: }
648:
649: private boolean areLinesEqual(String expected, String actual) {
650: if (expected.equals(actual)) {
651: return true;
652: }
653: Matcher expectedMatcher = COLOR_PROPERTY_PATTERN
654: .matcher(expected);
655: Matcher actualMatcher = COLOR_PROPERTY_PATTERN.matcher(actual);
656: String expectedWithoutColor = expectedMatcher.replaceFirst("C");
657: String actualWithoutColor = actualMatcher.replaceFirst("C");
658: if (!expectedWithoutColor.equals(actualWithoutColor)) {
659: return false;
660: }
661: String expectedColor = expectedMatcher.group(1);
662: String actualColor = actualMatcher.group(1);
663: return ColorUtils.equals(expectedColor, ColorUtils
664: .getColor(actualColor));
665: }
666:
667: private List toLines(String text) {
668: StringTokenizer tokenizer = new StringTokenizer(text, "\n");
669: List result = new ArrayList();
670: while (tokenizer.hasMoreTokens()) {
671: result.add(tokenizer.nextToken());
672: }
673: return result;
674: }
675:
676: private void fillBuffer(Object obj, TreeModel model,
677: StringBuffer buffer, String indent) {
678: String text = getShownText(obj);
679: buffer.append(indent).append(text);
680: boolean bold = isBold(obj);
681: fillNodeProperties(bold, getShownColor(obj), buffer);
682: buffer.append('\n');
683: for (int i = 0, max = model.getChildCount(obj); i < max; i++) {
684: Object child = model.getChild(obj, i);
685: fillBuffer(child, model, buffer, indent + " ");
686: }
687: }
688:
689: private void fillNodeProperties(boolean bold, Color shownColor,
690: StringBuffer buffer) {
691: String shownColorDescription = getShownColorDescription(shownColor);
692: if (bold || (shownColorDescription != null)) {
693: buffer.append(" #(");
694: if (bold) {
695: buffer.append("bold");
696: if (shownColorDescription != null) {
697: buffer.append(',');
698: }
699: }
700: if (shownColorDescription != null) {
701: buffer.append("color=").append(shownColorDescription);
702: }
703: buffer.append(")");
704: }
705: }
706:
707: private String getShownColorDescription(Color shownColor) {
708: return isDefaultColor(shownColor) ? null : ColorUtils
709: .getColorDescription(shownColor);
710: }
711:
712: private boolean isDefaultColor(Color color) {
713: if (Color.BLACK.equals(color)) {
714: return true;
715: }
716: if (Utils.equals(color, getDefaultForegroundColor())) {
717: return true;
718: }
719: return false;
720: }
721:
722: private Color getDefaultForegroundColor() {
723: AccessibleContext context = jTree.getAccessibleContext();
724: if (context instanceof AccessibleComponent) {
725: return ((AccessibleComponent) context).getForeground();
726: }
727: return null;
728: }
729:
730: static String badTreePath(String path) {
731: return "Could not find element '" + path + "'";
732: }
733: }
|