1 /* 2 * $Id$ 3 * 4 * Copyright (c) 2004, 2011, Oracle and/or its affiliates. All rights reserved. 5 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 6 * 7 * This code is free software; you can redistribute it and/or modify it 8 * under the terms of the GNU General Public License version 2 only, as 9 * published by the Free Software Foundation. Oracle designates this 10 * particular file as subject to the "Classpath" exception as provided 11 * by Oracle in the LICENSE file that accompanied this code. 12 * 13 * This code is distributed in the hope that it will be useful, but WITHOUT 14 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 16 * version 2 for more details (a copy is included in the LICENSE file that 17 * accompanied this code). 18 * 19 * You should have received a copy of the GNU General Public License version 20 * 2 along with this work; if not, write to the Free Software Foundation, 21 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 22 * 23 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 24 * or visit www.oracle.com if you need additional information or have any 25 * questions. 26 */ 27 package com.sun.javatest.util; 28 29 import java.io.IOException; 30 import java.io.Writer; 31 import java.util.Comparator; 32 import java.util.Iterator; 33 import java.util.Map; 34 import java.util.TreeMap; 35 36 /** 37 * A class that provides a tree of information nodes that can be 38 * selectively printed, suitable for simple command line help. 39 */ 40 public class HelpTree 41 { 42 /** 43 * A node within a HelpTree. A node has a name, a description, 44 * and zero or more child nodes. 45 */ 46 public static class Node { 47 48 /** 49 * Create a node, with no children. 50 * @param name the name for the node 51 * @param description the description for the node 52 */ 53 public Node(String name, String description) { 54 this.name = name; 55 this.description = description; 56 } 57 58 /** 59 * Create a node, with given children. 60 * @param name the name for the node 61 * @param description the description for the node 62 * @param children the child nodes for the node 63 */ 64 public Node(String name, String description, Node[] children) { 65 this.name = name; 66 this.description = description; 67 this.children = children; 68 } 69 70 /** 71 * Create a node, with no children. The name and description are 72 * obtained from a resource bundle, using keys based on a common 73 * prefix. The key for the name will be <i>prefix</i>.name and 74 * the key for the description will be <i>prefix</i>.desc. 75 * @param i18n the resource bundle from which to obtain the 76 * name and description for the node. 77 * @param prefix the prefix for the names of the name and description 78 * entries in the resource bundle. 79 */ 80 public Node(I18NResourceBundle i18n, String prefix) { 81 name = i18n.getString(prefix + ".name"); 82 description = i18n.getString(prefix + ".desc"); 83 } 84 85 /** 86 * Create a node, with given children. The name and description are 87 * obtained from a resource bundle, using keys based on a common 88 * prefix. The key for the name will be <i>prefix</i>.name and 89 * the key for the description will be <i>prefix</i>.desc. 90 * @param i18n the resource bundle from which to obtain the 91 * name and description for the node. 92 * @param prefix the prefix for the names of the name and description 93 * entries in the resource bundle. 94 * @param children the child nodes for this node 95 */ 96 public Node(I18NResourceBundle i18n, String prefix, Node[] children) { 97 this(i18n, prefix); 98 this.children = children; 99 } 100 101 /** 102 * Create a node and its children. The name and description are 103 * obtained from a resource bundle, using keys based on a common 104 * prefix. The key for the name will be <i>prefix</i>.name and 105 * the key for the description will be <i>prefix</i>.desc. 106 * The children will each be created with no children of their 107 * own, using a prefix of <i>prefix</i>.<i>entry</i>. 108 * @param i18n the resource bundle from which to obtain the 109 * name and description for the node. 110 * @param prefix the prefix for the names of the name and description 111 * entries in the resource bundle. 112 * @param entries the array of <i>entry</i> names used to create 113 * the child nodes. 114 */ 115 public Node(I18NResourceBundle i18n, String prefix, String[] entries) { 116 this(i18n, prefix); 117 children = new Node[entries.length]; 118 for (int i = 0; i < children.length; i++) 119 children[i] = new Node(i18n, prefix + '.' + entries[i]); 120 } 121 122 /** 123 * Get the name of this node. 124 * @return the name of this node 125 */ 126 public final String getName() { 127 return name; 128 } 129 130 /** 131 * Get the description of this node. 132 * @return the description of this node 133 */ 134 public final String getDescription() { 135 return description; 136 } 137 138 /** 139 * Get the number of children of this node. 140 * @return the number of children of this node 141 */ 142 public int getChildCount() { 143 return (children == null ? 0 : children.length); 144 } 145 146 /** 147 * Get a specified child of this node. 148 * @param i the index of the desired child 149 * @return the specified child of this node 150 */ 151 public Node getChild(int i) { 152 if (i >= getChildCount()) 153 throw new IllegalArgumentException(); 154 return children[i]; 155 } 156 157 private String name; 158 private String description; 159 private Node[] children; 160 } 161 162 /** 163 * A selection of nodes within a HelpTree. 164 * @see HelpTree#find 165 */ 166 public class Selection { 167 private Selection(Node node) { 168 this(node, null); 169 } 170 171 private Selection(Map<Node, Selection> map) { 172 this(null, map); 173 } 174 175 private Selection(Node node, Map<Node, Selection> map) { 176 this.node = node; 177 this.map = map; 178 } 179 180 private Node node; 181 private Map<Node, Selection> map; 182 } 183 184 /** 185 * Create an empty HelpTree object. 186 */ 187 public HelpTree() { 188 nodes = new Node[0]; 189 } 190 191 /** 192 * Create a HelpTree object containing a given set of nodes. 193 * @param nodes the contents of the HelpTree 194 */ 195 public HelpTree(Node[] nodes) { 196 this.nodes = nodes; 197 } 198 199 /** 200 * Add a node to a help tree. 201 * @param node the node to be added to the tree 202 */ 203 public void addNode(Node node) { 204 nodes = DynamicArray.append(nodes, node); 205 } 206 207 /** 208 * Get the indentation used to adjust the left margin when writing 209 * the child nodes for a node. 210 * @return the indentation used to adjust the left margin when writing 211 * the child nodes for a node 212 * @see #setNodeIndent 213 */ 214 public int getNodeIndent() { 215 return nodeIndent; 216 } 217 218 /** 219 * Set the indentation used to adjust the left margin when writing 220 * the child nodes for a node. 221 * @param n the indentation used to adjust the left margin when writing 222 * the child nodes for a node 223 * @see #getNodeIndent 224 */ 225 public void setNodeIndent(int n) { 226 nodeIndent = n; 227 } 228 229 /** 230 * Get the indentation used to adjust the left margin when writing 231 * the description of a node. 232 * @return the indentation used to adjust the left margin when writing 233 * the description of a node 234 * @see #setDescriptionIndent 235 */ 236 public int getDescriptionIndent() { 237 return descriptionIndent; 238 } 239 240 /** 241 * Set the indentation used to adjust the left margin when writing 242 * the description of a node. 243 * @param n the indentation used to adjust the left margin when writing 244 * the description of a node 245 * @see #getDescriptionIndent 246 */ 247 public void setDescriptionIndent(int n) { 248 descriptionIndent = n; 249 } 250 251 /** 252 * Get a selection representing the nodes that match the given words. 253 * If there are nodes whose name or description contain all of the 254 * given words, then those nodes will be returned. 255 * Otherwise, all nodes whose name or description contain at least one 256 * of the given words will be returned. 257 * @param words the words to be searched for 258 * @return a Selection containing the matching nodes 259 */ 260 public Selection find(String[] words) { 261 Selection s = find(words, ALL); 262 263 if (s == null && words.length > 1) 264 s = find(words, ANY); 265 266 return s; 267 } 268 269 /** 270 * Get a selection representing the nodes that match all of the given words. 271 * @param words the words to be searched for 272 * @return a Selection containing the matching nodes 273 */ 274 public Selection findAll(String[] words) { 275 return find(words, ALL); 276 } 277 278 /** 279 * Get a selection representing the nodes that each match 280 * at least one of the given words. 281 * @param words the words to be searched for 282 * @return a Selection containing the matching nodes 283 */ 284 public Selection findAny(String[] words) { 285 return find(words, ANY); 286 } 287 288 private Selection find(String[] words, int mode) { 289 Map<Node, Selection> map = null; 290 291 for (int i = 0; i < nodes.length; i++) { 292 Node node = nodes[i]; 293 Selection s = find(node, words, mode); 294 if (s != null) { 295 if (map == null) 296 map = new TreeMap<>(nodeComparator); 297 map.put(node, s); 298 } 299 } 300 301 return (map == null ? null : new Selection(map)); 302 } 303 304 private Selection find(Node node, String[] words, int mode) { 305 if (mode == ALL) { 306 if (containsAllOf(node.name, words) || containsAllOf(node.description, words)) 307 return new Selection(node); 308 } 309 else if (mode == ANY) { 310 if (containsAnyOf(node.name, words) || containsAnyOf(node.description, words)) 311 return new Selection(node); 312 } 313 else 314 throw new IllegalArgumentException(); 315 316 if (node.children == null) 317 return null; 318 319 Map<Node, Selection> map = null; 320 321 for (int i = 0; i < node.children.length; i++) { 322 Node child = node.children[i]; 323 Selection s = find(child, words, mode); 324 if (s != null) { 325 if (map == null) 326 map = new TreeMap<>(nodeComparator); 327 map.put(child, s); 328 } 329 } 330 331 return (map == null ? null : new Selection(node, map)); 332 } 333 334 /** 335 * Write out all the nodes in this HelpTree. 336 * @param out the writer to which to write the nodes. 337 * If out is a com.sun.javatest.util.WrapWriter, it will be 338 * used directly, otherwise a WrapWriter will be created 339 * that will write to the given writer. 340 * @throws IOException if the is a problem writing the 341 * nodes. 342 * @see WrapWriter 343 */ 344 public void write(Writer out) throws IOException { 345 WrapWriter ww = getWrapWriter(out); 346 347 for (int i = 0; i < nodes.length; i++) { 348 write(ww, nodes[i]); 349 ww.write('\n'); 350 } 351 352 if (ww != out) 353 ww.flush(); 354 } 355 356 /** 357 * Write out selected nodes in this HelpTree. 358 * @param out the writer to which to write the nodes. 359 * If out is a com.sun.javatest.util.WrapWriter, it will be 360 * used directly, otherwise a WrapWriter will be created 361 * that will write to the given writer. 362 * @param s a Selection object containing the nodes to be written 363 * @throws IOException if the is a problem writing the 364 * nodes. 365 * @see WrapWriter 366 */ 367 public void write(Writer out, Selection s) throws IOException { 368 WrapWriter ww = getWrapWriter(out); 369 370 write(ww, s.map); 371 372 if (ww != out) 373 ww.flush(); 374 } 375 376 /** 377 * Write out a summary of all the nodes in this HelpTree. 378 * The summary will contain the name and description of the 379 * top level nodes, but not any of their children. 380 * @param out the writer to which to write the nodes. 381 * If out is a com.sun.javatest.util.WrapWriter, it will be 382 * used directly, otherwise a WrapWriter will be created 383 * that will write to the given writer. 384 * @throws IOException if the is a problem writing the 385 * nodes. 386 * @see WrapWriter 387 */ 388 public void writeSummary(Writer out) throws IOException { 389 WrapWriter ww = getWrapWriter(out); 390 391 for (int i = 0; i < nodes.length; i++) 392 writeHead(ww, nodes[i]); 393 394 if (ww != out) 395 ww.flush(); 396 } 397 398 /** 399 * Sets the comparator which will be used in {@link #find(String[]) find} methods 400 * method 401 * @param comparator Comparator to set 402 */ 403 public void setNodeComparator(Comparator<Node> comparator){ 404 nodeComparator = comparator; 405 } 406 407 /** 408 * Returns current comparator used in {@link #find(String[]) find} methods 409 * method 410 * @return current node comparator 411 */ 412 public Comparator<Node> getNodeComparator(){ 413 return nodeComparator; 414 } 415 416 private void write(WrapWriter out, Map<Node, Selection> m) throws IOException { 417 int margin = out.getLeftMargin(); 418 for (Iterator<Map.Entry<Node, Selection>> iter = m.entrySet().iterator(); iter.hasNext(); ) { 419 Map.Entry<Node, Selection> e = iter.next(); 420 Node node = (e.getKey()); 421 Selection s = (e.getValue()); 422 if (s.map == null) 423 write(out, node); 424 else { 425 writeHead(out, node); 426 out.setLeftMargin(margin + nodeIndent); 427 write(out, s.map); 428 out.setLeftMargin(margin); 429 } 430 if (margin == 0) 431 out.write('\n'); 432 } 433 } 434 435 private void write(WrapWriter out, Node node) throws IOException { 436 int baseMargin = out.getLeftMargin(); 437 438 writeHead(out, node); 439 440 Node[] children = node.children; 441 if (children != null && children.length > 0) { 442 out.setLeftMargin(baseMargin + nodeIndent); 443 for (int i = 0; i < children.length; i++) 444 write(out, children[i]); 445 } 446 447 out.setLeftMargin(baseMargin); 448 } 449 450 private void writeHead(WrapWriter out, Node node) throws IOException { 451 int baseMargin = out.getLeftMargin(); 452 453 String name = node.name; 454 String desc = node.description; 455 if (name != null) { 456 out.write(name); 457 out.write(' '); 458 if (desc != null) { 459 out.setLeftMargin(baseMargin + descriptionIndent); 460 if (out.getCharsOnLineSoFar() + 2 > out.getLeftMargin()) 461 out.write('\n'); 462 out.write(desc); 463 } 464 out.write('\n'); 465 } 466 467 out.setLeftMargin(baseMargin); 468 } 469 470 private boolean containsAllOf(String text, String[] words) { 471 for (int i = 0; i < words.length; i++) { 472 if (!contains(text, words[i])) 473 return false; 474 } 475 return true; 476 } 477 478 private boolean containsAnyOf(String text, String[] words) { 479 for (int i = 0; i < words.length; i++) { 480 if (contains(text, words[i])) 481 return true; 482 } 483 return false; 484 } 485 486 private boolean contains(String text, String word) { 487 int startIndex = text.toLowerCase().indexOf(word.toLowerCase()); 488 if (startIndex == -1) 489 return false; 490 491 int endIndex = startIndex + word.length(); 492 493 return ((startIndex == 0 || !Character.isLetter(text.charAt(startIndex - 1))) 494 && (endIndex == text.length() || !Character.isLetter(text.charAt(endIndex)))); 495 } 496 497 private WrapWriter getWrapWriter(Writer out) { 498 return (out instanceof WrapWriter ? (WrapWriter) out : new WrapWriter(out)); 499 } 500 501 private Node[] nodes; 502 503 private int nodeIndent = 4; 504 private int descriptionIndent = 16; 505 506 private static final int ALL = 1; 507 private static final int ANY = 2; 508 509 private Comparator<Node> nodeComparator = new Comparator<Node>() { 510 public int compare(Node n1, Node n2) { 511 int v = compareStrings(n1.name, n2.name); 512 return (v != 0 ? v : compareStrings(n1.description, n2.description)); 513 } 514 515 private int compareStrings(String s1, String s2) { 516 if (s1 == null && s2 == null) 517 return 0; 518 519 if (s1 == null || s2 == null) 520 return (s1 == null ? -1 : +1); 521 522 return s1.toLowerCase().compareTo(s2.toLowerCase()); 523 } 524 }; 525 }