1 /*
   2  * $Id$
   3  *
   4  * Copyright (c) 2001, 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.interview;
  28 
  29 import java.text.MessageFormat;
  30 import java.util.Map;
  31 import java.util.ResourceBundle;
  32 import java.util.Vector;
  33 
  34 /**
  35  * A {@link Question question} to support the construction of an
  36  * open-ended set of complex values determined by a specified subinterview.
  37  *
  38  * <p>A "loop" is created by creating an instance of a subtype of ListQuestion.
  39  * The subtype must implement createBody() to create instances of the subinterview
  40  * for the body of the loop. getNext() should return the next question after the
  41  * loop has been completed.
  42  *
  43  * <p>Computationally, this question behaves more like a "fork" than a "loop".
  44  * Semantically, it is as though all the bodies are evaluated together,
  45  * in parallel, rather than serially one after the other.
  46  * In the  GUI presentation, it is expected that only one body is displayed
  47  * at a time, and that the user can choose which body is viewed.
  48  * This avoids having all the loops unrolled all the time in the display of
  49  * the current path.
  50  * Internally, each ListQuestion has a sibling that is created automatically,
  51  * and together, these two questions bracket the set of loop bodies.
  52  */
  53 public abstract class ListQuestion extends Question
  54 {
  55     /**
  56      * A special subtype of Interview to use for the questions in the body of
  57      * a loop. The body has an index, which identifies its position within
  58      * the list of current loop bodies, and a summary string to identify
  59      * this instance of the loop body.
  60      */
  61     public static abstract class Body extends Interview {
  62         /**
  63          * Create an instance of a loop body.
  64          * @param question The loop question for which this is a body instance.
  65          * @param index The position of this body within the set of all the bodies.
  66          * The value is normally just a hint (albeit a possibly string one).
  67          * The index will be updated if necessary when the body is actually
  68          * set as one of the bodies of the loop.
  69          */
  70         protected Body(ListQuestion question, int index) {
  71             super(question.getInterview(),
  72                   question.getBaseTag() + "." + index);
  73             this.question = question;
  74             this.index = index;
  75         }
  76 
  77         /**
  78          * Get a string to uniquely identify this instance of the loop body,
  79          * or null if there is insufficient information so far to make a
  80          * determination. The string will be used to identify the loop body
  81          * to the user.
  82          * @return a string to uniquely identify this instance of the loop body,
  83          * or null if there is insufficient information so far to make a
  84          * determination.
  85          */
  86         public abstract String getSummary();
  87 
  88         /**
  89          * Get the position of this loop body within the set of all the loop
  90          * bodies for the question.
  91          * @return the position of this loop body within the set of all the loop
  92          * bodies for the question
  93          */
  94         public int getIndex() {
  95             return index;
  96         }
  97 
  98         /**
  99          * Set the recorded position of this loop body within the set
 100          * of all the loop bodies for the question. By itself, this method
 101          * does not actually affect the loop bodies.
 102          * See {@link ListQuestion#setBodies} for details on updating the
 103          * bodies of the loop.
 104          * @param newIndex the new position of this loop body within the
 105          * set of all the loop bodies for the question
 106          */
 107         void setIndex(int newIndex) {
 108             if (newIndex != index) {
 109                 index = newIndex;
 110                 setBaseTag(question.getBaseTag() + "." + index);
 111             }
 112         }
 113 
 114         /**
 115          * Get a default summary to be used to identify this instance of the
 116          * the loop body, to be used when getSummary() returns null.
 117          * The summary will be a standard prefix string possibly followed
 118          * by a number to distinguish between multiple bodies using the
 119          * default summary. The default summary will be unique and persist
 120          * for the life of this body or until getSummary() returns a non-null
 121          * value.
 122          * @return a default summary to be used to identify this instance of the
 123          * the loop body, to be used when getSummary() returns null.
 124          */
 125         public String getDefaultSummary() {
 126             if (defaultSummary == null) {
 127                 // recycle any default summaries that are no longer required
 128                 Vector<Body> bodies = question.bodies;
 129                 for (int i = 0; i < bodies.size(); i++) {
 130                     Body b = (bodies.elementAt(i));
 131                     if (b.defaultSummary != null
 132                         && b.getSummary() != null
 133                         && !b.defaultSummary.equals(b.getSummary())) {
 134                         b.defaultSummary = null;
 135                     }
 136                 }
 137 
 138                 // try and find an unused unique value v not used by any other default summary
 139                 for (int v = 0; v < bodies.size(); v++) {
 140                     String s = MessageFormat.format(i18n.getString("lp.newValue"),
 141                                                     new Object[] { new Integer(v) });
 142                     // check s is not the same as any current default summary;
 143                     // if it is, reset it to null
 144                     for (int i = 0; i < bodies.size(); i++) {
 145                         Body b = (bodies.elementAt(i));
 146                         if (s.equals(b.defaultSummary)) {
 147                             s = null;
 148                             break;
 149                         }
 150                     }
 151                     // if s is not null, it is unique, different from other default
 152                     // summaries, so use it...
 153                     if (s != null) {
 154                         defaultSummary = s;
 155                         break;
 156                     }
 157                 }
 158             }
 159 
 160             return defaultSummary;
 161         }
 162 
 163         /**
 164          * Check if this body has been completed. It is considered to have
 165          * been completed if none of the questions in this body
 166          * on the current path return null as the result of getNext().
 167          * @return true is this body has been completed.
 168          */
 169         public boolean isBodyFinishable() {
 170             return isInterviewFinishable();
 171         }
 172 
 173         private ListQuestion question;
 174         private int index;
 175         private String defaultSummary;
 176     };
 177 
 178     /**
 179      * Create a question with a nominated tag.
 180      * @param interview The interview containing this question.
 181      * @param tag A unique tag to identify this specific question.
 182      */
 183     protected ListQuestion(Interview interview, String tag) {
 184         super(interview, tag);
 185 
 186         if (this instanceof EndQuestion) {
 187             end = (EndQuestion) this;
 188             bodies = null;
 189         }
 190         else {
 191             end = new EndQuestion(interview, tag, this);
 192             bodies = new Vector<>();
 193         }
 194     }
 195 
 196     /**
 197      * Create a new instance of a body for this loop question.
 198      * The body is a subinterview that contains the questions
 199      * for the body of the loop.
 200      * The body does not become one of the set of bodies for the loop
 201      * until the set is updated with {@link #setBodies}.
 202      * @param index the position that this body will have within
 203      * the set of bodies for the loop. This value should be passed
 204      * through to the Body constructor.
 205      * @return a new instance of a body for this loop question
 206      */
 207     public abstract Body createBody(int index);
 208 
 209     /**
 210      * Check if this is the question that appears at the beginning or
 211      * at the end of the loop. When a ListQuestion is created, a sibling
 212      * is automatically created that will appear at the end of the loop.
 213      * @return false if this is the main question, that appears at the
 214      * head of the loop, or true if this is the question that is
 215      * automatically created to appear at the end of the lop.
 216      */
 217     public final boolean isEnd() {
 218         return (this instanceof EndQuestion);
 219     }
 220 
 221     /**
 222      * Get the sibling question that appears at the other end of the loop.
 223      * When a ListQuestion is created, a sibling is automatically created
 224      * that will appear at the end of the loop. From either of these questions,
 225      * you can use this method to get at the other one.
 226      * @return the sibling question that appears at the other end of the loop
 227      */
 228     public ListQuestion getOther() {
 229         return end;
 230     }
 231 
 232     /**
 233      * Get the currently selected loop body, or null, as selected by by setValue.
 234      * @return the currently selected loop body, or null, if none.
 235      */
 236     public Body getSelectedBody() {
 237         if (value >= 0 && value < bodies.size())
 238             return bodies.elementAt(value);
 239         else
 240             return null;
 241     }
 242 
 243     /**
 244      * Get the index of the currently selected loop body, or an out of range
 245      * value (typically less than zero) if none is selected.
 246      * @return the index of the currently selected loop body, or an out of range
 247      * value (typically less than zero) if none is selected
 248      * @see #setValue
 249      */
 250     public int getValue() {
 251         return value;
 252     }
 253 
 254     /**
 255      * Verify this question is on the current path, and if it is,
 256      * return the current value.
 257      * @return the current value of this question
 258      * @throws Interview.NotOnPathFault if this question is not on the
 259      * current path
 260      * @see #getValue
 261      */
 262     public int getValueOnPath()
 263         throws Interview.NotOnPathFault
 264     {
 265         interview.verifyPathContains(this);
 266         return getValue();
 267     }
 268 
 269     /**
 270      * Get a string representation of the index of the currently
 271      * selected loop body, or an out of range value
 272      * (typically less than zero) if none is selected.
 273      */
 274     public String getStringValue() {
 275         return String.valueOf(value);
 276     }
 277 
 278     /**
 279      * Set the index of the loop body to be selected.
 280      * If the value is out of range, no loop body will be selected.
 281      * @param newValue the index of the loop body to be selected
 282      * @see #getValue
 283      */
 284     public void setValue(int newValue) {
 285         int oldValue = value;
 286         value = newValue;
 287         if (normalizeValue(value) != normalizeValue(oldValue)) {
 288             interview.updatePath(this);
 289             interview.setEdited(true);
 290         }
 291     }
 292 
 293     private int normalizeValue(int value) {
 294         return (value >= 0 && value < bodies.size() ? value : -1);
 295     }
 296 
 297     /**
 298      * Set the index of the loop body to be selected.
 299      * If the value is out of range, no loop body will be selected.
 300      * @param s a string containing the index of the loop body
 301      * to be selected. If the string does not contain a valid
 302      * integer, the value will be set to -1.
 303      * @see #getValue
 304      */
 305     public void setValue(String s) {
 306         try {
 307             if (s != null) {
 308                 setValue(Integer.parseInt(s));
 309                 return;
 310             }
 311         }
 312         catch (NumberFormatException e) {
 313             // ignore
 314         }
 315         setValue(-1);
 316     }
 317 
 318     /**
 319      * Check if the question currently has a valid response.
 320      * For a ListQuestion, this is normally true.
 321      * @return true if the question currently has a valid response,
 322      * and false otherwise.
 323      **/
 324     public boolean isValueValid() {
 325         return true;  // should probably reflect whether bodies are valid
 326     }
 327 
 328     /**
 329      * Check if the question always has a valid response.
 330      * For a ListQuestion, this is normally false.
 331      * @return true if the question always has a valid response,
 332      * and false otherwise.
 333      **/
 334     public boolean isValueAlwaysValid() {
 335         return false;
 336     }
 337 
 338     /**
 339      * Remove all the bodies currently allocated for this question,
 340      * and set the value of the question to indicate no loop
 341      * body selected.
 342      */
 343     public void clear() {
 344         setValue(Integer.MIN_VALUE);
 345         bodies.setSize(0);
 346     }
 347 
 348     /**
 349      * Get the summary text for the end question.
 350      * When a ListQuestion is created, a sibling is automatically created
 351      * that will appear at the end of the loop.
 352      * Override this method to override the default behavior to
 353      * get the summary text from the standard resource bundle.
 354      * The tag for the end question is the same as the tag for the
 355      * main question, with ".end" appended.
 356      * @return the summary text for the end question
 357      * @see #getSummary
 358      * @see #getOther
 359      */
 360     public String getEndSummary() {
 361         return end.getDefaultSummary();
 362     }
 363 
 364     /**
 365      * Get the question text for the end question.
 366      * When a ListQuestion is created, a sibling is automatically created
 367      * that will appear at the end of the loop.
 368      * Override this method to override the default behavior to
 369      * get the question text from the standard resource bundle.
 370      * The tag for the end question is the same as the tag for the
 371      * main question, with ".end" appended.
 372      * @return the question text for the end question
 373      * @see #getEndTextArgs
 374      * @see #getText
 375      * @see #getOther
 376      */
 377     public String getEndText() {
 378         return end.getDefaultText();
 379     }
 380 
 381 
 382     /**
 383      * Get the formatting arguments for the question text for the end question.
 384      * When a ListQuestion is created, a sibling is automatically created
 385      * that will appear at the end of the loop.
 386      * Override this method to override the default behavior to
 387      * return null.
 388      * @return the formatting arguments for the question text for the end question
 389      * @see #getEndText
 390      * @see #getTextArgs
 391      * @see #getOther
 392      */
 393     public Object[] getEndTextArgs() {
 394         return end.getDefaultTextArgs();
 395     }
 396 
 397     protected void load(Map<String, String> data) {
 398         bodies.setSize(0);
 399         String c = data.get(tag + ".count");
 400         if (c != null && c.length() > 0) {
 401             try {
 402                 int n = Integer.parseInt(c);
 403                 for (int i = 0; i < n; i++)
 404                     bodies.add(createBody(i));
 405                 // once the bodies are created as children of this question's
 406                 // interview, they'll be reloaded by the interviews load method
 407             }
 408             catch (NumberFormatException ignore) {
 409             }
 410         }
 411 
 412         String v = data.get(tag + ".curr");
 413         if (v == null || v.length() == 0)
 414             value = 0;
 415         else {
 416             try {
 417                 value = Integer.parseInt(v);
 418             }
 419             catch (NumberFormatException ignore) {
 420                 value = 0;
 421             }
 422         }
 423     }
 424 
 425     protected void save(Map<String, String> data) {
 426         data.put(tag + ".count", String.valueOf(bodies.size()));
 427         data.put(tag + ".curr", String.valueOf(value));
 428     }
 429 
 430     /**
 431      * Get the set of bodies currently allocated within the loop.
 432      * @return the set of bodies currently allocated within the loop
 433      * @see #setBodies
 434      */
 435     public Body[] getBodies() {
 436         Body[] b = new Body[bodies.size()];
 437         bodies.copyInto(b);
 438         return b;
 439     }
 440 
 441     /**
 442      * Get the number of bodies (iterations) currently allocated within the loop.
 443      * @return the number of bodies currently allocated within the loop
 444      */
 445     public int getBodyCount() {
 446         return (bodies == null ? 0 : bodies.size());
 447     }
 448 
 449     /**
 450      * Get a specified body from the loop.
 451      * @param index the position of the desired body within the set of bodies
 452      * currently allocated within the loop.
 453      * @return the specified body
 454      * @throws ArrayIndexOutOfBoundsException if index does not identify a
 455      * valid body
 456      */
 457     public Body getBody(int index) {
 458         return bodies.elementAt(index);
 459     }
 460 
 461     /**
 462      * Set the set of bodies allocated within the loop, and the
 463      * index of one which should be selected.
 464      * The bodies will normally come from a combination of
 465      * the bodies returned from getBodies() or new ones
 466      * created by createBody().
 467      * @param newBodies the set of bodies to be taken as the
 468      * new set of loop bodies
 469      * @param newValue the index of the body which should be
 470      * the selected body.
 471      * @see #getBodies
 472      */
 473     public void setBodies(Body[] newBodies, int newValue) {
 474         Body oldSelectedBody = getSelectedBody();
 475         int oldIncompleteCount = getIncompleteBodyCount();
 476 
 477         boolean edited = false;
 478 
 479         if (newBodies.length != bodies.size()) {
 480             bodies.setSize(newBodies.length);
 481             edited = true;
 482         }
 483 
 484         for (int i = 0; i < newBodies.length; i++) {
 485             Body b = newBodies[i];
 486             if (b != bodies.elementAt(i)) {
 487                 b.setIndex(i);
 488                 bodies.setElementAt(b, i);
 489                 edited = true;
 490             }
 491         }
 492 
 493         value = newValue;
 494         Body newSelectedBody = getSelectedBody();
 495         int newIncompleteCount = getIncompleteBodyCount();
 496 
 497         if (newSelectedBody != oldSelectedBody
 498             || ((oldIncompleteCount == 0) != (newIncompleteCount == 0))) {
 499             interview.updatePath(this);
 500         }
 501 
 502         interview.setEdited(edited);
 503     }
 504 
 505     /**
 506      * Get the number of bodies for this loop that are currently incomplete,
 507      * as determined by {@link  Body#isBodyFinishable}.
 508      * @return the number of bodies for this loop that are currently incomplete.
 509      */
 510     public int getIncompleteBodyCount() {
 511         int count = 0;
 512         for (int i = 0; i < bodies.size(); i++) {
 513             Body b = bodies.elementAt(i);
 514             if (!b.isInterviewFinishable())
 515                 count++;
 516         }
 517         return count;
 518     }
 519 
 520 
 521     private final EndQuestion end;
 522     private final Vector<Body> bodies;
 523     private int value;
 524 
 525     private static final ResourceBundle i18n = Interview.i18n;
 526 
 527     private static class EndQuestion extends ListQuestion {
 528         EndQuestion(Interview interview, String tag, ListQuestion head) {
 529             super(interview, tag + ".end");
 530             this.head = head;
 531         }
 532 
 533         public Question getNext() {
 534             boolean allBodiesFinishable = true;
 535             for (int i = 0; i < head.getBodyCount(); i++) {
 536                 Body b = head.getBody(i);
 537                 if (!b.isInterviewFinishable()) {
 538                     allBodiesFinishable = false;
 539                     break;
 540                 }
 541             }
 542 
 543             return (allBodiesFinishable ? head.getNext() : null);
 544         }
 545 
 546         public String getSummary() {
 547             // ListQuestion.getEndSummary can be overridden, but defaults to
 548             // getDefaultSummary() here, which calls super.getSummary()
 549             return head.getEndSummary();
 550         }
 551 
 552         String getDefaultSummary() {
 553             return super.getSummary();
 554         }
 555 
 556         public String getText() {
 557             // ListQuestion.getEndText can be overridden, but defaults to
 558             // getDefaultText() here, which calls super.getText()
 559             return head.getEndText();
 560         }
 561 
 562         String getDefaultText() {
 563             return super.getText();
 564         }
 565 
 566         public Object[] getTextArgs() {
 567             // ListQuestion.getEndTextArgs can be overridden, but defaults to
 568             // getDefaultTextArgs() here, which calls super.getText()
 569             return head.getEndTextArgs();
 570         }
 571 
 572         Object[] getDefaultTextArgs() {
 573             return super.getTextArgs();
 574         }
 575 
 576         public int getValue() {
 577             return head.getValue();
 578         }
 579 
 580         public String getStringValue() {
 581             return head.getStringValue();
 582         }
 583 
 584         public void setValue(int value) {
 585             head.setValue(value);
 586         }
 587 
 588         public void setValue(String s) {
 589             head.setValue(s);
 590         }
 591 
 592         public Body getSelectedBody() {
 593             return head.getSelectedBody();
 594         }
 595 
 596         public Body createBody(int index) {
 597             return head.createBody(index);
 598         }
 599 
 600         public ListQuestion getOther() {
 601             return head;
 602         }
 603 
 604         public void clear() {
 605             head.clear();
 606         }
 607 
 608         protected void load(Map<String, String> data) {
 609         }
 610 
 611         protected void save(Map<String, String> data) {
 612         }
 613 
 614         public Body[] getBodies() {
 615             return head.getBodies();
 616         }
 617 
 618         public int getBodyCount() {
 619             return head.getBodyCount();
 620         }
 621 
 622         public Body getBody(int index) {
 623             return head.getBody(index);
 624         }
 625 
 626         public void setBodies(Body[] newBodies, int newValue) {
 627             head.setBodies(newBodies, newValue);
 628         }
 629 
 630         public int getIncompleteBodyCount() {
 631             return head.getIncompleteBodyCount();
 632         }
 633 
 634         private ListQuestion head;
 635 
 636     }
 637 }