1 /* 2 * $Id$ 3 * 4 * Copyright (c) 1996, 2015, 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.io.File; 30 import java.net.MalformedURLException; 31 import java.net.URL; 32 import java.net.URLClassLoader; 33 import java.security.AccessController; 34 import java.security.PrivilegedAction; 35 import java.text.MessageFormat; 36 import java.util.ArrayList; 37 import java.util.Arrays; 38 import java.util.HashMap; 39 import java.util.HashSet; 40 import java.util.Iterator; 41 import java.util.LinkedHashMap; 42 import java.util.List; 43 import java.util.Locale; 44 import java.util.Map; 45 import java.util.MissingResourceException; 46 import java.util.Properties; 47 import java.util.ResourceBundle; 48 import java.util.Set; 49 import java.util.Vector; 50 51 52 //import com.sun.javatest.util.DirectoryClassLoader; 53 54 /** 55 * The base class for an interview: a series of {@link Question questions}, to be 56 * presented to the user via some tool such as an assistant or wizard. 57 * Interviews may be stand-alone, or designed to be part of other interviews. 58 */ 59 public class Interview 60 { 61 //----- inner classes ---------------------------------------- 62 63 /** 64 * This exception is to report problems that occur while updating an interview. 65 */ 66 public static class Fault extends Exception 67 { 68 /** 69 * Create a Fault. 70 * @param i18n A resource bundle in which to find the detail message. 71 * @param s The key for the detail message. 72 */ 73 public Fault(ResourceBundle i18n, String s) { 74 super(i18n.getString(s)); 75 } 76 77 /** 78 * Create a Fault. 79 * @param i18n A resource bundle in which to find the detail message. 80 * @param s The key for the detail message. 81 * @param o An argument to be formatted with the detail message by 82 * {@link java.text.MessageFormat#format} 83 */ 84 public Fault(ResourceBundle i18n, String s, Object o) { 85 super(MessageFormat.format(i18n.getString(s), o)); 86 } 87 88 /** 89 * Create a Fault. 90 * @param i18n A resource bundle in which to find the detail message. 91 * @param s The key for the detail message. 92 * @param o An array of arguments to be formatted with the detail message by 93 * {@link java.text.MessageFormat#format} 94 */ 95 public Fault(ResourceBundle i18n, String s, Object[] o) { 96 super(MessageFormat.format(i18n.getString(s), o)); 97 } 98 } 99 100 /** 101 * This exception is thrown when a question is expected to be on 102 * the current path, and is not. 103 */ 104 public static class NotOnPathFault extends Fault 105 { 106 NotOnPathFault(Question q) { 107 super(i18n, "interview.questionNotOnPath", q.getTag()); 108 } 109 } 110 111 /** 112 * Not for use, provided for backwards binary compatibility. 113 * @deprecated No longer used in this API, direct JavaHelp usage was removed. 114 */ 115 @Deprecated 116 public static class BadHelpFault extends Fault { 117 public BadHelpFault(ResourceBundle i18n, String s, Object e) { 118 super(i18n, s, e); 119 } 120 }; 121 122 123 /** 124 * Not for use, provided for backwards binary compatibility. 125 * @deprecated No longer used in this API, direct JavaHelp usage was removed. 126 */ 127 @Deprecated 128 public static class HelpNotFoundFault extends Fault { 129 public HelpNotFoundFault(ResourceBundle i18n, String s, String name) { 130 super(i18n, s, name); 131 } 132 }; 133 134 135 /** 136 * An observer interface for receiving notifications as the state of 137 * the interview is updated. 138 */ 139 public static interface Observer { 140 /** 141 * Invoked when the current question in the interview has been changed. 142 * @param q the new current question 143 */ 144 void currentQuestionChanged(Question q); 145 146 /** 147 * Invoked when the set of questions in the current path has been 148 * changed. This is normally because the response to one of the 149 * questions on the path has been changed, thereby causing a change 150 * to its successor questions. 151 */ 152 void pathUpdated(); 153 } 154 155 //----- constructors ---------------------------------------- 156 157 /** 158 * Create a top-level interview. 159 * @param tag A tag that will be used to qualify the tags of any 160 * questions in this interview, to help ensure uniqueness of those 161 * tags. 162 */ 163 protected Interview(String tag) { 164 this(null, tag); 165 } 166 167 /** 168 * Create an interview to be used as part of another interview. 169 * @param parent The parent interview of which this is a part. 170 * @param baseTag A name that will be used to qualify the tags of any 171 * questions in this interview, to help ensure uniqueness of those 172 * tags. It will be combined with the parent's tag if that has been 173 * specified. 174 */ 175 protected Interview(Interview parent, String baseTag) { 176 this.parent = parent; 177 setBaseTag(baseTag); 178 179 if (parent == null) 180 root = this; 181 else { 182 parent.add(this); 183 root = parent.root; 184 semantics = parent.getInterviewSemantics(); 185 } 186 } 187 188 //----- basic facilities ---------------------------------------- 189 190 /** 191 * Get the parent interview for which this is a child. 192 * @return the parent interview, or null if no parent has been specified. 193 */ 194 public Interview getParent() { 195 return parent; 196 } 197 198 /** 199 * Get a tag used to qualify the tags of questions in this interview. 200 * @return the title 201 */ 202 public String getTag() { 203 return tag; 204 } 205 206 /** 207 * Set a descriptive title to be used to annotate this interview. 208 * @param title A short descriptive title. 209 * @see #getTitle 210 */ 211 protected void setTitle(String title) { 212 this.title = title; 213 } 214 215 /** 216 * Get a descriptive title associated with this interview. 217 * If not specified, the system will try and locate the title in the 218 * interview's resource bundle, using the resource name <code>title</code>. 219 * of the interview. 220 * @return the title 221 * @see #setTitle 222 */ 223 public String getTitle() { 224 if (title == null) { 225 // Need to dance a bit here to avoid "title" being picked up 226 // by the i18n validation scripts as a necessary key in i18n 227 // instead of bundle. Another solution would be to make 228 // getI18NString static and pass the resource bundle in as the 229 // first arg. 230 String titleKey = "title"; 231 title = getI18NString(titleKey).trim(); 232 } 233 234 return title; 235 } 236 237 /** 238 * Set a default image to be used for the questions of an interview. 239 * @param u A URL for the image 240 * @see Question#setImage 241 * @see Question#getImage 242 * @see #getDefaultImage 243 */ 244 protected void setDefaultImage(URL u) { 245 defaultImage = u; 246 } 247 248 /** 249 * Get a default image to be used for the questions of an interview. 250 * If no default has been set for this interview, the parent's 251 * default image (if any) is used instead. 252 * @return a URL for the default image to be used 253 * @see #setDefaultImage 254 */ 255 public URL getDefaultImage() { 256 if (defaultImage == null && parent != null) 257 return parent.getDefaultImage(); 258 259 return defaultImage; 260 } 261 262 /** 263 * Set the base name of the resource bundle used to look up 264 * internationalized strings, such as the title and text of each 265 * question. If the name starts with '/', it will be treated 266 * as an absolute resource name, and used "as is"; 267 * otherwise it will be treated as relative to the 268 * package in which the actual interview class is defined. 269 * The default is the interview tag name if this is a root 270 * interview. If this is a child interview, there is no default 271 * resource bundle. 272 * @param name The name of the resource bundle used to look 273 * up internationalized strings. 274 * @throws MissingResourceException if the resource bundle 275 * cannot be found. 276 * @see #getResourceBundle 277 */ 278 protected void setResourceBundle(String name) 279 throws MissingResourceException 280 { 281 // name is not null 282 if (!name.equals(bundleName)) { 283 Class<?> c = getClass(); 284 final ClassLoader cl = c.getClassLoader(); 285 final String rn; 286 if (name.startsWith("/")) 287 rn = name.substring(1); 288 else { 289 String cn = c.getName(); 290 String pn = cn.substring(0, cn.lastIndexOf('.')); 291 rn = pn + "." + name; 292 } 293 //System.err.println("INT: looking for bundle: " + rn); 294 bundle = AccessController.doPrivileged( 295 new PrivilegedAction<ResourceBundle>() { 296 public ResourceBundle run() { 297 return ResourceBundle.getBundle(rn, Locale.getDefault(), cl); 298 } 299 }); 300 bundleName = name; 301 } 302 } 303 /** 304 * Set the base name of the resource bundle used to look up 305 * internationalized strings, such as the title and text of each 306 * question. If the name is treated as filename of file 307 * which is located in directory file. 308 * The default is the interview tag name if this is a root 309 * interview. If this is a child interview, there is no default 310 * resource bundle. 311 * @param name The name of the resource bundle used to look 312 * up internationalized strings. 313 * @param file The directory to find name. 314 * @throws MissingResourceException if the resource bundle 315 * cannot be found. 316 * @see #getResourceBundle 317 */ 318 protected void setResourceBundle(final String name, File file) 319 throws MissingResourceException { 320 if (bundleName != null && bundleName.equals(name)) { 321 return; 322 } 323 try { 324 URL[] url = {new URL("file:" + file.getAbsolutePath() + "/")}; 325 final URLClassLoader cl = new URLClassLoader(url); 326 bundle = AccessController.doPrivileged( 327 new PrivilegedAction<ResourceBundle>() { 328 public ResourceBundle run() { 329 return ResourceBundle.getBundle(name, Locale.getDefault(), cl); 330 } 331 }); 332 bundleName = name; 333 } catch (MalformedURLException e) { 334 } 335 336 } 337 338 /** 339 * Get the resource bundle for this interview, used to look up 340 * internationalized strings, such as the title and text of each question. 341 * If the bundle has not been set explicitly, it defaults to the 342 * parent's resource bundle; the root interview has a default resource 343 * bundle based on the interview tag name. 344 * @return the resource bundle for this interview. 345 * @see #setResourceBundle 346 */ 347 public ResourceBundle getResourceBundle() { 348 if (bundle == null && parent != null) 349 return parent.getResourceBundle(); 350 else 351 return bundle; 352 } 353 354 /** 355 * Set the name of the help set used to locate the "more info" 356 * for each question. The name should identify a resource containing 357 * a JavaHelp helpset file. If the name starts with '/', it will 358 * be treated as an absolute resource name, and used "as is"; 359 * otherwise it will be treated as relative to the 360 * package in which the actual interview class is defined. 361 * If help sets are specified for child interviews, they will 362 * automatically be added into the help set for the root interview. 363 * @param name The name of the help set containing the "more info" 364 * for each question. 365 * @throws Interview.HelpNotFoundFault if the help set could not be located 366 * @throws Interview.BadHelpFault if some problem occurred while opening the help set 367 * @see #getHelpSet 368 * @see #setHelpSet(Object) 369 */ 370 protected void setHelpSet(String name) throws Interview.Fault { 371 setHelpSet(helpSetFactory.createHelpSetObject(name, getClass())); 372 } 373 374 375 /** 376 * Set the help set used to locate the "more info" for each question. 377 * If help sets are specified for child interviews, they will 378 * automatically be added into the help set for the root interview. 379 * @param hs The help set containing the "more info" for each question 380 * in this interview. 381 * @see #getHelpSet 382 * @see #setHelpSet(String) 383 */ 384 protected void setHelpSet(Object hs) { 385 helpSet = helpSetFactory.updateHelpSetObject(this, hs); 386 } 387 388 /** 389 * Set the name of the help set used to locate the "more info" 390 * for each question. The name should identify a resource containing 391 * a JavaHelp helpset file. If the name is treated as filename of file 392 * which is located in directory file. 393 * If help sets are specified for child interviews, they will 394 * automatically be added into the help set for the root interview. 395 * @param name The name of the help set containing the "more info" 396 * for each question. 397 * @param file The directory to find help set. 398 * @throws Interview.HelpNotFoundFault if the help set could not be located 399 * @throws Interview.BadHelpFault if some problem occurred while opening the help set 400 * @see #getHelpSet 401 * @see #setHelpSet(Object) 402 * @see #setHelpSet(String) 403 */ 404 protected void setHelpSet(String name, File file) throws Interview.Fault { 405 setHelpSet(helpSetFactory.createHelpSetObject(name, file)); 406 } 407 408 409 /** 410 * Get the help set used to locate the "more info" for each question. If the 411 * help set has not been set explicitly, it defaults to the parent's help 412 * set. 413 * 414 * @return the help set used to locate "more info" for questions in this 415 * interview. 416 * @see #setHelpSet 417 */ 418 public Object getHelpSet() { 419 if (helpSet == null && parent != null) 420 return parent.getHelpSet(); 421 422 return helpSet; 423 } 424 425 /** 426 * Initializes the help factory - generally only called once per instance of the 427 * system. 428 * @return Create the help factory for the interview system. 429 */ 430 private static HelpSetFactory createHelpFactory() { 431 try { 432 Class<? extends HelpSetFactory> factoryClass = 433 Class.forName("com.sun.interview.JavaHelpFactory").asSubclass(HelpSetFactory.class); 434 return factoryClass.getDeclaredConstructor().newInstance(); 435 } catch (ClassNotFoundException e) { 436 return HelpSetFactory.DEFAULT; 437 } catch (Exception e) { 438 e.printStackTrace(System.err); 439 return HelpSetFactory.DEFAULT; 440 } 441 442 } 443 444 /** 445 * Mark this interview as having been edited or not. 446 * @param edited whether or not this interview is marked as edited 447 */ 448 public void setEdited(boolean edited) { 449 Interview i = this; 450 while (i.parent != null) 451 i = i.parent; 452 i.edited = edited; 453 } 454 455 456 /** 457 * Determine if this interview as having been edited or not. 458 * @return true if this interview is marked as having been edited 459 */ 460 public boolean isEdited() { 461 Interview i = this; 462 while (i.parent != null) 463 i = i.parent; 464 return i.edited; 465 } 466 467 /** 468 * Get the first question of the interview. 469 * @return the first question of the interview 470 * @see #setFirstQuestion 471 */ 472 public Question getFirstQuestion() { 473 return firstQuestion; 474 } 475 476 /** 477 * Set the first question for an interview. This may be called more 478 * than once, but only while the interview is being constructed. 479 * Once any method has been called that refers to the interview 480 * path, the initial question may not be changed. 481 * @param q The initial question 482 * @throws IllegalStateException if it is too late to change the 483 * initial question. 484 * @see #getFirstQuestion 485 */ 486 protected void setFirstQuestion(Question q) { 487 if (path != null) 488 throw new IllegalStateException(); 489 490 firstQuestion = q; 491 492 // OLD: the problem with this is that reset() calls updatePath() 493 // which might call methods which refer to uninitialize data, 494 // so can't safely call reset() here 495 // 496 // if (parent == null) 497 // reset(); 498 499 // if we wanted to permit the first question to be changed, 500 // consider the following: 501 // if (parent == null) 502 // path = null; 503 } 504 505 //--------------------------------------------------------- 506 507 /** 508 * Get a sub-interview with a given tag name. All descendents are 509 * searched (i.e. all children, all their children, etc.) 510 * @param tag The tag of the interview to be found. 511 * @return the sub-interview with the specified name. 512 * @throws Interview.Fault if no interview is found with the given name. 513 */ 514 public Interview getInterview(String tag) throws Fault { 515 if (tag == null) 516 throw new NullPointerException(); 517 518 Interview i = getInterview0(tag); 519 if (i != null) 520 return i; 521 else 522 throw new Fault(i18n, "interview.cantFindInterview", tag); 523 } 524 525 private Interview getInterview0(String t) { 526 if (t.equals(tag)) 527 return this; 528 529 for (int i = 0; i < children.size(); i++) { 530 Interview c = children.elementAt(i); 531 Interview iv = c.getInterview0(t); 532 if (iv != null) 533 return iv; 534 } 535 536 return null; 537 } 538 539 Set<Interview> getInterviews() { 540 Set<Interview> s = new HashSet<>(); 541 getInterviews0(s); 542 return s; 543 } 544 545 private void getInterviews0(Set<Interview> s) { 546 s.add(this); 547 for (int i = 0; i < children.size(); i++) { 548 Interview child = children.elementAt(i); 549 child.getInterviews0(s); 550 } 551 } 552 553 //----- navigation ---------------------------------------- 554 555 /** 556 * Determine if a question is the first question of the interview. 557 * @param q the question to check 558 * @return true if this is the first question. 559 */ 560 public boolean isFirst(Question q) { 561 return (q == firstQuestion); 562 } 563 564 /** 565 * Determine if a question is the last question of the interview. 566 * @param q the question to check 567 * @return true if this is the last question. 568 */ 569 public boolean isLast(Question q) { 570 return (q instanceof FinalQuestion && q.interview.caller == null); 571 } 572 573 /** 574 * Determine if a question has a non-null successor. 575 * @param q the question to check 576 * @return true if this question has a non-null successor. 577 */ 578 public boolean hasNext(Question q) { 579 return (q.getNext() != null); 580 } 581 582 583 /** 584 * Determine if a question has a successor which is neither null 585 * nor an ErrorQuestion. 586 * @param q the question to check 587 * @return true if this question has a successor which is neither null 588 * nor an ErrorQuestion 589 */ 590 public boolean hasValidNext(Question q) { 591 Question qn = q.getNext(); 592 return (qn != null && !(qn instanceof ErrorQuestion)); 593 } 594 595 /** 596 * Start (or restart) the interview. The current question is reset to the first 597 * question, and the current path is evaluated from there. 598 */ 599 public void reset() { 600 ensurePathInitialized(); 601 602 // first, reset back to the beginning 603 updateEnabled = true; 604 caller = null; 605 currIndex = 0; 606 path.clear(); 607 path.addQuestion(firstQuestion); 608 609 if (root == this) { 610 rawPath.clear(); 611 rawPath.addQuestion(firstQuestion); 612 } 613 614 hiddenPath.clear(); 615 updatePath(firstQuestion); 616 // notify observers 617 notifyCurrentQuestionChanged(firstQuestion); 618 } 619 620 /** 621 * Start (or restart) the interview. The current question is reset to the first 622 * question, and the current path is evaluated from there. 623 */ 624 private void reset(Question q) { 625 ensurePathInitialized(); 626 627 // first, reset back to the beginning 628 updateEnabled = true; 629 caller = null; 630 currIndex = 0; 631 path.clear(); 632 hiddenPath.clear(); 633 if (root == this) { 634 rawPath.clear(); 635 rawPath.addQuestion(firstQuestion); 636 } 637 638 path.addQuestion(firstQuestion); 639 updatePath(firstQuestion); 640 641 // now update to the selected question 642 if (q == firstQuestion || q == null) 643 // already there; just need to notify observers 644 notifyCurrentQuestionChanged(firstQuestion); 645 else { 646 // try and select the specified question 647 try { 648 setCurrentQuestion(q); 649 } 650 catch (Fault e) { 651 notifyCurrentQuestionChanged(firstQuestion); 652 } 653 } 654 } 655 656 /** 657 * Advance to the next question in the interview. 658 * Questions that have been {@link Question#isEnabled disabled} will 659 * be skipped over. 660 * @throws Interview.Fault if there are no more questions 661 */ 662 public void next() throws Fault { 663 ensurePathInitialized(); 664 665 Interview i = this; 666 667 // first, step in until we get to the current question 668 while (i.path.questionAt(i.currIndex) instanceof InterviewQuestion) { 669 InterviewQuestion iq = (InterviewQuestion) (i.path.questionAt(i.currIndex)); 670 i = iq.getTargetInterview(); 671 } 672 673 // next, step forward to the next question 674 i.currIndex++; 675 676 // finally, normalize the result 677 while (true) { 678 if (i.currIndex == i.path.size()) { 679 i.currIndex--; 680 throw new Fault(i18n, "interview.noMoreQuestions"); 681 } 682 683 Question q = i.path.questionAt(i.currIndex); 684 if (q instanceof InterviewQuestion) { 685 InterviewQuestion iq = (InterviewQuestion) q; 686 i = iq.getTargetInterview(); 687 i.currIndex = 0; 688 } 689 else if (q instanceof FinalQuestion && i.caller != null) { 690 i = i.caller.getInterview(); 691 i.currIndex++; 692 } 693 else 694 break; 695 } 696 697 Question q = i.path.questionAt(i.currIndex); 698 notifyCurrentQuestionChanged(q); 699 } 700 701 /** 702 * Back up to the previous question in the interview. 703 * Questions that have been {@link Question#isEnabled disabled} will 704 * be skipped over. 705 * @throws Interview.Fault if there is no previous question. 706 */ 707 public void prev() throws Fault { 708 ensurePathInitialized(); 709 710 Interview i = this; 711 712 // first, step in until we get to the current question 713 while (i.path.questionAt(i.currIndex) instanceof InterviewQuestion) { 714 InterviewQuestion iq = (InterviewQuestion) (i.path.questionAt(i.currIndex)); 715 i = iq.getTargetInterview(); 716 } 717 718 // next, step back to the next question 719 i.currIndex--; 720 721 // finally, normalize the result 722 while (true) { 723 if (i.currIndex < 0) { 724 if (i.caller == null) { 725 i.currIndex = 0; 726 throw new Fault(i18n, "interview.noMoreQuestions"); 727 } 728 else { 729 i = i.caller.getInterview(); 730 i.currIndex--; 731 } 732 } 733 else if (i.path.questionAt(i.currIndex) instanceof InterviewQuestion) { 734 InterviewQuestion iq = (InterviewQuestion) (i.path.questionAt(i.currIndex)); 735 i = iq.getTargetInterview(); 736 i.currIndex = i.path.size() - 1; 737 } 738 else if (i.path.questionAt(i.currIndex) instanceof FinalQuestion) { 739 i.currIndex--; 740 } 741 else 742 break; 743 } 744 745 Question q = i.path.questionAt(i.currIndex); 746 notifyCurrentQuestionChanged(q); 747 } 748 749 /** 750 * Advance to the last question in the interview. 751 * Questions that have been {@link Question#isEnabled disabled} will 752 * be skipped over. 753 * @throws Interview.Fault if there are no more questions 754 */ 755 public void last() throws Fault { 756 ensurePathInitialized(); 757 758 Interview i = this; 759 760 // first, step in until we get to the current question 761 while (i.path.questionAt(i.currIndex) instanceof InterviewQuestion) { 762 InterviewQuestion iq = (InterviewQuestion) (i.path.questionAt(i.currIndex)); 763 i = iq.getTargetInterview(); 764 } 765 766 // navigate around the interview without upsetting any interview's currIndex 767 int index = i.currIndex; 768 769 // Scan forward looking for candidates for the last question. 770 // The alternative is to advance i.currIndex to the end of this 771 // interview and normalize the result, but that gets complicated 772 // with the interaction between nested interviews and hidden 773 // questions. 774 Question cq = i.path.questionAt(index); 775 Question lq = cq; 776 index++; 777 778 while (index < i.path.size()) { 779 Question q = i.path.questionAt(index); 780 781 if (q instanceof InterviewQuestion) { 782 i = ((InterviewQuestion) q).getTargetInterview(); 783 index = 0; 784 } 785 else if (q instanceof FinalQuestion && i.caller != null) { 786 Interview callInterview = i.caller.getInterview(); 787 int callIndex = callInterview.path.indexOf(i); 788 if (callIndex == -1) 789 throw new IllegalStateException(); 790 i = callInterview; 791 index = callIndex + 1; 792 } 793 else { 794 // update candidate and move on 795 lq = q; 796 index++; 797 } 798 } 799 800 if (lq == cq) { 801 if ( !(lq instanceof FinalQuestion)) 802 throw new Fault(i18n, "interview.noMoreQuestions"); 803 } 804 else 805 setCurrentQuestion(lq); 806 } 807 808 /** 809 * Check if the interview has been started. An interview is 810 * considered to be at the beginning if there is only one 811 * question on the current path of a type that requires a response. 812 * This indirectly implies it must be the last question on 813 * the current path, and must only be preceded by 814 * {@link NullQuestion information-only} questions. 815 * @return true if the first answerable question is unanswered. 816 */ 817 public boolean isStarted() { 818 Question[] path = root.getPath(); 819 for (int i = 0; i < path.length - 1; i++) { 820 Question q = path[i]; 821 if (!(q instanceof NullQuestion)) 822 return true; 823 } 824 return false; 825 } 826 827 /** 828 * Check if the interview has been completed. An interview is 829 * considered to have been completed if the final question 830 * on the current path is of type {@link FinalQuestion}. 831 * @return true if the interview has been completed. 832 */ 833 public boolean isFinishable() { 834 ensurePathInitialized(); 835 836 Interview i = root; 837 return (i.path.lastQuestion() instanceof FinalQuestion); 838 } 839 840 /** 841 * Check if this subinterview has been completed. A subinterview is 842 * considered to have been completed if none of the questions from 843 * this subinterview on the current path return null as the result 844 * of getNext(). 845 *<em>Note:</em>compare this to isFinishable() which checks that the 846 * entire interview (of which this subinterview may be a part) is 847 * complete. 848 * @return true is this subinterview has been completed. 849 */ 850 protected boolean isInterviewFinishable() { 851 return (path != null && path.lastQuestion() instanceof FinalQuestion); 852 } 853 854 855 /** 856 * Jump to a specific question in the interview. The question 857 * must be on the current path, but can be either before or 858 * after the current position at the time this is called. 859 * @param q The question which is to become the current 860 * question in the interview. 861 * @throws Interview.Fault if the question given is not on the current path. 862 * @see #getCurrentQuestion 863 */ 864 public void setCurrentQuestion(Question q) throws Fault { 865 if (q == null) 866 throw new NullPointerException(); 867 868 if (q == getCurrentQuestion()) 869 return; 870 871 boolean ok = root.setCurrentQuestion0(q); 872 if (!ok) 873 throw new NotOnPathFault(q); 874 875 notifyCurrentQuestionChanged(q); 876 } 877 878 private boolean setCurrentQuestion0(Question q) { 879 ensurePathInitialized(); 880 881 for (int i = 0; i < path.size(); i++) { 882 Question qq = path.questionAt(i); 883 if (qq.equals(q)) { 884 currIndex = i; 885 return true; 886 } 887 else if (qq instanceof InterviewQuestion) { 888 if (((InterviewQuestion) qq).getTargetInterview().setCurrentQuestion0(q)) { 889 currIndex = i; 890 return true; 891 } 892 } 893 } 894 return false; 895 } 896 897 /** 898 * Get the current question in the interview. 899 * @return The current question. 900 * @see #setCurrentQuestion 901 */ 902 public Question getCurrentQuestion() { 903 ensurePathInitialized(); 904 905 Interview i = root; 906 Question q = i.path.questionAt(i.currIndex); 907 while (q instanceof InterviewQuestion) { 908 i = ((InterviewQuestion) q).getTargetInterview(); 909 q = i.path.questionAt(i.currIndex); 910 } 911 return q; 912 } 913 914 private void setCurrentQuestionFromPath(Question[] path) { 915 root.setCurrentQuestionFromPath0(path); 916 } 917 918 private void setCurrentQuestionFromPath0(Question[] path) { 919 for (int i = path.length - 1; i >= 0; i--) { 920 if (setCurrentQuestion0(path[i])) { 921 notifyCurrentQuestionChanged(path[i]); 922 return; 923 } 924 } 925 } 926 927 //----- path stuff ---------------------------------------- 928 929 /** 930 * Get the set of questions on the current path. 931 * The first question is determined by the interview; after that, 932 * each question in turn determines its successor. The path ends 933 * when a question indicates no successor (or erroneously returns 934 * a question that is already on the path, that would otherwise 935 * form a cycle). The special type of question, {@link FinalQuestion}, 936 * never returns a successor. 937 * Within a particular interview, a question may refer to a 938 * nested interview, before continuing within the original interview. 939 * Any such references to nested interviews are automatically 940 * expanded by this method, leaving just the complete set of basic 941 * questions on the path. 942 * @return an array containing the list of questions on the current path. 943 * @see #setFirstQuestion 944 * @see Question#getNext 945 * @see #getPathToCurrent 946 */ 947 public Question[] getPath() { 948 Vector<Question> v = new Vector<>(); 949 iteratePath0(v, true, true, true); 950 Question[] p = new Question[v.size()]; 951 v.copyInto(p); 952 return p; 953 } 954 955 /** 956 * Get the set of questions on the current path up to and 957 * including the current question. 958 * @return an array containing the list of questions on the 959 * current path up to and including the current question 960 * @see #getPath 961 */ 962 public Question[] getPathToCurrent() { 963 Vector<Question> v = new Vector<>(); 964 iteratePath0(v, true, false, true); 965 Question[] p = new Question[v.size()]; 966 v.copyInto(p); 967 return p; 968 } 969 970 /** 971 * Get the current set path of questions, including some things normally 972 * hidden. Hidden, disabled and final questions are included upon demand. 973 * The list of questions is flattend to only include questions, no 974 * representation of the interview structure is given. 975 * @param includeFinals Should FinalQuestions be included. 976 * @return The current active path of questions, based on the requested 977 * options. Returns null if no path information is available. 978 */ 979 public Question[] getRawPath(boolean includeFinals) { 980 if (rawPath == null) 981 return null; 982 else 983 return rawPath.getQuestions(); 984 } 985 986 /** 987 * Get an iterator for the set of questions on the current path. 988 * The first question is determined by the interview; after that, 989 * each question in turn determines its successor. The path ends 990 * when a question indicates no successor (or erroneously returns 991 * a question that is already on the path, that would otherwise 992 * form a cycle). The special type of question, {@link FinalQuestion}, 993 * never returns a successor. 994 * Within a particular interview, a question may refer to a 995 * nested interview, before continuing within the original interview. 996 * Such nested interviews may optionally be expanded by this method, 997 * depending on the arguments. 998 * @param flattenNestedInterviews If true, any nested interviews will 999 * be expanded in place and returned via the iterator; otherwise, the 1000 * the nested interview will be returned instead. 1001 * @return an Iterator for the questions on the current path 1002 * @see #iteratePathToCurrent 1003 */ 1004 public Iterator<Question> iteratePath(boolean flattenNestedInterviews) { 1005 Vector<Question> v = new Vector<>(); 1006 iteratePath0(v, flattenNestedInterviews, true, true); 1007 return v.iterator(); 1008 } 1009 1010 1011 /** 1012 * Get an iterator for the set of questions on the current path 1013 * up to and including the current question. 1014 * @param flattenNestedInterviews If true, any nested interviews will 1015 * be expanded in place and returned via the iterator; otherwise, the 1016 * the nested interview will be returned instead. 1017 * @return an Iterator for the questions on the current path 1018 * up to and including the current question 1019 * @see #iteratePath 1020 */ 1021 public Iterator<Question> iteratePathToCurrent(boolean flattenNestedInterviews) { 1022 Vector<Question> v = new Vector<>(); 1023 iteratePath0(v, flattenNestedInterviews, false, true); 1024 return v.iterator(); 1025 } 1026 1027 1028 private void iteratePath0(List<Question> l, boolean flattenNestedInterviews, boolean all, boolean addFinal) { 1029 ensurePathInitialized(); 1030 1031 int n = (all ? path.size() : currIndex + 1); 1032 for (int i = 0; i < n; i++) { 1033 Question q = path.questionAt(i); 1034 if (q instanceof InterviewQuestion) { 1035 if (flattenNestedInterviews) 1036 ((InterviewQuestion) q).getTargetInterview().iteratePath0(l, true, all, false); 1037 else 1038 l.add(q); 1039 } 1040 else if (!addFinal && q instanceof FinalQuestion) 1041 return; 1042 else 1043 l.add(q); 1044 } 1045 } 1046 1047 1048 /** 1049 * Verify that the current path contains a specified question, 1050 * and throw an exception if it does not. 1051 * @param q the question to be checked 1052 * @throws Interview.NotOnPathFault if the current path does not contain 1053 * the specified question. 1054 */ 1055 public void verifyPathContains(Question q) 1056 throws NotOnPathFault 1057 { 1058 if (!pathContains(q)) 1059 throw new NotOnPathFault(q); 1060 } 1061 1062 /** 1063 * Check if the path contains a specific question. 1064 * @param q The question for which to check. 1065 * @return true if the question is found on the current path. 1066 */ 1067 public boolean pathContains(Question q) { 1068 return root.pathContains0(q); 1069 } 1070 1071 /** 1072 * Check if the path contains questions from a specific interview. 1073 * @param i The interview for which to check. 1074 * @return true if the interview is found on the current path. 1075 */ 1076 public boolean pathContains(Interview i) { 1077 return root.pathContains0(i); 1078 } 1079 1080 private boolean pathContains0(Object o) { 1081 ensurePathInitialized(); 1082 1083 for (int index = 0; index < path.size(); index++) { 1084 Question q = path.questionAt(index); 1085 if (o == q) 1086 return true; 1087 1088 if (q instanceof InterviewQuestion) { 1089 InterviewQuestion iq = (InterviewQuestion) q; 1090 Interview i = iq.getTargetInterview(); 1091 if (o == i) 1092 return true; 1093 1094 if (i.pathContains0(o)) 1095 return true; 1096 } 1097 } 1098 1099 return false; 1100 } 1101 1102 /** 1103 * Get the complete set of questions in this interview and 1104 * recursively, in all child interviews. 1105 * @return a set of all questions in this and every child interview. 1106 */ 1107 public Set<Question> getQuestions() { 1108 Set<Question> s = new HashSet<>(); 1109 getQuestions0(s); 1110 return s; 1111 } 1112 1113 private void getQuestions0(Set<Question> s) { 1114 s.addAll(allQuestions.values()); 1115 1116 for (int i = 0; i < children.size(); i++) { 1117 Interview child = children.elementAt(i); 1118 child.getQuestions0(s); 1119 } 1120 } 1121 1122 /** 1123 * Get all questions in this interview and 1124 * recursively, in all child interviews. 1125 * @return a map containing all questions in this and every child interview. 1126 */ 1127 public Map<String, Question> getAllQuestions() { 1128 Map<String, Question> m = new LinkedHashMap<>(); 1129 getAllQuestions0(m); 1130 return m; 1131 } 1132 1133 private void getAllQuestions0(Map<String, Question> m) { 1134 m.putAll(allQuestions); 1135 1136 for (int i = 0; i < children.size(); i++) { 1137 Interview child = children.elementAt(i); 1138 child.getAllQuestions0(m); 1139 } 1140 } 1141 1142 /** 1143 * Check whether any questions on the current path have any 1144 * associated checklist items. 1145 * @return true if no questions have any corresponding checklist 1146 * items, and false otherwise. 1147 */ 1148 public boolean isChecklistEmpty() { 1149 for (Iterator<Question> iter = iteratePath(true); iter.hasNext(); ) { 1150 Question q = iter.next(); 1151 Checklist.Item[] items = q.getChecklistItems(); 1152 if (items != null && items.length > 0) 1153 return false; 1154 } 1155 return true; 1156 } 1157 1158 /** 1159 * Create a checklist composed of all checklist items 1160 * for questions on the current path. 1161 * @return a checklist composed of all checklist items 1162 * for questions on the current path. 1163 * @see #getPath 1164 * @see Question#getChecklistItems 1165 */ 1166 public Checklist createChecklist() { 1167 Checklist c = new Checklist(); 1168 for (Iterator<Question> iter = iteratePath(true); iter.hasNext(); ) { 1169 Question q = iter.next(); 1170 Checklist.Item[] items = q.getChecklistItems(); 1171 if (items != null) { 1172 for (Checklist.Item item : items) c.add(item); 1173 } 1174 } 1175 return c; 1176 } 1177 1178 /** 1179 * Create a checklist item based on entries in the interview's resource bundle. 1180 * @param sectionKey A key to identify the section name within the interview's resource bundle 1181 * @param textKey A key to identify the checklist item text within the interview's resource bundle 1182 * @return a Checklist.Item object composed from the appropriate entries in the interview's resource bundle 1183 */ 1184 public Checklist.Item createChecklistItem(String sectionKey, String textKey) { 1185 String section = getI18NString(sectionKey); 1186 String text = getI18NString(textKey); 1187 return new Checklist.Item(section, text); 1188 } 1189 1190 1191 /** 1192 * Create a checklist item based on entries in the interview's resource bundle. 1193 * @param sectionKey A key to identify the section name within the interview's resource bundle 1194 * @param textKey A key to identify the checklist item text within the interview's resource bundle 1195 * @param textArg a single argument to be formatted into the checklist item text 1196 * @return a Checklist.Item object composed from the appropriate entries in the interview's resource bundle and the specified argument value 1197 */ 1198 public Checklist.Item createChecklistItem(String sectionKey, String textKey, Object textArg) { 1199 String section = getI18NString(sectionKey); 1200 String text = getI18NString(textKey, textArg); 1201 return new Checklist.Item(section, text); 1202 } 1203 1204 1205 /** 1206 * Create a checklist item based on entries in the interview's resource bundle. 1207 * @param sectionKey A key to identify the section name within the interview's resource bundle 1208 * @param textKey A key to identify the checklist item text within the interview's resource bundle 1209 * @param textArgs an array of arguments to be formatted into the checklist item text 1210 * @return a Checklist.Item object composed from the appropriate entries in the interview's resource bundle and the specified argument values 1211 */ 1212 public Checklist.Item createChecklistItem(String sectionKey, String textKey, Object[] textArgs) { 1213 String section = getI18NString(sectionKey); 1214 String text = getI18NString(textKey, textArgs); 1215 return new Checklist.Item(section, text); 1216 } 1217 1218 //----- markers --------------------------------- 1219 1220 /** 1221 * Add a named marker for a question. 1222 * @param q The question for which to add the marker 1223 * @param name The name of the marker to be added. 1224 * @throws NullPointerException if the question is null. 1225 */ 1226 void addMarker(Question q, String name) { 1227 if (root != this) { 1228 root.addMarker(q, name); 1229 return; 1230 } 1231 1232 if (q == null) 1233 throw new NullPointerException(); 1234 1235 if (allMarkers == null) 1236 allMarkers = new HashMap<>(); 1237 1238 Set<Question> markersForName = allMarkers.get(name); 1239 if (markersForName == null) { 1240 markersForName = new HashSet<>(); 1241 allMarkers.put(name, markersForName); 1242 } 1243 1244 markersForName.add(q); 1245 } 1246 1247 /** 1248 * Remove a named marker for a question. 1249 * @param q The question for which to remove the marker 1250 * @param name The name of the marker to be removeded. 1251 * @throws NullPointerException if the question is null. 1252 */ 1253 void removeMarker(Question q, String name) { 1254 if (root != this) { 1255 root.removeMarker(q, name); 1256 return; 1257 } 1258 1259 if (q == null) 1260 throw new NullPointerException(); 1261 1262 if (allMarkers == null) 1263 return; 1264 1265 Set<Question> markersForName = allMarkers.get(name); 1266 if (markersForName == null) 1267 return; 1268 1269 markersForName.remove(q); 1270 1271 if (markersForName.size() == 0) 1272 allMarkers.remove(name); 1273 } 1274 1275 /** 1276 * Check if a question has a named marker. 1277 * @param q The question for which to check for the marker 1278 * @param name The name of the marker to be removed. 1279 * @throws NullPointerException if the question is null. 1280 */ 1281 boolean hasMarker(Question q, String name) { 1282 if (root != this) 1283 return root.hasMarker(q, name); 1284 1285 if (q == null) 1286 throw new NullPointerException(); 1287 1288 if (allMarkers == null) 1289 return false; 1290 1291 Set<Question> markersForName = allMarkers.get(name); 1292 if (markersForName == null) 1293 return false; 1294 1295 return markersForName.contains(q); 1296 } 1297 1298 /** 1299 * Remove all the markers with a specified name. 1300 * @param name The name of the markers to be removed 1301 */ 1302 public void removeMarkers(String name) { 1303 if (root != this) { 1304 root.removeMarkers(name); 1305 return; 1306 } 1307 1308 // just have to remove the appropriate set of markers 1309 if (allMarkers != null) 1310 allMarkers.remove(name); 1311 } 1312 1313 /** 1314 * Remove all the markers, whatever their name. 1315 */ 1316 public void removeAllMarkers() { 1317 if (root != this) { 1318 root.removeAllMarkers(); 1319 return; 1320 } 1321 1322 allMarkers = null; 1323 } 1324 1325 /** 1326 * Clear the response to marked questions. 1327 * @param name The name of the markers for the questions to be cleared. 1328 */ 1329 public void clearMarkedResponses(String name) { 1330 if (root != this) { 1331 root.clearMarkedResponses(name); 1332 return; 1333 } 1334 1335 if (allMarkers == null) // no markers at all 1336 return; 1337 1338 Set<Question> markersForName = allMarkers.get(name); 1339 if (markersForName == null) // no markers for this name 1340 return; 1341 1342 updateEnabled = false; 1343 Question oldCurrentQuestion = getCurrentQuestion(); 1344 1345 for (Question q : markersForName) { 1346 q.clear(); 1347 } 1348 1349 updateEnabled = true; 1350 updatePath(firstQuestion); 1351 1352 Question newCurrentQuestion = getCurrentQuestion(); 1353 if (newCurrentQuestion != oldCurrentQuestion) 1354 notifyCurrentQuestionChanged(newCurrentQuestion); 1355 } 1356 1357 private void loadMarkers(Map<String, String> data) { 1358 String s = data.get(MARKERS); 1359 int count = 0; 1360 if (s != null) { 1361 try { 1362 count = Integer.parseInt(s); 1363 } 1364 catch (NumberFormatException e) { 1365 // ignore 1366 } 1367 } 1368 1369 allMarkers = null; 1370 1371 for (int i = 0; i < count; i++) { 1372 String name = data.get(MARKERS_PREF + i + ".name"); 1373 String tags = data.get(MARKERS_PREF + i); 1374 if (tags != null) 1375 loadMarkers(name, tags); 1376 } 1377 } 1378 1379 private void loadMarkers(String name, String tags) { 1380 int start = -1; 1381 for (int i = 0; i < tags.length(); i++) { 1382 if (tags.charAt(i) == '\n') { 1383 if (start != -1) { 1384 String tag = tags.substring(start, i).trim(); 1385 loadMarker(name, tag); 1386 start = -1; 1387 } 1388 } 1389 else 1390 if (start == -1) 1391 start = i; 1392 } 1393 if (start != -1) { 1394 String tag = tags.substring(start).trim(); 1395 loadMarker(name, tag); 1396 } 1397 } 1398 1399 private void loadMarker(String name, String tag) { 1400 if (tag.length() > 0) { 1401 Question q = lookup(tag); 1402 if (q != null) 1403 addMarker(q, name); 1404 } 1405 } 1406 1407 private void saveMarkers(Map<String, String> data) { 1408 if (allMarkers == null) 1409 return; 1410 1411 int i = 0; 1412 for (Map.Entry<String, Set<Question>> e : allMarkers.entrySet()) { 1413 String name = e.getKey(); 1414 Set<Question> markersForName = e.getValue(); 1415 if (name != null) 1416 data.put(MARKERS_PREF + i + ".name", name); 1417 StringBuffer sb = new StringBuffer(); 1418 for (Question q : markersForName) { 1419 if (sb.length() > 0) 1420 sb.append('\n'); 1421 sb.append(q.getTag()); 1422 } 1423 data.put(MARKERS_PREF + i, sb.toString()); 1424 i++; 1425 } 1426 1427 if (i > 0) 1428 data.put(MARKERS, String.valueOf(i)); 1429 } 1430 1431 1432 //----- nested interview stuff --------------------------------- 1433 1434 /** 1435 * Return a special type of question used to indicate that 1436 * a sub-interview interview should be called before proceeding 1437 * to the next question in this interview. 1438 * @param i The nested interview to be called next 1439 * @param q The next question to be asked when the nested 1440 * interview has completes with a {@link FinalQuestion final question}. 1441 * @return a pseudo-question that will call a nested interview before 1442 * continuing with the specified follow-on question. 1443 */ 1444 protected Question callInterview(Interview i, Question q) { 1445 return new InterviewQuestion(this, i, q); 1446 } 1447 1448 //----- load/save stuff ---------------------------------------- 1449 1450 /** 1451 * Clear any responses to all the questions in this interview, and then 1452 * recursively, in its child interviews. 1453 */ 1454 public void clear() { 1455 updateEnabled = false; 1456 for (Question q : allQuestions.values()) { 1457 q.clear(); 1458 } 1459 1460 for (int i = 0; i < children.size(); i++) { 1461 Interview child = children.elementAt(i); 1462 child.clear(); 1463 } 1464 if (parent == null) { 1465 extraValues = null; 1466 templateValues = null; 1467 reset(); 1468 } 1469 } 1470 1471 /** 1472 * Load the state for questions from an archive map. The map 1473 * will be passed to each question in this interview and in any 1474 * child interviews, and each question should {@link Question#load load} 1475 * its state, according to its tag. 1476 * The data must normally contain a valid checksum, generated during {@link #save}. 1477 * @param data The archive map from which the state should be loaded. 1478 * @throws Interview.Fault if the checksum is found to be incorrect. 1479 */ 1480 public void load(Map<String, String> data) throws Fault { 1481 load(data, true); 1482 } 1483 1484 /** 1485 * Load the state for questions from an archive map. The map 1486 * will be passed to each question in this interview and in any 1487 * child interviews, and each question should {@link Question#load load} 1488 * its state, according to its tag. 1489 * The data must normally contain a valid checksum, generated during {@link #save}. 1490 * @param data The archive map from which the state should be loaded. 1491 * @param checkChecksum If true, the checksum in the data will be checked. 1492 * @throws Interview.Fault if the checksum is found to be incorrect. 1493 */ 1494 public void load(Map<String, String> data, boolean checkChecksum) throws Fault { 1495 if (checkChecksum && !isChecksumValid(data, true)) 1496 throw new Fault(i18n, "interview.checksumError"); 1497 1498 if (parent == null) { 1499 String iTag = data.get(INTERVIEW); 1500 if (iTag != null && !iTag.equals(getClass().getName())) 1501 throw new Fault(i18n, "interview.classMismatch"); 1502 1503 loadExternalValues(data); 1504 loadTemplateValues(data); 1505 } 1506 1507 updateEnabled = false; 1508 1509 // clear all the answers in this interview before loading an 1510 // responses from the archive 1511 for (Question q : allQuestions.values()) { 1512 q.clear(); 1513 } 1514 1515 for (Question q : allQuestions.values()) { 1516 q.load(data); 1517 } 1518 1519 for (int i = 0; i < children.size(); i++) { 1520 Interview child = children.elementAt(i); 1521 child.load(data, false); 1522 } 1523 1524 if (parent == null) { 1525 String qTag = data.get(QUESTION); 1526 Question q = (qTag == null ? null : lookup(qTag)); 1527 reset(q == null ? firstQuestion : q); 1528 } 1529 1530 loadMarkers(data); 1531 } 1532 1533 /** 1534 * Check if the checksum is valid for a set of responses. 1535 * When responses are saved to a map, they are checksummed, 1536 * so that they can be checked for validity when reloaded. 1537 * This method verifies that a set of responses are acceptable 1538 * for loading. 1539 * @param data The set of responses to be checked. 1540 * @param okIfOmitted A boolean determining the response if 1541 * there is no checksum available in the data 1542 * @return Always true. 1543 * @deprecated As of version 4.4.1, checksums are no longer 1544 * calculated or checked. True is always returned. 1545 */ 1546 public static boolean isChecksumValid(Map<String, String> data, boolean okIfOmitted) { 1547 return true; 1548 } 1549 1550 /** 1551 * Save the state for questions in an archive map. The map 1552 * will be passed to each question in this interview and in any 1553 * child interviews, and each question should {@link Question#save save} 1554 * its state, according to its tag. 1555 * @param data The archive in which the values should be saved. 1556 */ 1557 public void save(Map<String, String> data) { 1558 // only in the root interview 1559 if (parent == null) { 1560 data.put(INTERVIEW, getClass().getName()); 1561 data.put(QUESTION, getCurrentQuestion().getTag()); 1562 writeLocale(data); 1563 1564 if (extraValues != null && extraValues.size() > 0) { 1565 Set<String> keys = getPropertyKeys(); 1566 for (String key : keys) { 1567 data.put(EXTERNAL_PREF + key, extraValues.get(key)); 1568 } // while 1569 } 1570 1571 if (templateValues != null && templateValues.size() > 0) { 1572 Set<String> keys = templateValues.keySet(); 1573 for (String key : keys) { 1574 data.put(TEMPLATE_PREF + key, retrieveTemplateProperty(key)); 1575 } // while 1576 } 1577 1578 } 1579 for (Question q : allQuestions.values()) { 1580 try { 1581 q.save(data); 1582 } 1583 catch (RuntimeException ex) { 1584 System.err.println("warning: " + ex.toString()); 1585 System.err.println("while saving value for question " + q.getTag() + " in interview " + getTag()); 1586 } 1587 } 1588 1589 for (Interview child : children) { 1590 child.save(data); 1591 } 1592 1593 saveMarkers(data); 1594 1595 //data.put(CHECKSUM, Long.toString(computeChecksum(data), 16)); 1596 } 1597 1598 /** 1599 * Writes information about current locale to the given map. 1600 * <br> 1601 * This information is used later to properly restore locale-sensitive values, 1602 * like numerics. 1603 * @param data target map to write data to 1604 * @see #LOCALE 1605 * @see #readLocale(Map) 1606 */ 1607 protected static void writeLocale(Map<String, String> data) { 1608 data.put(LOCALE, Locale.getDefault().toString()); 1609 } 1610 1611 /** 1612 * Reads information about locale from the given map. <br> 1613 * Implementation looks for the string keyed by {@link #LOCALE} and then 1614 * tries to decode it to valid locale object. 1615 * @param data map with interview values 1616 * @return locale, decoded from value taken from map; or default (current) locale 1617 * @see #LOCALE 1618 * @see #writeLocale(Map) 1619 */ 1620 protected static Locale readLocale(Map<?, ?> data) { 1621 Locale result = null; 1622 Object o = data.get(LOCALE); 1623 if (o != null) { 1624 if (o instanceof Locale) { 1625 result = (Locale) o; 1626 } else if (o instanceof String) { 1627 /* try to decode Locale object from its string representation 1628 * @see java.util.Locale#toString() 1629 * Examples: "", "en", "de_DE", "_GB", "en_US_WIN", "de__POSIX", "fr__MAC" 1630 */ 1631 String s = ((String) o).trim(); 1632 String language = "", country = "", variant = ""; 1633 if (s.length() != 0) { 1634 try { 1635 // decode language 1636 int i = s.indexOf('_'); 1637 if (i == -1) { 1638 // there's no separator in the string. This can be 1639 // only the language 1640 language = s; 1641 } else if (i == 0) { 1642 language = ""; 1643 } else { 1644 language = s.substring(0, i); 1645 } 1646 // now decode country 1647 if (i < s.length() - 1) { 1648 s = s.substring(i + 1); 1649 i = s.indexOf('_'); 1650 if (i == -1) { 1651 // there's no separator in the remaining string. 1652 // This is a country 1653 country = s; 1654 } else if (i == 0) { 1655 country = ""; 1656 } else { 1657 country = s.substring(0, i); 1658 } 1659 // now decode variant 1660 if (i < s.length() - 1) { 1661 variant = s.substring(i + 1); 1662 } 1663 } 1664 result = new Locale(language, country, variant); 1665 } catch (Exception e) { 1666 // suppress exception and use default locale 1667 result = null; 1668 } 1669 } 1670 } 1671 } 1672 if (result == null) { 1673 result = Locale.getDefault(); 1674 } 1675 return result; 1676 } 1677 1678 /* 1679 private static void testReadLocale() { 1680 String[] samples = new String[] { "", "en", "de_DE", "_GB", 1681 "en_US_WIN", "de__POSIX", "fr__MAC" }; 1682 Map data = new HashMap(1); 1683 for (String s : samples) { 1684 data.put(LOCALE, s); 1685 System.out.println(s + " -> " + readLocale(data)); 1686 data.clear(); 1687 } 1688 } 1689 1690 private static long computeChecksum(Map data) { 1691 long cs = 0; 1692 for (Iterator iter = data.entrySet().iterator(); iter.hasNext(); ) { 1693 Map.Entry e = (Map.Entry) (iter.next()); 1694 String key = (String) (e.getKey()); 1695 String value = (String)(e.getValue()); 1696 if (!key.equals(CHECKSUM)) { 1697 cs += computeChecksum(key) * computeChecksum(value); 1698 } 1699 } 1700 // ensure result is >= 0 to avoid problems with signed hex numbers 1701 return (cs == Long.MIN_VALUE ? 0 : cs < 0 ? -cs : cs); 1702 } 1703 1704 private static long computeChecksum(String s) { 1705 if (s == null) 1706 return 1; 1707 else { 1708 long cs = 0; 1709 for (int i = 0; i < s.length(); i++) { 1710 cs = cs * 37 + s.charAt(i); 1711 } 1712 return cs; 1713 } 1714 } 1715 */ 1716 1717 /** 1718 * Export values for questions on the current path, by calling {@link Question#export} 1719 * for each question returned by {@link #getPath}. 1720 * It should be called on the root interview to export the values for all 1721 * questions on the current path, or it can be called on a sub-interview 1722 * to export just the values from the question in that sub-interview (and in turn, 1723 * in any further sub-interviews for which there are questions on the path.) 1724 * Unchecked exceptions that arise from each question's export method are treated 1725 * according to the policy set by setExportIgnoreExceptionPolicy for the interview 1726 * for which export was called. 1727 * It may be convenient to ignore runtime exceptions during export, if exceptions 1728 * may be thrown when the interview is incomplete. It may be preferred not 1729 * to ignore any exceptions, if no exceptions are expected. 1730 * @param data The map in which the values will be placed. 1731 * @see #getPath 1732 * @see #setExportIgnoreExceptionPolicy 1733 * @see #EXPORT_IGNORE_ALL_EXCEPTIONS 1734 * @see #EXPORT_IGNORE_RUNTIME_EXCEPTIONS 1735 * @see #EXPORT_IGNORE_NO_EXCEPTIONS 1736 */ 1737 public void export(Map<String, String> data) { 1738 ArrayList<Question> path = new ArrayList<>(); 1739 1740 // new 4.3 semantics allow the path to contain InterviewQuestions, which 1741 // in turn allows sub-interviews to export data. 1742 if (semantics >= SEMANTIC_VERSION_43) 1743 iteratePath0(path, false, true, true); 1744 else 1745 iteratePath0(path, true, true, true); 1746 1747 export0(data, path, false); 1748 1749 // note - hiddenPath only used in root interview, null hiddenPaths 1750 // are expected in this case 1751 if (semantics >= SEMANTIC_VERSION_43 && hiddenPath != null) 1752 export0(data, hiddenPath, true); 1753 } 1754 1755 private void export0(Map<String, String> data, ArrayList<Question> paths, boolean processHidden) { 1756 for (Question path : paths) { 1757 try { 1758 if (path instanceof InterviewQuestion) { 1759 if (!processHidden) 1760 ((InterviewQuestion) path).getTargetInterview().export(data); 1761 else 1762 continue; 1763 } else { 1764 path.export(data); 1765 } 1766 } catch (RuntimeException e) { 1767 switch (exportIgnoreExceptionPolicy) { 1768 case EXPORT_IGNORE_ALL_EXCEPTIONS: 1769 case EXPORT_IGNORE_RUNTIME_EXCEPTIONS: 1770 break; 1771 case EXPORT_IGNORE_NO_EXCEPTIONS: 1772 throw e; 1773 } 1774 } catch (Error e) { 1775 switch (exportIgnoreExceptionPolicy) { 1776 case EXPORT_IGNORE_ALL_EXCEPTIONS: 1777 break; 1778 case EXPORT_IGNORE_RUNTIME_EXCEPTIONS: 1779 case EXPORT_IGNORE_NO_EXCEPTIONS: 1780 throw e; 1781 } 1782 } 1783 } 1784 } 1785 1786 /** 1787 * Get a value representing the policy regarding how to treat 1788 * exceptions that may arise during export. 1789 * @see #setExportIgnoreExceptionPolicy 1790 * @return a value representing the policy regarding how to treat 1791 * exceptions that may arise during export. 1792 * @see #export 1793 * @see #EXPORT_IGNORE_ALL_EXCEPTIONS 1794 * @see #EXPORT_IGNORE_RUNTIME_EXCEPTIONS 1795 * @see #EXPORT_IGNORE_NO_EXCEPTIONS 1796 */ 1797 public int getExportIgnoreExceptionPolicy() { 1798 return exportIgnoreExceptionPolicy; 1799 } 1800 1801 1802 /** 1803 * Set the policy regarding how to treat exceptions that may arise during export. 1804 * The default value is to ignore runtime exceptions. 1805 * @param policy a value representing the policy regarding how to treat exceptions that may arise during export 1806 * @see #getExportIgnoreExceptionPolicy 1807 * @see #export 1808 * @see #EXPORT_IGNORE_ALL_EXCEPTIONS 1809 * @see #EXPORT_IGNORE_RUNTIME_EXCEPTIONS 1810 * @see #EXPORT_IGNORE_NO_EXCEPTIONS 1811 */ 1812 public void setExportIgnoreExceptionPolicy(int policy) { 1813 if (policy < 0 || policy >= EXPORT_NUM_IGNORE_POLICIES) 1814 throw new IllegalArgumentException(); 1815 exportIgnoreExceptionPolicy = policy; 1816 } 1817 1818 /** 1819 * A value indicating that export should ignore all exceptions that arise 1820 * while calling each question's export method. 1821 * @see #setExportIgnoreExceptionPolicy 1822 * @see #export 1823 */ 1824 public static final int EXPORT_IGNORE_ALL_EXCEPTIONS = 0; 1825 1826 /** 1827 * A value indicating that export should ignore runtime exceptions that arise 1828 * while calling each question's export method. 1829 * @see #setExportIgnoreExceptionPolicy 1830 * @see #export 1831 */ 1832 public static final int EXPORT_IGNORE_RUNTIME_EXCEPTIONS = 1; 1833 1834 /** 1835 * A value indicating that export should not ignore any exceptions that arise 1836 * while calling each question's export method. 1837 * @see #setExportIgnoreExceptionPolicy 1838 * @see #export 1839 */ 1840 public static final int EXPORT_IGNORE_NO_EXCEPTIONS = 2; 1841 private static final int EXPORT_NUM_IGNORE_POLICIES = 3; 1842 private int exportIgnoreExceptionPolicy = EXPORT_IGNORE_RUNTIME_EXCEPTIONS; 1843 1844 1845 //----- observers ---------------------------------------- 1846 1847 /** 1848 * Add an observer to monitor updates to the interview. 1849 * @param o an observer to be notified as changes occur 1850 */ 1851 synchronized public void addObserver(Observer o) { 1852 if (o == null) 1853 throw new NullPointerException(); 1854 1855 // we take the hit here of shuffling arrays to make the 1856 // notification faster and more convenient (no casting) 1857 Observer[] newObs = new Observer[observers.length + 1]; 1858 System.arraycopy(observers, 0, newObs, 0, observers.length); 1859 newObs[observers.length] = o; 1860 observers = newObs; 1861 } 1862 1863 /** 1864 * Remove an observer previously registered to monitor updates to the interview. 1865 * @param o the observer to be removed from the list taht are notified 1866 */ 1867 synchronized public void removeObserver(Observer o) { 1868 if (o == null) 1869 throw new NullPointerException(); 1870 1871 // we take the hit here of shuffling arrays to make the 1872 // notification faster and more convenient (no casting) 1873 for (int i = 0; i < observers.length; i++) { 1874 if (observers[i] == o) { 1875 Observer[] newObs = 1876 new Observer[observers.length - 1]; 1877 System.arraycopy(observers, 0, newObs, 0, i); 1878 System.arraycopy(observers, i + 1, newObs, i, observers.length - i - 1); 1879 observers = newObs; 1880 return; 1881 } 1882 } 1883 } 1884 1885 synchronized public boolean containsObserver(Observer o) { 1886 if (o == null) 1887 throw new NullPointerException(); 1888 1889 for (Observer observer : observers) { 1890 if (observer == o) { 1891 return true; 1892 } 1893 } 1894 1895 return false; 1896 } 1897 1898 private void notifyCurrentQuestionChanged(Question q) { 1899 for (int i = 0; i < observers.length && q == getCurrentQuestion(); i++) 1900 observers[i].currentQuestionChanged(q); 1901 } 1902 1903 private void notifyPathUpdated() { 1904 for (Observer observer : observers) observer.pathUpdated(); 1905 } 1906 1907 private Observer[] observers = new Observer[0]; 1908 1909 //----- tag stuff ---------------------------------------- 1910 1911 /** 1912 * Change the base tag for this interview. 1913 * This should not be done for most interviews, since the base tag 1914 * is the basis for storing loading and storing values, and changing 1915 * the base tag may lead to unexpected results. 1916 * Changing the base tag will caused the tags in all statically 1917 * nested interviews and questions to be updated as well. 1918 * This method is primarily intended to be used when renaming 1919 * dynamically allocated loop bodies in ListQuestion. 1920 * @param newBaseTag the new value for the base tag. 1921 */ 1922 protected void setBaseTag(String newBaseTag) { 1923 baseTag = newBaseTag; 1924 updateTags(); 1925 } 1926 1927 private void updateTags() { 1928 // update our own tag 1929 if (parent == null || parent.tag == null) 1930 tag = baseTag; 1931 else if (baseTag == null) // should we allow this? 1932 tag = parent.getTag(); 1933 else 1934 tag = parent.getTag() + "." + baseTag; 1935 1936 // update the tags for the questions in the interview 1937 // and rebuild the tag map 1938 Map<String, Question> newAllQuestions = new LinkedHashMap<>(); 1939 for (Question q : allQuestions.values()) { 1940 q.updateTag(); 1941 newAllQuestions.put(q.getTag(), q); 1942 } 1943 allQuestions = newAllQuestions; 1944 1945 // recursively update children 1946 for (Interview i : children) { 1947 i.updateTags(); 1948 } 1949 } 1950 1951 //----- adding subinterviews and questions ------------------------ 1952 1953 void add(Interview child) { 1954 children.add(child); 1955 } 1956 1957 void add(Question question) { 1958 String qTag = question.getTag(); 1959 Question prev = allQuestions.put(qTag, question); 1960 if (prev != null) 1961 throw new IllegalArgumentException("duplicate questions for tag: " + qTag); 1962 } 1963 1964 //----- versioning ---------------------------------------- 1965 1966 /** 1967 * This method is being used to toggle changes which are not 1968 * backwards compatible with existing interviews. Changing this value 1969 * after you first initialize the top-level interview object is not 1970 * recommended or supported. This method should be called as soon as 1971 * possible during construction. It is recommended that you select 1972 * the most recent version possible when developing a new interview. 1973 * As this interview ages and the harness libraries progress, the 1974 * interview should remain locked at the current behavior. Each subsequent 1975 * version works under the assumption that the behavior of all previous 1976 * versions is also enabled, there is no way to select individual behaviors. 1977 * 1978 * <p> 1979 * The version numbers are effectively an indication of the harness version 1980 * where the behavior was added (32 = 3.2, 50 = 5.0). Gaps in numbering 1981 * would indicate that no incompatible behavior changes occurred. 1982 * </p> 1983 * 1984 * <p> 1985 * Select PRE_32 behavior to select behaviors prior to 3.2. 1986 * </p> 1987 * 1988 * <p> 1989 * In Version 32, the first versioned semantic change was introduced. 1990 * Interviews will generally request SEMANTIC_PRE_32 for old semantics. 1991 * This version has the behavioral changes: 1992 * <dl> 1993 * <dt>ChoiceQuestion</dt> 1994 * <dd>If the value is reset to null, resulting in the value being 1995 * "cleared", new behavior simply calls <code>clear()</code> to do this. Old 1996 * behavior was to select either the last value or first possible 1997 * choice (from the array of possible choices) THEN call <code>updatePath()</code> 1998 * and <code>setEdited()</code>.</dd> 1999 * <dt>FileListQuestion</dt> 2000 * <dd>During construction, <code>clear()</code> will be called before the 2001 * default value is set, in older implementations it was not called.</dd> 2002 * <dt>FileQuestion</dt> 2003 * <dd>During construction, <code>clear()</code> will be called before the 2004 * default value is set, in older implementations it was not called.</dd> 2005 * </dl> 2006 * </p> 2007 * 2008 * <p> 2009 * In Version 43 changes to the way in which <code>export()</code> works were 2010 * introduced. Earlier than this version, the list of questions to call 2011 * <code>export()</code> upon with pre-generated as a flattened list of 2012 * Questions (with all sub-interviews removed). In 43 and later, the 2013 * structure is NOT flattened, but instead exporting will recurse into 2014 * sub-interviews by calling its (the Interview) <code>export()</code>. 2015 * Additionally, questions which are on the path but hidden will be exported. 2016 * Note that being hidden is not the same at being disabled. 2017 * </p> 2018 * 2019 * <p> 2020 * In Version 50, FileListQuestion has significantly altered processing 2021 * of filters. See the {@link com.sun.interview.FileListQuestion#isValueValid} 2022 * for an explanation. 2023 * </p> 2024 * 2025 * @param value Which semantics the interview should use. 2026 * @see #SEMANTIC_PRE_32 2027 * @see #SEMANTIC_VERSION_32 2028 * @see #SEMANTIC_VERSION_43 2029 * @see #SEMANTIC_VERSION_50 2030 * @see #SEMANTIC_MAX_VERSION 2031 * @since 3.2 2032 * @see #getInterviewSemantics 2033 */ 2034 public void setInterviewSemantics(int value) { 2035 if (value <= SEMANTIC_MAX_VERSION) 2036 semantics = value; 2037 } 2038 2039 /** 2040 * Determine which semantics are being used for interview and question 2041 * behavior. This is important because new behavior in future versions 2042 * can cause unanticipated code flow, resulting in incorrect behavior of 2043 * existing code. 2044 * @return The semantics that the interview is currently using. 2045 * @see #setInterviewSemantics 2046 * @since 3.2 2047 */ 2048 public int getInterviewSemantics() { 2049 return semantics; 2050 } 2051 2052 //----- external value management ---------------------------------------- 2053 2054 /** 2055 * Store an "external" value into the configuration. This is a value 2056 * not associated with any interview question and in a separate namespace 2057 * than all the question keys. 2058 * @param key The name of the key to store. 2059 * @param value The value associated with the given key. Null to remove 2060 * the property from the current set of properties for this interview. 2061 * @return The old value of this property, null if not previously set. 2062 * @see #retrieveProperty 2063 */ 2064 public String storeProperty(String key, String value) { 2065 if (getParent() != null) 2066 return getParent().storeProperty(key, value); 2067 else { 2068 if (value == null) { 2069 // remove 2070 if (extraValues == null) 2071 return null; 2072 else 2073 return extraValues.remove(key); 2074 } 2075 2076 if (extraValues == null) 2077 extraValues = new HashMap<>(); 2078 2079 return extraValues.put(key, value); 2080 } 2081 } 2082 2083 /** 2084 * Store a template value into the configuration. 2085 * @param key The name of the key to store. 2086 * @param value The value associated with the given key. 2087 * @return The old value of this property, null if not previously set. 2088 */ 2089 public String storeTemplateProperty(String key, String value) { 2090 if (getParent() != null) 2091 return getParent().storeTemplateProperty(key, value); 2092 else { 2093 ensureTemValuesInitialized(); 2094 return templateValues.put(key, value); 2095 } 2096 } 2097 2098 /** 2099 * Clear a previous template properties and store the new into the configuration. 2100 * @param props The properties to store. 2101 */ 2102 public void storeTemplateProperties(Map<String, String> props) { 2103 if (getParent() != null) 2104 getParent().storeTemplateProperties(props); 2105 else { 2106 ensureTemValuesInitialized(); 2107 templateValues.clear(); 2108 templateValues.putAll(props); 2109 } 2110 } 2111 2112 2113 /** 2114 * Retrieve a property from the collection of "external" values being 2115 * stored in the configuration. 2116 * @param key The key which identifies the property to retrieve. 2117 * @return The value associated with the given key, or null if it is not 2118 * found. 2119 * @see #storeProperty 2120 */ 2121 public String retrieveProperty(String key) { 2122 if (getParent() != null) 2123 return getParent().retrieveProperty(key); 2124 else { 2125 if (extraValues == null) 2126 return null; 2127 2128 return extraValues.get(key); 2129 } 2130 } 2131 2132 /** 2133 * Retrieve a template property. 2134 * @param key The key which identifies the property to retrieve. 2135 * @return The value associated with the given key, or null if it is not 2136 * found. 2137 */ 2138 public String retrieveTemplateProperty(String key) { 2139 if (getParent() != null) 2140 return getParent().retrieveTemplateProperty(key); 2141 else { 2142 ensureTemValuesInitialized(); 2143 return templateValues.get(key); 2144 } 2145 } 2146 2147 public Set<String> retrieveTemplateKeys() { 2148 if (getParent() != null) 2149 return getParent().retrieveTemplateKeys(); 2150 else { 2151 ensureTemValuesInitialized(); 2152 return templateValues.keySet(); 2153 } 2154 } 2155 2156 2157 2158 /** 2159 * Retrieve set of keys for the "external" values being stored in the 2160 * configuration. 2161 * @return The set of keys currently available. Null if there are none. 2162 * All values in the Set are Strings. 2163 * @see #storeProperty 2164 * @see #retrieveProperty 2165 */ 2166 public Set<String> getPropertyKeys() { 2167 if (getParent() != null) 2168 return parent.getPropertyKeys(); 2169 else { 2170 if (extraValues == null || extraValues.size() == 0) 2171 return null; 2172 2173 return extraValues.keySet(); 2174 } 2175 } 2176 2177 /** 2178 * Get a (shallow) copy of the current "external" values. 2179 * @see #storeProperty 2180 * @see #retrieveProperty 2181 * @return The copy of the properties, null if there are none. 2182 */ 2183 public Map<String,String> getExternalProperties() { 2184 if (extraValues != null) 2185 return new HashMap<>(extraValues); 2186 else 2187 return null; 2188 } 2189 2190 2191 /** 2192 * @see #load(Map) 2193 */ 2194 private void loadExternalValues(Map<String, String> data) { 2195 2196 if (extraValues != null) { 2197 extraValues.clear(); 2198 } 2199 2200 Set<String> keys = data.keySet(); 2201 2202 for (String key : keys) { 2203 // look for special external value keys 2204 // should consider removing it from data, is it safe to alter 2205 // that object? 2206 if (key.startsWith(EXTERNAL_PREF)) { 2207 if (extraValues == null) 2208 extraValues = new HashMap<>(); 2209 2210 String val = data.get(key); 2211 2212 // store it, minus the special prefix 2213 extraValues.put(key.substring(EXTERNAL_PREF.length()), val); 2214 } 2215 } // while 2216 } 2217 2218 private void loadTemplateValues(Map<String, String> data) { 2219 2220 if (templateValues != null) { 2221 templateValues.clear(); 2222 } 2223 2224 Set<String> keys = data.keySet(); 2225 for (String key : keys) { 2226 if (key.startsWith(TEMPLATE_PREF)) { 2227 ensureTemValuesInitialized(); 2228 String val = data.get(key); 2229 // store it, minus the special prefix 2230 templateValues.put(key.substring(TEMPLATE_PREF.length()), val); 2231 } 2232 } 2233 } 2234 2235 public void propagateTemplateForAll() { 2236 ensureTemValuesInitialized(); 2237 for (Question q : getAllQuestions().values()) { 2238 q.load(templateValues); 2239 } 2240 } 2241 2242 //----- internal utilities ---------------------------------------- 2243 2244 private void ensureTemValuesInitialized() { 2245 if (templateValues == null) { 2246 templateValues = new HashMap<>(); 2247 } 2248 } 2249 2250 private void ensurePathInitialized() { 2251 if (path == null) { 2252 path = new Path(); 2253 hiddenPath = new ArrayList<>(); 2254 2255 if (parent == null) 2256 rawPath = new Path(); 2257 2258 reset(); 2259 } 2260 2261 if (parent == null && rawPath == null) 2262 rawPath = new Path(); 2263 } 2264 2265 2266 private Question lookup(String tag) { 2267 Question q = allQuestions.get(tag); 2268 // if q is null, search children till we find it 2269 for (int i = 0; i < children.size() && q == null; i++) { 2270 Interview child = children.elementAt(i); 2271 q = child.lookup(tag); 2272 } 2273 2274 return q; 2275 } 2276 2277 /** 2278 * Determine if this is the root interview. 2279 * @return True if this is the root interview. 2280 */ 2281 public boolean isRoot() { 2282 return parent == null; 2283 } 2284 2285 2286 /** 2287 * Get the root interview object for an interview series. 2288 * Some parts of the data are associated only with the root interview, such 2289 * as tags, external values and the {@link Interview#getRawPath} information. 2290 */ 2291 public Interview getRoot() { 2292 /* 2293 Interview i = this; 2294 while (i.root != this) 2295 i = i.parent; 2296 return i; 2297 */ 2298 return root; 2299 } 2300 2301 /** 2302 * Update the current path, typically because a response to 2303 * a question has changed. 2304 */ 2305 public void updatePath() { 2306 root.updatePath0(root.firstQuestion); 2307 } 2308 2309 /** 2310 * Update the current path, typically because a response to 2311 * a question has changed. 2312 * @param q The question that was changed. 2313 */ 2314 public void updatePath(Question q) { 2315 root.updatePath0(q); 2316 } 2317 2318 private void updatePath0(Question q) { 2319 ASSERT(root == this); 2320 2321 if (!updateEnabled) { 2322 // avoid frequent updates during load 2323 return; 2324 } 2325 2326 if (path == null) { 2327 // path has not been initialized yet, so no need to update it 2328 return; 2329 } 2330 2331 // version 4.3 and later allow path reevaluation even if the question 2332 // isn't an active question (probably disabled) 2333 if (semantics < SEMANTIC_VERSION_43 && !pathContains(q)) { 2334 return; 2335 } 2336 2337 // keep a copy of the current path so that if the current 2338 // question is no longer on the path at the end of the update 2339 // we can adjust it as best we can. 2340 Question[] currPath = getPathToCurrent(); 2341 2342 trimPath(q); 2343 predictPath(q); 2344 //showPath(this, q, 0); 2345 verifyPredictPath(); 2346 2347 if (!pathContains(currPath[currPath.length - 1])) 2348 setCurrentQuestionFromPath(currPath); 2349 2350 notifyPathUpdated(); 2351 } 2352 2353 private void verifyPredictPath(){ 2354 for (Interview i : getInterviews()) { 2355 if (i.path != null && i.currIndex >= i.path.size()) { 2356 i.currIndex = i.path.size() - 1; 2357 } 2358 } 2359 } 2360 2361 /* useful debug routine 2362 private void showPath(Interview i, Question q, int depth) { 2363 for (int d = 0; d < depth; d++) 2364 System.err.print(" "); 2365 System.err.println(i.getClass().getName() + " " + i.getTag()); 2366 for (int p = 0; p < i.path.size(); p++) { 2367 for (int d = 0; d < depth; d++) 2368 System.err.print(" "); 2369 Question pq = i.path.questionAt(p); 2370 System.err.print(p + ": " + pq.getClass().getName() + " " + pq.getTag()); 2371 if (pq == q) 2372 System.err.print(" *"); 2373 System.err.println(); 2374 if (pq instanceof InterviewQuestion) 2375 showPath(((InterviewQuestion)pq).getTargetInterview(), q, depth+1); 2376 } 2377 } 2378 */ 2379 2380 private void trimPath(Question q) { 2381 Object o = q; 2382 Interview i = q.getInterview(); 2383 while (i != null) { 2384 // try to find o within i's path 2385 i.ensurePathInitialized(); 2386 Path iPath = i.path; 2387 int oIndex = -1; 2388 for (int pi = 0; pi < iPath.size(); pi++) { 2389 Question qq = iPath.questionAt(pi); 2390 if (qq == o 2391 || (qq instanceof InterviewQuestion 2392 && ((InterviewQuestion) qq).getTargetInterview() == o)) { 2393 oIndex = pi; 2394 break; 2395 } 2396 } 2397 // if not found, this question is not on path 2398 // otherwise, trim i's path to end with o 2399 if (oIndex == -1) 2400 return; 2401 else 2402 iPath.setSize(oIndex + 1); 2403 2404 // repeat with caller, all the way up the call stack 2405 o = i; 2406 i = (i.caller == null ? null : i.caller.getInterview()); 2407 } 2408 } 2409 2410 private void predictPath(Question q) { 2411 // start filling out path 2412 Interview i = q.getInterview(); 2413 i.ensurePathInitialized(); 2414 q = predictNext(q); 2415 while (true) { 2416 // note: multiple exit conditions within loop body 2417 if (q == null || pathContains(q)) 2418 break; 2419 else if (q instanceof FinalQuestion) { 2420 // end of an interview; continue in caller if available 2421 i.path.addQuestion(q); 2422 i.root.rawPath.addQuestion(q); 2423 if (i.caller == null) { 2424 break; 2425 } 2426 else { 2427 q = i.caller.getNext(); 2428 i = i.caller.getInterview(); 2429 } 2430 } 2431 else if (q instanceof InterviewQuestion) { 2432 InterviewQuestion iq = (InterviewQuestion)q; 2433 Interview i2 = iq.getTargetInterview(); 2434 if (pathContains(i2)) 2435 break; 2436 else if (i2 instanceof ListQuestion.Body) { 2437 // no need to predict the body, right? 2438 // because it was done in predictNext() 2439 i2.caller = iq; 2440 i.path.addQuestion(iq); 2441 i.root.rawPath.addQuestion(iq); 2442 q = (i2.path.lastQuestion() instanceof FinalQuestion ? iq.getNext() : null); 2443 } 2444 else { 2445 i2.caller = iq; 2446 if (i2.path == null) { 2447 i2.path = new Path(); 2448 i2.hiddenPath = new ArrayList<>(); 2449 } 2450 else { 2451 i2.path.clear(); 2452 i2.hiddenPath.clear(); 2453 } 2454 2455 i.path.addQuestion(iq); 2456 i.root.rawPath.addQuestion(iq); 2457 i = i2; 2458 q = i2.firstQuestion; 2459 } 2460 } 2461 else { 2462 if (q.isEnabled()) 2463 i.path.addQuestion(q); 2464 else if (q.isHidden() && !root.hiddenPath.contains(q)) 2465 root.hiddenPath.add(q); 2466 2467 if (root.rawPath.indexOf(q) == -1) 2468 i.root.rawPath.addQuestion(q); 2469 q = predictNext(q); 2470 } 2471 } 2472 } 2473 2474 private Question predictNext(Question q) { 2475 if (q.isEnabled() && !q.isValueValid()) 2476 return null; 2477 2478 if (q instanceof ListQuestion && q.isEnabled()) { 2479 final ListQuestion lq = (ListQuestion) q; 2480 if (lq.isEnd()) 2481 return q.getNext(); 2482 2483 for (int index = 0; index < lq.getBodyCount(); index++) { 2484 Interview b = lq.getBody(index); 2485 if (b.path == null) { 2486 b.path = new Path(); 2487 } 2488 else { 2489 b.path.clear(); 2490 } 2491 2492 b.path.addQuestion(b.firstQuestion); 2493 b.caller = null; 2494 b.predictPath(b.firstQuestion); 2495 } 2496 2497 Interview lqBody = lq.getSelectedBody(); 2498 Question lqOther = lq.getOther(); 2499 if (lqBody == null) 2500 return lqOther.getNext(); 2501 else { 2502 Interview lqInt = lq.getInterview(); 2503 return new InterviewQuestion(lqInt, lqBody, lqOther); 2504 } 2505 } 2506 2507 return q.getNext(); 2508 } 2509 2510 /** 2511 * Get an entry from the resource bundle. 2512 * If the resource cannot be found, a message is printed to the console 2513 * and the result will be a string containing the method parameters. 2514 * @param key the name of the entry to be returned 2515 * {@link java.text.MessageFormat#format} 2516 * @return the formatted string 2517 */ 2518 private String getI18NString(String key) { 2519 return getI18NString(key, empty); 2520 } 2521 2522 private static final Object[] empty = { }; 2523 2524 /** 2525 * Get an entry from the resource bundle. 2526 * If the resource cannot be found, a message is printed to the console 2527 * and the result will be a string containing the method parameters. 2528 * @param key the name of the entry to be returned 2529 * @param arg an argument to be formatted into the result using 2530 * {@link java.text.MessageFormat#format} 2531 * @return the formatted string 2532 */ 2533 private String getI18NString(String key, Object arg) { 2534 return getI18NString(key, new Object[] { arg }); 2535 } 2536 2537 /** 2538 * Get an entry from the resource bundle. 2539 * If the resource cannot be found, a message is printed to the console 2540 * and the result will be a string containing the method parameters. 2541 * @param key the name of the entry to be returned 2542 * @param args an array of arguments to be formatted into the result using 2543 * {@link java.text.MessageFormat#format} 2544 * @return the formatted string 2545 */ 2546 private String getI18NString(String key, Object[] args) { 2547 try { 2548 ResourceBundle b = getResourceBundle(); 2549 if (b != null) 2550 return MessageFormat.format(b.getString(key), args); 2551 } 2552 catch (MissingResourceException e) { 2553 // should msgs like this be i18n and optional? 2554 System.err.println("WARNING: missing resource: " + key); 2555 } 2556 2557 StringBuffer sb = new StringBuffer(key); 2558 for (int i = 0; i < args.length; i++) { 2559 sb.append('\n'); 2560 sb.append(Arrays.toString(args)); 2561 } 2562 return sb.toString(); 2563 } 2564 2565 /** 2566 * Get an entry from the resource bundle. 2567 * The parent and other ancestors bundles will be checked first before 2568 * this interview's bundle, allowing the root interview a chance to override 2569 * the default value provided by this interview. 2570 * @param key the name of the entry to be returned 2571 * @return the value of the resource, or null if not found 2572 */ 2573 protected String getResourceString(String key) { 2574 return getResourceString(key, true); 2575 } 2576 2577 /** 2578 * Get an entry from the resource bundle. If checkAncestorsFirst is true, 2579 * then the parent and other ancestors bundles will be checked first before 2580 * this interview's bundle, allowing the root interview a chance to override 2581 * the default value provided by this interview. Otherwise, the parent bundles 2582 * will only be checked if this bundle does not provide a value. 2583 * @param key the name of the entry to be returned 2584 * @param checkAncestorsFirst whether to recursively call this method on the 2585 * parent (if any) before checking this bundle, or only afterwards, if this 2586 * bundle does not provide a value 2587 * @return the value of the resource, or null if not found 2588 */ 2589 protected String getResourceString(String key, boolean checkAncestorsFirst) { 2590 try { 2591 String s = null; 2592 if (checkAncestorsFirst) { 2593 if (parent != null) 2594 s = parent.getResourceString(key, checkAncestorsFirst); 2595 if (s == null) { 2596 ResourceBundle b = getResourceBundle(); 2597 if (b != null) 2598 s = b.getString(key); 2599 } 2600 } 2601 else { 2602 ResourceBundle b = getResourceBundle(); 2603 if (b != null) 2604 s = b.getString(key); 2605 if (s == null && parent != null) 2606 s = parent.getResourceString(key, checkAncestorsFirst); 2607 } 2608 return s; 2609 } 2610 catch (MissingResourceException e) { 2611 return null; 2612 } 2613 } 2614 2615 // can change this to "assert(b)" in JDK 1.5 2616 private static final void ASSERT(boolean b) { 2617 if (!b) 2618 throw new IllegalStateException(); 2619 } 2620 2621 /** 2622 * The parent interview, if applicable; otherwise null. 2623 */ 2624 private final Interview parent; 2625 2626 /** 2627 * The root (most parent) interview; never null 2628 */ 2629 private final Interview root; 2630 2631 private String baseTag; // tag relative to parent 2632 2633 private String tag; // full tag: parent tag + baseTag 2634 2635 /** 2636 * A descriptive title for the interview. 2637 */ 2638 private String title; 2639 2640 /** 2641 * The first question of the interview. 2642 */ 2643 private Question firstQuestion; 2644 2645 2646 /** 2647 * Any child interviews. 2648 */ 2649 private Vector<Interview> children = new Vector<>(); 2650 2651 /** 2652 * An index of the questions in this interview. 2653 */ 2654 private Map<String, Question> allQuestions = new LinkedHashMap<>(); 2655 2656 /** 2657 * The default image for questions in the interview. 2658 */ 2659 private URL defaultImage; 2660 2661 private Object helpSet; 2662 2663 // object to create HelpSet and Help ID 2664 // in batch mode, this factory should return stubs. 2665 protected final static HelpSetFactory helpSetFactory = createHelpFactory(); 2666 private String bundleName; 2667 private ResourceBundle bundle; 2668 2669 private Path path; 2670 private Path rawPath; 2671 private ArrayList<Question> hiddenPath; 2672 private int currIndex; 2673 private InterviewQuestion caller; 2674 private boolean updateEnabled; 2675 private boolean edited; 2676 2677 private Map<String, Set<Question>> allMarkers; 2678 private Map<String, String> extraValues; // used in top-level interview only 2679 private Map<String, String> templateValues; 2680 2681 private int semantics = SEMANTIC_PRE_32; 2682 2683 static final ResourceBundle i18n = ResourceBundle.getBundle("com.sun.interview.i18n"); 2684 2685 /** 2686 * Where necessary, the harness interview should behave as it did before the 2687 * 3.2 release. This does not control every single possible change in 2688 * behavior, but does control certain behaviors which may cause problems with 2689 * interview code written against an earlier version of the harness. 2690 * @see #setInterviewSemantics 2691 */ 2692 public static final int SEMANTIC_PRE_32 = 0; 2693 2694 /** 2695 * 2696 * Where necessary, the harness interview should behave as it did for the 2697 * 3.2 release. This does not control every single possible change in 2698 * behavior, but does control certain behaviors which may cause problems with 2699 * interview code written against an earlier version of the harness. 2700 * @see #setInterviewSemantics 2701 */ 2702 public static final int SEMANTIC_VERSION_32 = 1; 2703 2704 /** 2705 * 2706 * Where necessary, the harness interview should behave as it did for the 2707 * 4.3 release. This does not control every single possible change in 2708 * behavior, but does control certain behaviors which may cause problems with 2709 * interview code written against an earlier version of the harness. 2710 * 2711 * 2712 * @see #setInterviewSemantics 2713 */ 2714 public static final int SEMANTIC_VERSION_43 = 2; 2715 2716 /** 2717 * 2718 * Where necessary, the harness interview should behave as it did for the 2719 * 4.3 release. This does not control every single possible change in 2720 * behavior, but does control certain behaviors which may cause problems with 2721 * interview code written against an earlier version of the harness. 2722 * 2723 * 2724 * @see #setInterviewSemantics 2725 */ 2726 public static final int SEMANTIC_VERSION_50 = 3; 2727 2728 /** 2729 * The highest version number currently in use. Note that the compiler 2730 * will probably inline this during compilation, so you will be locked at 2731 * the version which you compile against. This is probably a useful 2732 * behavior in this case. 2733 * @see #setInterviewSemantics 2734 */ 2735 public static final int SEMANTIC_MAX_VERSION = 3; 2736 2737 static class Path { 2738 void addQuestion(Question q) { 2739 if (questions == null) 2740 questions = new Question[10]; 2741 else if (numQuestions == questions.length) { 2742 Question[] newQuestions = new Question[2 * questions.length]; 2743 System.arraycopy(questions, 0, newQuestions, 0, questions.length); 2744 questions = newQuestions; 2745 } 2746 2747 questions[numQuestions++] = q; 2748 } 2749 2750 Question questionAt(int index) throws ArrayIndexOutOfBoundsException { 2751 if (index < 0 || index >= numQuestions) 2752 throw new ArrayIndexOutOfBoundsException(); 2753 return questions[index]; 2754 } 2755 2756 Question lastQuestion() { 2757 return questionAt(numQuestions - 1); 2758 } 2759 2760 // return shallow copy 2761 Question[] getQuestions() { 2762 if (questions == null) 2763 return null; 2764 2765 Question[] copy = new Question[questions.length]; 2766 System.arraycopy(questions, 0, copy, 0, questions.length); 2767 return copy; 2768 } 2769 2770 int indexOf(Interview interview) { 2771 for (int index = 0; index < numQuestions; index++) { 2772 Question q = questions[index]; 2773 if (q instanceof InterviewQuestion 2774 && ((InterviewQuestion) q).getTargetInterview() == interview) 2775 return index; 2776 } 2777 return -1; 2778 } 2779 2780 int indexOf(Question target) { 2781 for (int index = 0; index < numQuestions; index++) { 2782 Question q = questions[index]; 2783 if (q == target) 2784 return index; 2785 } 2786 return -1; 2787 } 2788 2789 int size() { 2790 return numQuestions; 2791 } 2792 2793 void setSize(int newSize) { 2794 // expected case is only to shrink size, so questions != null && newSize < questions.length 2795 if (questions != null) { 2796 if (newSize > questions.length) { 2797 Question[] newQuestions = new Question[newSize]; 2798 System.arraycopy(questions, 0, newQuestions, 0, questions.length); 2799 questions = newQuestions; 2800 2801 } 2802 for (int i = newSize; i < numQuestions; i++) 2803 questions[i] = null; 2804 } 2805 else if (newSize > 0) 2806 questions = new Question[newSize]; 2807 2808 numQuestions = newSize; 2809 } 2810 2811 2812 void clear() { 2813 for (int i = 0; i < numQuestions; i++) 2814 questions[i] = null; 2815 numQuestions = 0; 2816 } 2817 2818 private Question[] questions; 2819 private int numQuestions; 2820 } 2821 2822 protected final static String QUESTION = "QUESTION"; 2823 protected final static String INTERVIEW = "INTERVIEW"; 2824 protected final static String LOCALE = "LOCALE"; 2825 //protected final static String CHECKSUM = "CHECKSUM"; 2826 protected final static String MARKERS = "MARKERS"; 2827 protected final static String MARKERS_PREF = "MARKERS."; 2828 protected static final String EXTERNAL_PREF = "EXTERNAL."; 2829 protected static final String TEMPLATE_PREF = "TEMPLATE."; 2830 2831 }