1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 1996, 2010, 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.interview;
  28 
  29 import com.sun.javatest.util.Properties;
  30 
  31 import java.io.*;
  32 import java.nio.charset.StandardCharsets;
  33 import java.text.MessageFormat;
  34 import java.util.*;
  35 
  36 /**
  37  * An API (with a basic front-end application) for generating HTML printouts
  38  * of an {@link Interview interview}.
  39  */
  40 public class WizPrint
  41 {
  42     /**
  43      * This exception is to report problems that occur with command line arguments.
  44      */
  45     public static class BadArgs extends Exception
  46     {
  47         /**
  48          * Create a Fault.
  49          * @param i18n A resource bundle in which to find the detail message.
  50          * @param s The key for the detail message.
  51          */
  52         public BadArgs(ResourceBundle i18n, String s) {
  53             super(i18n.getString(s));
  54         }
  55 
  56         /**
  57          * Create a Fault.
  58          * @param i18n A resource bundle in which to find the detail message.
  59          * @param s The key for the detail message.
  60          * @param o An argument to be formatted with the detail message by
  61          * {@link java.text.MessageFormat#format}
  62          */
  63         public BadArgs(ResourceBundle i18n, String s, Object o) {
  64             super(MessageFormat.format(i18n.getString(s), new Object[] {o}));
  65         }
  66 
  67         /**
  68          * Create a Fault.
  69          * @param i18n A resource bundle in which to find the detail message.
  70          * @param s The key for the detail message.
  71          * @param o An array of arguments to be formatted with the detail message by
  72          * {@link java.text.MessageFormat#format}
  73          */
  74         public BadArgs(ResourceBundle i18n, String s, Object[] o) {
  75             super(MessageFormat.format(i18n.getString(s), o));
  76         }
  77     }
  78 
  79     /**
  80      * This exception is to report problems that occur while updating an interview.
  81      */
  82     public static class Fault extends Exception
  83     {
  84         /**
  85          * Create a Fault.
  86          * @param i18n A resource bundle in which to find the detail message.
  87          * @param s The key for the detail message.
  88          */
  89         public Fault(ResourceBundle i18n, String s) {
  90             super(i18n.getString(s));
  91         }
  92 
  93         /**
  94          * Create a Fault.
  95          * @param i18n A resource bundle in which to find the detail message.
  96          * @param s The key for the detail message.
  97          * @param o An argument to be formatted with the detail message by
  98          * {@link java.text.MessageFormat#format}
  99          */
 100         public Fault(ResourceBundle i18n, String s, Object o) {
 101             super(MessageFormat.format(i18n.getString(s), new Object[] {o}));
 102         }
 103 
 104         /**
 105          * Create a Fault.
 106          * @param i18n A resource bundle in which to find the detail message.
 107          * @param s The key for the detail message.
 108          * @param o An array of arguments to be formatted with the detail message by
 109          * {@link java.text.MessageFormat#format}
 110          */
 111         public Fault(ResourceBundle i18n, String s, Object[] o) {
 112             super(MessageFormat.format(i18n.getString(s), o));
 113         }
 114     }
 115 
 116     /**
 117      * Write a short description of the command line syntax and options to System.err.
 118      */
 119     public static void usage() {
 120         String prog = System.getProperty("program");
 121         if (prog == null)
 122             prog = formatI18N("wp.prog", WizPrint.class.getName());
 123         String msg = formatI18N("wp.usage", prog);
 124 
 125         boolean newline = true;
 126         for (int i = 0; i < msg.length(); i++) {
 127             char c = msg.charAt(i);
 128             if (c == '\n') {
 129                 System.err.println();
 130                 newline = true;
 131             }
 132             else {
 133                 System.err.print(c);
 134                 newline = false;
 135             }
 136         }
 137         if (!newline)
 138             System.err.println();
 139     }
 140 
 141     /**
 142      * Simple command-line front-end to the facilities of the API.
 143      * @param args Command line arguments.
 144      * @see #usage
 145      */
 146     public static void main(String[] args) {
 147         try {
 148             boolean path = false;;
 149             String interviewClassName = null;
 150             File interviewFile = null;
 151             File outFileName = null;
 152 
 153             for (int i = 0; i < args.length; i++) {
 154                 if (args[i].equals("-o") && i + 1 < args.length)
 155                     outFileName = new File(args[++i]);
 156                 else if (args[i].equals("-path"))
 157                     path = true;
 158                 else if (args[i].equals("-all"))
 159                     path = false;
 160                 else if (args[i].startsWith("-"))
 161                     throw new BadArgs(i18n, "wp.badArg", args[i]);
 162                 else if (i == args.length - 1) {
 163                     if (args[i].endsWith(".jti"))
 164                         interviewFile = new File(args[i]);
 165                     else
 166                         interviewClassName = args[i];
 167                 }
 168                 else
 169                     throw new BadArgs(i18n, "wp.badArg", args[i]);
 170             }
 171 
 172             Map<String, String> interviewData = null;
 173 
 174             if (interviewFile != null) {
 175                 try {
 176                     InputStream in = new BufferedInputStream(new FileInputStream(interviewFile));
 177                     interviewData = Properties.load(in);
 178                     interviewClassName = interviewData.get("INTERVIEW");
 179                 }
 180                 catch (FileNotFoundException e) {
 181                     throw new Fault(i18n, "wp.cantFindFile", interviewFile);
 182                 }
 183                 catch (IOException e) {
 184                     throw new Fault(i18n, "wp.cantReadFile", new Object[] { interviewFile, e });
 185                 }
 186             }
 187 
 188             if (interviewClassName == null)
 189                 throw new BadArgs(i18n, "wp.noInterview");
 190 
 191             if (outFileName == null) {
 192                 // try and create a default
 193                 if (interviewFile != null) {
 194                     String ip = interviewFile.getPath();
 195                     int dot = ip.lastIndexOf(".");
 196                     if (dot != -1)
 197                         outFileName = new File(ip.substring(0, dot) + ".html");
 198                 }
 199                 if (outFileName == null)
 200                     throw new BadArgs(i18n, "wp.noOutput");
 201             }
 202 
 203             Class<?> ic = Class.forName(interviewClassName, true, ClassLoader.getSystemClassLoader());
 204             Interview interview = (Interview)(ic.newInstance());
 205             Question[] questions;
 206 
 207             if (interviewData != null)
 208                 interview.load(interviewData);
 209 
 210             if (path) {
 211                 questions = interview.getPath();
 212             }
 213             else {
 214                 // enumerate questions, sort on tag
 215                 SortedVector v = new SortedVector();
 216                 for (Iterator<Question> iter = interview.getQuestions().iterator(); iter.hasNext(); ) {
 217                     Question q = iter.next();
 218                     v.insert(q);
 219                 }
 220                 questions = new Question[v.size()];
 221                 v.copyInto(questions);
 222             }
 223 
 224             try {
 225                 Writer out = new OutputStreamWriter(new FileOutputStream(outFileName), StandardCharsets.UTF_8);
 226                 WizPrint wp = new WizPrint(interview, questions);
 227                 wp.setShowTags(!path);
 228                 wp.setShowResponses(path);
 229                 wp.setShowResponseTypes(!path);
 230                 wp.write(out);
 231             }
 232             catch (IOException e) {
 233                 throw new Fault(i18n, "wp.cantWriteFile", new Object[] { outFileName, e });
 234             }
 235         }
 236         catch (BadArgs e) {
 237             System.err.println(formatI18N("wp.error", e.getMessage()));
 238             usage();
 239             System.exit(1);
 240         }
 241         catch (Fault e) {
 242             System.err.println(e.getMessage());
 243             System.exit(2);
 244         }
 245         catch (Throwable e) {
 246             e.printStackTrace();
 247             System.exit(3);
 248         }
 249     }
 250 
 251     /**
 252      * Create an object for printing the current set of questions from an interview.
 253      * @param interview The parent interview which contains the questions.
 254      */
 255     public WizPrint(Interview interview) {
 256         this(interview, interview.getPath());
 257     }
 258 
 259     /**
 260      * Create an object for printing a set of questions from an interview.
 261      * @param interview The parent interview which contains the questions.
 262      * @param questions The selected set of questions to be printed.
 263      */
 264     public WizPrint(Interview interview, Question[] questions) {
 265         this.interview = interview;
 266         this.questions = questions;
 267     }
 268 
 269     /**
 270      * Determine whether or not responses should be shown when the
 271      * interview is "printed" to HTML.
 272      * @return true if responses should be shown
 273      * @see #setShowResponses
 274      */
 275     public boolean getShowResponses() {
 276         return showResponses;
 277     }
 278 
 279     /**
 280      * Specify whether or not responses should be shown when the
 281      * interview is "printed" to HTML.
 282      * @param showResponses should be true if responses should be shown
 283      * @see #getShowResponses
 284      */
 285     public void setShowResponses(boolean showResponses) {
 286         this.showResponses = showResponses;
 287     }
 288 
 289     /**
 290      * Determine whether or not the types of responses should be shown
 291      * when the interview is "printed" to HTML.
 292      * @return true if the types of responses should be shown
 293      * @see #setShowResponseTypes
 294      */
 295     public boolean getShowResponseTypes() {
 296         return showResponseTypes;
 297     }
 298 
 299     /**
 300      * Specify whether or not the types of responses should be shown
 301      * when the interview is "printed" to HTML.
 302      * @param showResponseTypes should be true if the types of responses should be shown
 303      * @see #getShowResponseTypes
 304      */
 305     public void setShowResponseTypes(boolean showResponseTypes) {
 306         this.showResponseTypes = showResponseTypes;
 307     }
 308 
 309     /**
 310      * Determine whether or not question tags should be shown
 311      * when the interview is "printed" to HTML.
 312      * @return true if the questions' tags should be shown
 313      * @see #setShowTags
 314      */
 315     public boolean getShowTags() {
 316         return showTags;
 317     }
 318 
 319     /**
 320      * Specify whether or not question tags should be shown
 321      * when the interview is "printed" to HTML.
 322      * @param showTags should be true if the questions' tags should be shown
 323      * @see #getShowTags
 324      */
 325     public void setShowTags(boolean showTags) {
 326         this.showTags = showTags;
 327     }
 328 
 329     /**
 330      * Write the selected questions to the given stream, as a complete
 331      * HTML document.  The stream is closed after the writing is complete.
 332      * @param o the Writer to which to write the specified information
 333      * about an interview
 334      * @throws IOException if there are problems writing to the given Writer
 335      */
 336     public void write(Writer o) throws IOException {
 337         try {
 338             setWriter(o);
 339             startTag(DOCTYPE);
 340             newLine();
 341             startTag(HTML);
 342             newLine();
 343             startTag(HEAD);
 344             writeTag(TITLE, interview.getTitle());
 345             endTag(HEAD);
 346             newLine();
 347             startTag(BODY);
 348             writeTag(H1, interview.getTitle());
 349             newLine();
 350             writeIndex();
 351             writeQuestions();
 352             endTag(BODY);
 353             endTag(HTML);
 354             newLine();
 355         }
 356         finally {
 357             out.flush();
 358             out.close();
 359         }
 360     }
 361 
 362     /**
 363      * Write an index to the set of questions.
 364      */
 365     private void writeIndex() throws IOException {
 366         startTag(UL);
 367         newLine();
 368         for (int i = 0; i < questions.length; i++) {
 369             Question q = questions[i];
 370             startTag(LI);
 371             startTag(A);
 372             writeAttr(HREF, "#" + q.getTag());
 373             writeText(q.getSummary());
 374             endTag(A);
 375             newLine();
 376         }
 377         endTag(UL);
 378         newLine();
 379         newLine();
 380     }
 381 
 382     /**
 383      * Write the body of the document, containing the questions.
 384      */
 385     private void writeQuestions() throws IOException {
 386         startTag(HR);
 387         for (int i = 0; i < questions.length; i++) {
 388             if (i > 0) {
 389                 startTag(HR);
 390                 writeAttr(STYLE, TEXT_LEFT);
 391                 writeAttr(WIDTH, "25%");
 392             }
 393 
 394             Question q = questions[i];
 395 
 396             String tag = q.getTag();
 397             if (tag != null) {
 398                 startTag(A);
 399                 writeAttr(NAME, q.getTag());
 400                 endTag(A);
 401                 newLine();
 402             }
 403 
 404             writeTag(H3, q.getSummary());
 405             newLine();
 406 
 407             if (showTags) {
 408                 startTag(P);
 409                 startTag(I);
 410                 writeI18N("wp.tag");
 411                 if (tag == null)
 412                     writeTag(I, "null");  // I18N??
 413                 else
 414                     writeTag(B, tag);
 415                 endTag(I);
 416                 endTag(P);
 417                 newLine();
 418             }
 419 
 420             if (q instanceof ErrorQuestion) {
 421                 startTag(P);
 422                 startTag(FONT);
 423                 writeAttr(SIZE, "+1");
 424                 writeText(q.getText());
 425                 endTag(FONT);
 426                 endTag(P);
 427             }
 428             else {
 429                 startTag(P);
 430                 writeText(q.getText());
 431                 endTag(P);
 432                 newLine();
 433                 if (showResponseTypes)
 434                     writeResponseType(q);
 435                 if (showResponses)
 436                     writeResponse(q);
 437             }
 438         }
 439     }
 440 
 441     /**
 442      * Write the response to a question.
 443      * @param q The question whose response is to be printed.
 444      */
 445     private void writeResponse(Question q) throws IOException {
 446         if (q instanceof ChoiceArrayQuestion) {
 447             ChoiceArrayQuestion caq = (ChoiceArrayQuestion)q;
 448             writeResponse(caq.getValue(), caq.getChoices(), caq.getDisplayChoices());
 449         }
 450         else if (q instanceof ChoiceQuestion) {
 451             ChoiceQuestion cq = (ChoiceQuestion)q;
 452             if (cq.getChoices() == cq.getDisplayChoices())
 453                 writeResponse(cq.getValue());
 454             else
 455                 writeResponse(cq.getValue(), cq.getDisplayValue());
 456         }
 457         else if (q instanceof ErrorQuestion) {
 458             // no response
 459         }
 460         else if (q instanceof FileListQuestion) {
 461             FileListQuestion fq = (FileListQuestion)q;
 462             File[] f = fq.getValue();
 463             writeResponse(filesToStrings(f));
 464         }
 465         else if (q instanceof FileQuestion) {
 466             FileQuestion fq = (FileQuestion)q;
 467             File f = fq.getValue();
 468             writeResponse(f == null ? null : f.getPath());
 469         }
 470         else if (q instanceof FinalQuestion) {
 471             // no response
 472         }
 473         else if (q instanceof FloatQuestion) {
 474             FloatQuestion fq = (FloatQuestion)q;
 475             writeResponse(fq.getStringValue());
 476         }
 477         else if (q instanceof InetAddressQuestion) {
 478             InetAddressQuestion iq = (InetAddressQuestion)q;
 479             writeResponse(iq.getStringValue());
 480         }
 481         else if (q instanceof IntQuestion) {
 482             IntQuestion iq = (IntQuestion)q;
 483             writeResponse(iq.getStringValue());
 484         }
 485         else if (q instanceof NullQuestion) {
 486             // no response
 487         }
 488         else if (q instanceof PropertiesQuestion) {
 489             PropertiesQuestion pq = (PropertiesQuestion)q;
 490             writeResponse(pq);
 491         }
 492         else if (q instanceof StringQuestion) {
 493             StringQuestion sq = (StringQuestion)q;
 494             writeResponse(sq.getValue());
 495         }
 496         else if (q instanceof StringListQuestion) {
 497             StringListQuestion sq = (StringListQuestion)q;
 498             writeResponse(sq.getValue());
 499         }
 500         else if (q instanceof TreeQuestion) {
 501             TreeQuestion tq = (TreeQuestion)q;
 502             String[] nodes = tq.getValue();
 503             if (nodes == null || nodes.length == 0)
 504                 writeResponse(i18n.getString("wp.all"));
 505             else
 506                 writeResponse(nodes);
 507         }
 508         else {
 509             writeResponse(q.getStringValue());
 510         }
 511     }
 512 
 513     /**
 514      * Write a response.
 515      * @param s The text of the response.
 516      */
 517     private void writeResponse(String s) throws IOException {
 518         startTag(P);
 519         startTag(I);
 520         writeI18N("wp.response");
 521         if (s == null)
 522             writeI18N("wp.noResponse");
 523         else
 524             writeTag(B, s);
 525         endTag(I);
 526         endTag(P);
 527         newLine();
 528         newLine();
 529     }
 530 
 531     /**
 532      * Write a response.
 533      * @param response The text of the response.
 534      * @param displayText The display text of the response.
 535      */
 536     private void writeResponse(String response, String displayText) throws IOException {
 537         startTag(P);
 538         startTag(I);
 539         writeI18N("wp.response");
 540         if (response == null)
 541             writeI18N("wp.noResponse");
 542         else {
 543             writeTag(B, response);
 544             if (displayText != null) {
 545                 writeText(" ");
 546                 writeI18N("wp.display", displayText);
 547             }
 548         }
 549         endTag(I);
 550         endTag(P);
 551         newLine();
 552         newLine();
 553     }
 554 
 555     /**
 556      * Write a response based on a set of named boolean values
 557      * @param values An array of boolean values.
 558      * @param choices An array of matching names, one per boolean.
 559      */
 560     private void writeResponse(boolean[] values, String[] choices, String[] displayChoices) throws IOException {
 561         startTag(P);
 562         startTag(I);
 563         writeI18N("wp.response");
 564         if (values == null)
 565             writeI18N("wp.noResponse");
 566         else {
 567             for (int i = 0; i < values.length; i++) {
 568                 if (i > 0)
 569                     writeI18N("wp.listSep");
 570                 startTag(B);
 571                 /*
 572                 if (values[i])
 573                     writeText(choices[i]);
 574                 else
 575                     writeTag(STRIKE, choices[i]);
 576                 */
 577                 if (values[i] == false)
 578                     startTag(STRIKE);
 579 
 580                 writeText(choices[i]);
 581                 if (displayChoices != null && !equal(choices[i], displayChoices[i])) {
 582                     writeText(" ");
 583                     writeI18N("wp.display", displayChoices[i]);
 584                 }
 585 
 586                 if (values[i] == false)
 587                     endTag(STRIKE);
 588 
 589                 endTag(B);
 590             }
 591         }
 592         endTag(I);
 593         endTag(P);
 594         newLine();
 595         newLine();
 596     }
 597 
 598     /**
 599      * Write a response list
 600      * @param responses The text of the response.
 601      */
 602     private void writeResponse(String[] responses) throws IOException {
 603         startTag(P);
 604         startTag(I);
 605         writeI18N("wp.response");
 606         if (responses == null || responses.length == 0
 607                 || (responses.length == 1 && responses[0].isEmpty())) {
 608             writeI18N("wp.noResponse");
 609             endTag(I);
 610             endTag(P);
 611         } else {
 612             endTag(I);
 613             endTag(P);
 614             startTag(UL);
 615             for (int i = 0; i < responses.length; i++) {
 616                 if (responses[i] != null && !responses[i].isEmpty()) {
 617                     startTag(LI);
 618                     startTag(B);
 619                     startTag(I);
 620                     writeText(responses[i]);
 621                     endTag(I);
 622                     endTag(B);
 623                 }
 624             }
 625             endTag(UL);
 626         }
 627         newLine();
 628         newLine();
 629     }
 630 
 631     /**
 632      * Write the response output.  This methods is not generic like the others
 633      * because of the rich and optional API for this particular question.
 634      */
 635     private void writeResponse(PropertiesQuestion pq) throws IOException {
 636         String[] groups = pq.getGroups();
 637         String[] headers = new String[] {pq.getKeyHeaderName(),
 638                                          pq.getValueHeaderName()};
 639         String[][] nullGroup = pq.getGroup(null);
 640         if (nullGroup != null && nullGroup.length != 0)
 641             writePQTable(headers, nullGroup);
 642         else if (groups == null || groups.length == 0) {
 643             // no properties for this question it seems
 644             writeI18N("wp.noResponse");
 645         }
 646         else {
 647             // fall through
 648         }
 649 
 650         if (groups != null)
 651             for (int i = 0; i < groups.length; i++) {
 652                 // heading
 653                 startTag(BR);
 654                 startTag(B);
 655                 writeText(pq.getGroupDisplayName(groups[i]));
 656                 endTag(B);
 657 
 658                 // data
 659                 startTag(BR);
 660                 writePQTable(headers, pq.getGroup(groups[i]));
 661                 startTag(P);
 662             }   // for
 663     }
 664 
 665     private void writePQTable(String[] headers, String[][] values)
 666             throws IOException {
 667         if (values == null || values.length == 0)
 668             return;
 669 
 670         startTag(TABLE);
 671         writeAttr("border", "2");
 672         writeAttr("title",
 673                   i18n.getString("wp.table.title"));
 674         writeAttr("summary",
 675                   i18n.getString("wp.table.summ"));
 676         startTag(TR);
 677 
 678         // headers
 679         startTag(TH);
 680         writeAttr("align", "left");
 681         writeAttr("scope", "col");      // 508
 682         writeText(headers[0]);
 683         startTag(TH);
 684         writeAttr("align", "left");
 685         writeAttr("scope", "col");      // 508
 686         writeText(headers[1]);
 687 
 688         for (int i = 0; i < values.length; i++) {
 689             startTag(TR);
 690             startTag(TD);
 691             writeText(values[i][0]);
 692             startTag(TD);
 693             writeText(values[i][1]);
 694             endTag(TR);
 695         }   // for
 696 
 697         endTag(TABLE);
 698     }
 699 
 700     /**
 701      * Write the response to a question.
 702      * @param q The question whose response is to be printed.
 703      */
 704     private void writeResponseType(Question q) throws IOException {
 705         if (q instanceof ChoiceArrayQuestion) {
 706             ChoiceArrayQuestion cq = (ChoiceArrayQuestion)q;
 707             StringBuffer sb = new StringBuffer();
 708             sb.append(i18n.getString("wp.type.chooseAny"));
 709             String[] choices = cq.getChoices();
 710             for (int i = 0; i < choices.length; i++) {
 711                 if (i > 0)
 712                     sb.append(i18n.getString("wp.listSep"));
 713                 sb.append(choices[i] == null ? i18n.getString("wp.unset") : choices[i]);
 714             }
 715             writeResponseType(sb.toString());
 716         }
 717         else if (q instanceof ChoiceQuestion) {
 718             ChoiceQuestion cq = (ChoiceQuestion)q;
 719             StringBuffer sb = new StringBuffer();
 720             sb.append(i18n.getString("wp.type.chooseOne"));
 721             String[] choices = cq.getChoices();
 722             for (int i = 0; i < choices.length; i++) {
 723                 if (i > 0)
 724                     sb.append(i18n.getString("wp.listSep"));
 725                 sb.append(choices[i] == null ? i18n.getString("wp.unset") : choices[i]);
 726             }
 727             writeResponseType(sb.toString());
 728         }
 729         else if (q instanceof ErrorQuestion) {
 730             // no response
 731         }
 732         else if (q instanceof FileQuestion) {
 733             writeResponseType(i18n.getString("wp.type.file"));
 734         }
 735         else if (q instanceof FileListQuestion) {
 736             writeResponseType(i18n.getString("wp.type.fileList"));
 737         }
 738         else if (q instanceof FinalQuestion) {
 739             // no response
 740         }
 741         else if (q instanceof FloatQuestion) {
 742             FloatQuestion fq = (FloatQuestion)q;
 743             float lwb = fq.getLowerBound();
 744             float upb = fq.getUpperBound();
 745             writeResponseType(formatI18N("wp.type.float",
 746                                      new Object[] {
 747                                          new Integer(lwb == Float.MIN_VALUE ? 0 : 1),
 748                                          new Float(lwb),
 749                                          new Integer(upb == Float.MAX_VALUE ? 0 : 1),
 750                                          new Float(upb) }));
 751         }
 752         else if (q instanceof InetAddressQuestion) {
 753             writeResponseType(i18n.getString("wp.type.inetAddress"));
 754         }
 755         else if (q instanceof IntQuestion) {
 756             IntQuestion iq = (IntQuestion)q;
 757             int lwb = iq.getLowerBound();
 758             int upb = iq.getUpperBound();
 759             writeResponseType(formatI18N("wp.type.int",
 760                                      new Object[] {
 761                                          new Integer(lwb == Integer.MIN_VALUE ? 0 : 1),
 762                                          new Integer(lwb),
 763                                          new Integer(upb == Integer.MAX_VALUE ? 0 : 1),
 764                                          new Integer(upb) }));
 765         }
 766         else if (q instanceof NullQuestion) {
 767             // no response
 768         }
 769         else if (q instanceof StringQuestion) {
 770             writeResponseType(i18n.getString("wp.type.string"));
 771         }
 772         else if (q instanceof StringListQuestion) {
 773             writeResponseType(i18n.getString("wp.type.stringList"));
 774         }
 775         else if (q instanceof TreeQuestion) {
 776             writeResponseType(i18n.getString("wp.type.tree"));
 777         }
 778         else {
 779             startTag(P);
 780             writeTag(I, "unknown type of question; cannot determine response type");
 781             endTag(P);
 782         }
 783     }
 784 
 785     /**
 786      * Write a response.
 787      * @param s The text of the response.
 788      */
 789     private void writeResponseType(String s) throws IOException {
 790         startTag(P);
 791         startTag(I);
 792         if (showResponseTypes && showResponses)
 793             writeI18N("wp.responseType");  // is this being too clever?
 794         else
 795             writeI18N("wp.response");
 796         startTag(B);
 797         writeText(s);
 798         endTag(B);
 799         endTag(I);
 800         endTag(P);
 801         newLine();
 802         newLine();
 803     }
 804 
 805     private void writeI18N(String key) throws IOException {
 806         writeText(i18n.getString(key));
 807     }
 808 
 809     private void writeI18N(String key, Object arg) throws IOException {
 810         String s = formatI18N(key, arg);
 811         writeText(s);
 812     }
 813 
 814     /**
 815      * Convert a set of files to their string paths
 816      */
 817     private String[] filesToStrings(File[] f) {
 818         if (f == null)
 819             return null;
 820         String[] s = new String[f.length];
 821         for (int i = 0; i < s.length; i++)
 822             s[i] = f[i].getPath();
 823         return s;
 824     }
 825 
 826     /**
 827      * Write a newline.
 828      */
 829     private void newLine() throws IOException {
 830         if (state == IN_TAG) {
 831             out.write('>');
 832             state = IN_BODY;
 833         }
 834 
 835         out.newLine();
 836     }
 837 
 838     /**
 839      * Write an opening tag.
 840      * @param t The tag to be written
 841      */
 842     private void startTag(String t) throws IOException {
 843         if (state == IN_TAG)
 844             out.write('>');
 845 
 846         out.write('<');
 847         out.write(t);
 848         state = IN_TAG;
 849     }
 850 
 851     /**
 852      * Write an closing tag.
 853      * @param t The tag to be written
 854      */
 855     private void endTag(String t) throws IOException {
 856         if (state == IN_TAG)
 857             out.write('>');
 858 
 859         out.write("</");
 860         out.write(t);
 861         out.write('>');
 862         state = IN_BODY;
 863     }
 864 
 865     private void writeAttr(String name, String value) throws IOException {
 866         if (state != IN_TAG)
 867             throw new IllegalStateException();
 868 
 869         out.write(" ");
 870         out.write(name);
 871         out.write("=");
 872         boolean alpha = true;
 873         for (int i = 0; i < value.length() && alpha; i++)
 874             alpha = Character.isLetter(value.charAt(i));
 875         if (!alpha)
 876             out.write("\"");
 877         out.write(value);
 878         if (!alpha)
 879             out.write("\"");
 880     }
 881 
 882 
 883     /**
 884      * Write a string between opening and closing tags.
 885      * @param t The enclosing tag
 886      * @param s The enclosed text
 887      */
 888     private void writeTag(String t, String s) throws IOException {
 889         startTag(t);
 890         writeText(s);
 891         endTag(t);
 892     }
 893 
 894 
 895     /**
 896      * Write body text, applying any necessary escapes.
 897      */
 898     private void writeText(String s) throws IOException {
 899         if (state == IN_TAG) {
 900             out.write(">");
 901             state = IN_BODY;
 902         }
 903 
 904         if (s == null) {
 905             out.write("<i>");
 906             out.write(i18n.getString("wp.null"));
 907             out.write("</i>");
 908         }
 909         else {
 910             for (int i = 0; i < s.length(); i++) {
 911                 char c = s.charAt(i);
 912                 switch (c) {
 913                 case '\n':
 914                     out.write("<br>");
 915                     break;
 916                 case '<':
 917                 out.write("&lt;");
 918                 break;
 919                 case '>':
 920                     out.write("&gt;");
 921                     break;
 922                 case '&':
 923                     out.write("&amp;");
 924                     break;
 925                 default:
 926                     out.write(c);
 927                 }
 928             }
 929         }
 930     }
 931 
 932     private static String formatI18N(String key, Object arg) {
 933         return MessageFormat.format(i18n.getString(key), new Object[] { arg });
 934     }
 935 
 936     private static String formatI18N(String key, Object[] args) {
 937         return MessageFormat.format(i18n.getString(key), args);
 938     }
 939 
 940     private void setWriter(Writer o) {
 941         if (out instanceof BufferedWriter)
 942             out = (BufferedWriter)o;
 943         else
 944             out = new BufferedWriter(o);
 945     }
 946 
 947     private static boolean equal(String s1, String s2) {
 948         return (s1 == null ? s2 == null : s1.equals(s2));
 949     }
 950 
 951     private Interview interview;
 952     private Question[] questions;
 953     private BufferedWriter out;
 954     private boolean showResponses;
 955     private boolean showResponseTypes;
 956     private boolean showTags;
 957 
 958     private int state;
 959     private static final int IN_TAG = 1;
 960     private static final int IN_BODY = 2;
 961 
 962     private static final ResourceBundle i18n = ResourceBundle.getBundle("com.sun.interview.i18n");
 963 
 964     private static final String DOCTYPE = "!DOCTYPE HTML";
 965 
 966     private static final String A = "a";
 967     private static final String B = "b";
 968     private static final String BODY = "body";
 969     private static final String BR = "br";
 970     private static final String FONT = "font";
 971     private static final String H1 = "h1";
 972     private static final String H3 = "h3";
 973     private static final String HEAD = "head";
 974     private static final String HR = "hr";
 975     private static final String HREF = "href";
 976     private static final String HTML = "html";
 977     private static final String I = "i";
 978     private static final String TEXT_LEFT = "text-align:left;";
 979     private static final String LI = "li";
 980     private static final String NAME = "name";
 981     private static final String P = "p";
 982     private static final String SIZE = "size";
 983     private static final String STRIKE = "strike";
 984     private static final String TABLE = "table";
 985     private static final String TITLE = "title";
 986     private static final String TD = "td";
 987     private static final String TH = "th";
 988     private static final String TR = "tr";
 989     private static final String UL = "ul";
 990     private static final String WIDTH = "width";
 991     private static final String STYLE="style";
 992 
 993     private static class SortedVector
 994     {
 995         public SortedVector() {
 996             v = new Vector<Question>();
 997         }
 998 
 999         public SortedVector(int initialSize) {
1000             v = new Vector<>(initialSize);
1001         }
1002 
1003 
1004         public int size() {
1005             return v.size();
1006         }
1007 
1008         public Question elementAt(int index) {
1009             return v.elementAt(index);
1010         }
1011 
1012         public void insert(Question o) {
1013             v.insertElementAt(o, findSortIndex(o));
1014         }
1015 
1016         public void insert(Question o, boolean ignoreDuplicates) {
1017             int i = findSortIndex(o);
1018             if (ignoreDuplicates && (i < v.size()) && (compare(o, v.elementAt(i)) == 0))
1019                 return;
1020 
1021             v.insertElementAt(o, i);
1022         }
1023 
1024         public void copyInto(Question[] target) {
1025             v.copyInto(target);
1026         }
1027 
1028         protected int compare(Question o1, Question o2) {
1029             String p1 = o1.getTag();
1030             String p2 = o2.getTag();
1031 
1032             if (p1 == null && p2 == null)
1033                 return 0;
1034 
1035             if (p1 == null)
1036                 return -1;
1037 
1038             if (p2 == null)
1039                 return +1;
1040 
1041             return p1.compareTo(p2);
1042         }
1043 
1044         private int findSortIndex(Question o) {
1045             int lower = 0;
1046             int upper = v.size() - 1;
1047             int mid = 0;
1048 
1049             if (upper == -1) {
1050                 return 0;
1051             }
1052 
1053             int cmp = 0;
1054             Question last = v.elementAt(upper);
1055             cmp = compare(o, last);
1056             if (cmp > 0)
1057                 return upper + 1;
1058 
1059             while (lower <= upper) {
1060                 mid = lower + ((upper - lower) / 2);
1061                 Question entry = v.elementAt(mid);
1062                 cmp = compare(o, entry);
1063 
1064                 if (cmp == 0) {
1065                     // found a matching description
1066                     return mid;
1067                 } else if (cmp < 0) {
1068                     upper = mid - 1;
1069                 } else {
1070                     lower = mid + 1;
1071                 }
1072             }
1073 
1074             // didn't find it, but we indicate the index of where it would belong.
1075             return (cmp < 0) ? mid : mid + 1;
1076         }
1077 
1078         private Vector<Question> v;
1079     }
1080 
1081 }