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 }