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 map) {
 172             this(null, map);
 173         }
 174 
 175         private Selection(Node node, Map map) {
 176             this.node = node;
 177             this.map = map;
 178         }
 179 
 180         private Node node;
 181         private Map 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 m) throws IOException {
 417         int margin = out.getLeftMargin();
 418         for (Iterator iter = m.entrySet().iterator(); iter.hasNext(); ) {
 419             Map.Entry e = (Map.Entry) (iter.next());
 420             Node node = (Node) (e.getKey());
 421             Selection s = (Selection) (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 }