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 }