Logo Search packages:      
Sourcecode: cacao-oj6 version File versions

Interview.java

/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.interview;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.text.MessageFormat;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.Vector;


//import com.sun.javatest.util.DirectoryClassLoader;

/**
 * The base class for an interview: a series of {@link Question questions}, to be
 * presented to the user via some tool such as an assistant or wizard. 
 * Interviews may be stand-alone, or designed to be part of other interviews.
 */
00055 public class Interview 
{
    //----- inner classes ----------------------------------------

    /**
     * This exception is to report problems that occur while updating an interview.
     */
00062     public static class Fault extends Exception
    {
      /**
       * Create a Fault.
       * @param i18n A resource bundle in which to find the detail message.
       * @param s The key for the detail message.
       */
00069       public Fault(ResourceBundle i18n, String s) {
          super(i18n.getString(s));
      }

      /**
       * Create a Fault.
       * @param i18n A resource bundle in which to find the detail message.
       * @param s The key for the detail message.
       * @param o An argument to be formatted with the detail message by
       * {@link java.text.MessageFormat#format}
       */
00080       public Fault(ResourceBundle i18n, String s, Object o) {
          super(MessageFormat.format(i18n.getString(s), new Object[] {o}));
      }

      /**
       * Create a Fault.
       * @param i18n A resource bundle in which to find the detail message.
       * @param s The key for the detail message.
       * @param o An array of arguments to be formatted with the detail message by
       * {@link java.text.MessageFormat#format}
       */
00091       public Fault(ResourceBundle i18n, String s, Object[] o) {
          super(MessageFormat.format(i18n.getString(s), o));
      }
    }

    /**
     * This exception is thrown when a question is expected to be on
     * the current path, and is not.
     */
00100     public static class NotOnPathFault extends Fault
    {
      NotOnPathFault(Question q) {
          super(i18n, "interview.questionNotOnPath", q.getTag());
      }
    }

    /**
     * An observer interface for receiving notifications as the state of
     * the interview is updated.
     */
00111     public static interface Observer {
      /**
       * Invoked when the current question in the interview has been changed.
       * @param q the new current question
       */
      void currentQuestionChanged(Question q);
      
      /**
       * Invoked when the set of questions in the current path has been
       * changed. This is normally because the response to one of the
       * questions on the path has been changed, thereby causing a change
       * to its successor questions.
       */
      void pathUpdated();
    }

    //----- constructors ----------------------------------------

    /**
     * Create a top-level interview.
     * @param tag A tag that will be used to qualify the tags of any
     * questions in this interview, to help ensure uniqueness of those
     * tags.
     */
00135     protected Interview(String tag) {
      this(null, tag);
    }

    /**
     * Create an interview to be used as part of another interview.
     * @param parent The parent interview of which this is a part.
     * @param baseTag A name that will be used to qualify the tags of any
     * questions in this interview, to help ensure uniqueness of those
     * tags. It will be combined with the parent's tag if that has been
     * specified.
     */
00147     protected Interview(Interview parent, String baseTag) {
      this.parent = parent;
      setBaseTag(baseTag);

      if (parent == null)
          root = this;
      else {
          parent.add(this);
          root = parent.root;
          semantics = parent.getInterviewSemantics();
      }
    }

    //----- basic facilities ----------------------------------------

    /**
     * Get the parent interview for which this is a child.
     * @return the parent interview, or null if no parent has been specified.
     */
00166     public Interview getParent() {
      return parent;
    }

    /**
     * Get a tag used to qualify the tags of questions in this interview.
     * @return the title
     */
00174     public String getTag() {
      return tag;
    }

    /**
     * Set a descriptive title to be used to annotate this interview.
     * @param title A short descriptive title.
     * @see #getTitle
     */
00183     protected void setTitle(String title) {
      this.title = title;
    }

    /**
     * Get a descriptive title associated with this interview.
     * If not specified, the system will try and locate the title in the 
     * interview's resource bundle, using the resource name <code>title</code>.
     * of the interview.
     * @return the title
     * @see #setTitle
     */
00195     public String getTitle() {
      if (title == null) {
          // Need to dance a bit here to avoid "title" being picked up 
          // by the i18n validation scripts as a necessary key in i18n 
          // instead of bundle.  Another solution would be to make 
          // getI18NString static and pass the resource bundle in as the
          // first arg.
          String titleKey = "title";
          title = getI18NString(titleKey).trim();
      }

      return title;
    }

    /**
     * Set a default image to be used for the questions of an interview.
     * @param u A URL for the image
     * @see Question#setImage
     * @see Question#getImage
     * @see #getDefaultImage
     */
00216     protected void setDefaultImage(URL u) {
      defaultImage = u;
    }
    
    /** 
     * Get a default image to be used for the questions of an interview.
     * If no default has been set for this interview, the parent's 
     * default image (if any) is used instead.
     * @return a URL for the default image to be used
     * @see #setDefaultImage
     */
00227     public URL getDefaultImage() {
      if (defaultImage == null && parent != null)
          return parent.getDefaultImage();
      
      return defaultImage;
    }

    /**
     * Set the base name of the resource bundle used to look up 
     * internationized strings, such as the title and text of each
     * question.  If the name starts with '/', it will be treated 
     * as an absolute resource name, and used "as is"; 
     * otherwise it will be treated as relative to the
     * package in which the actual interview class is defined.
     * The default is the interview tag name if this is a root 
     * interview. If this is a child interview, there is no default
     * resource bundle.
     * @param name The name of the resource bundle used to look
     * up internationalized strings.
     * @throws MissingResourceException if the resource bundle 
     * cannot be found.
     * @see #getResourceBundle
     */
00250     protected void setResourceBundle(String name) 
      throws MissingResourceException 
    {
      if (name != bundleName) {
          if (name != null) {
            Class c = getClass();
            ClassLoader cl = c.getClassLoader();
            String rn;
            if (name.startsWith("/"))
                rn = name.substring(1);
            else {
                String cn = c.getName();
                String pn = cn.substring(0, cn.lastIndexOf('.'));
                rn = pn + "." + name;
            }
            //System.err.println("INT: looking for bundle: " + rn);
            bundle = ResourceBundle.getBundle(rn, Locale.getDefault(), cl);
          }
          bundleName = name;
      }
    }
    /**
     * Set the base name of the resource bundle used to look up 
     * internationized strings, such as the title and text of each
     * question. If the name is treated as filename of file
     * which is located in directory file.
     * The default is the interview tag name if this is a root 
     * interview. If this is a child interview, there is no default
     * resource bundle.
     * @param name The name of the resource bundle used to look
     * up internationalized strings.
     * @param file The directory to find name.
     * @throws MissingResourceException if the resource bundle 
     * cannot be found.
     * @see #getResourceBundle
     */
00286     protected void setResourceBundle(String name, File file) 
    throws MissingResourceException {
      if (name == bundleName) {
            return;
      }
      try {
            URL[] url = {new URL("file:" + file.getAbsolutePath() + "/")};
            URLClassLoader cl = new URLClassLoader(url);                
            bundle = ResourceBundle.getBundle(name, Locale.getDefault(),cl);
      } catch (MalformedURLException e) {
                              
            }
            
      }

    /**
     * Get the resource bundle for this interview, used to look up
     * internationalized strings, such as the title and text of each question.
     * If the bundle has not been set explicitly, it defaults to the
     * parent's resource bundle; the root interview has a default resource
     * bundle based on the interview tag name.
     * @return the resource bundle for this interview.
     * @see #setResourceBundle
     */
00310     public ResourceBundle getResourceBundle() {
      if (bundle == null && parent != null)
          return parent.getResourceBundle();
      else  
          return bundle;
    } 

    /**
     * Mark this interview as having been edited or not.
     * @param edited whether or not this interview is marked as edited
     */
00321     public void setEdited(boolean edited) {
      Interview i = this;
      while (i.parent != null)
          i = i.parent;
      i.edited = edited;
    }


    /**
     * Determine if this interview as having been edited or not.
     * @return true if this interview is marked as having been edited
     */
00333     public boolean isEdited() {
      Interview i = this;
      while (i.parent != null)
          i = i.parent;
      return i.edited;
    }

    /**
     * Get the first question of the interview.
     * @return the first question of the interview
     * @see #setFirstQuestion
     */
00345     public Question getFirstQuestion() {
      return firstQuestion;
    }
      
    /**
     * Set the first question for an interview. This may be called more
     * than once, but only while the interview is being constructed.
     * Once any method has been called that refers to the interview
     * path, the initial question may not be changed.
     * @param q The initial question
     * @throws IllegalStateException if it is too late to change the
     * initial question.
     * @see #getFirstQuestion
     */
00359     protected void setFirstQuestion(Question q) {
      if (path != null)
          throw new IllegalStateException();

      firstQuestion = q;

      // OLD: the problem with this is that reset() calls updatePath()
      // which might call methods which refer to uninitialize data,
      // so can't safely call reset() here
      //
      // if (parent == null)
      //    reset();

        // if we wanted to permit the first question to be changed,
      // consider the following:
      // if (parent == null)
      //    path = null;
    }

    //---------------------------------------------------------

    /**
     * Get a sub-interview with a given tag name. All descendents are 
     * searched (i.e. all children, all their children, etc.)
     * @param tag The tag of the interview to be found.
     * @return the sub-interview with the specified name.
     * @throws Interview.Fault if no interview is found with the given name.
     */
00387     public Interview getInterview(String tag) throws Fault {
      if (tag == null)
          throw new NullPointerException();

      Interview i = getInterview0(tag);
      if (i != null)
          return i;
      else
          throw new Fault(i18n, "interview.cantFindInterview", tag);
    }

    private Interview getInterview0(String t) {
      if (t.equals(tag))
          return this;
            
      for (int i = 0; i < children.size(); i++) {
          Interview c = (Interview) (children.elementAt(i));
          Interview iv = c.getInterview0(t);
          if (iv != null)
            return iv;
      }

      return null;
    }

    private Set getInterviews() {
      Set s = new HashSet();
      getInterviews0(s);
      return s;
    }

    private void getInterviews0(Set s) {
      s.add(this);
      for (int i = 0; i < children.size(); i++) {
          Interview child = (Interview) (children.elementAt(i));
          child.getInterviews0(s);
      }
    }

    //----- navigation ----------------------------------------
    
    /**
     * Determine if a question is the first question of the interview.
     * @param q the question to check
     * @return true if this is the first question.
     */
00433     public boolean isFirst(Question q) {
      return (q == firstQuestion);
    }

    /**
     * Determine if a question is the last question of the interview.
     * @param q the question to check
     * @return true if this is the last question.
     */
00442     public boolean isLast(Question q) {
      return (q instanceof FinalQuestion && q.interview.caller == null);
    }

    /**
     * Determine if a question has a non-null successor.
     * @param q the question to check
     * @return true if this question has a non-null successor.
     */
00451     public boolean hasNext(Question q) {
      return (q.getNext() != null);
    }


    /**
     * Determine if a question has a successor which is neither null 
     * nor an ErrorQuestion.
     * @param q the question to check
     * @return true if this question has a successor which is neither null 
     * nor an ErrorQuestion
     */
00463     public boolean hasValidNext(Question q) {
      Question qn = q.getNext();
      return (qn != null && !(qn instanceof ErrorQuestion));
    }

    /**
     * Start (or restart) the interview. The current question is reset to the first
     * question, and the current path is evaluated from there.
     */
00472     public void reset() {
      ensurePathInitialized();

      // first, reset back to the beginning
      updateEnabled = true;
      caller = null;
      currIndex = 0;
      path.clear();
      path.addQuestion(firstQuestion);
      updatePath(firstQuestion);
      // notify observers
      notifyCurrentQuestionChanged(firstQuestion);
    }

    /**
     * Start (or restart) the interview. The current question is reset to the first
     * question, and the current path is evaluated from there.
     */
00490     private void reset(Question q) {
      ensurePathInitialized();

      // first, reset back to the beginning
      updateEnabled = true;
      caller = null;
      currIndex = 0;
      path.clear();
      path.addQuestion(firstQuestion);
      updatePath(firstQuestion);

      // now update to the selected question
      if (q == firstQuestion || q == null)
          // already there; just need to notify observers
          notifyCurrentQuestionChanged(firstQuestion);
      else {
          // try and select the specified question
          try {
            setCurrentQuestion(q);
          }
          catch (Fault e) {
            notifyCurrentQuestionChanged(firstQuestion);
          }
      }
    }

    /**
     * Advance to the next question in the interview.
     * Questions that have been {@link Question#isEnabled disabled} will
     * be skipped over.
     * @throws Interview.Fault if there are no more questions
     */
00522     public void next() throws Fault {
      ensurePathInitialized();

      Interview i = this;

      // first, step in until we get to the current question
      while (i.path.questionAt(i.currIndex) instanceof InterviewQuestion) {
          InterviewQuestion iq = (InterviewQuestion) (i.path.questionAt(i.currIndex));
          i = iq.getTargetInterview();
      }

      // next, step forward to the next question
      i.currIndex++;

      // finally, normalize the result
      while (true) {
          if (i.currIndex == i.path.size()) {
            i.currIndex--;
            throw new Fault(i18n, "interview.noMoreQuestions");
          }

          Question q = i.path.questionAt(i.currIndex);
          if (q instanceof InterviewQuestion) {
            InterviewQuestion iq = (InterviewQuestion) q;
            i = iq.getTargetInterview();
            i.currIndex = 0;
          }
          else if (q instanceof FinalQuestion && i.caller != null) {
            i = i.caller.getInterview();
            i.currIndex++;
          }
          else 
            break;            
      }

      Question q = i.path.questionAt(i.currIndex);
      notifyCurrentQuestionChanged(q);
    }

    /** 
     * Back up to the previous question in the interview.
     * Questions that have been {@link Question#isEnabled disabled} will
     * be skipped over.
     * @throws Interview.Fault if there is no previous question.
     */
00567     public void prev() throws Fault {
      ensurePathInitialized();

      Interview i = this;

      // first, step in until we get to the current question
      while (i.path.questionAt(i.currIndex) instanceof InterviewQuestion) {
          InterviewQuestion iq = (InterviewQuestion) (i.path.questionAt(i.currIndex));
          i = iq.getTargetInterview();
      }

      // next, step back to the next question
      i.currIndex--;

      // finally, normalize the result
      while (true) {
          if (i.currIndex < 0) {
            if (i.caller == null) {
                i.currIndex = 0;
                throw new Fault(i18n, "interview.noMoreQuestions");
            }
            else {
                i = i.caller.getInterview();
                i.currIndex--;
            }
          }
          else if (i.path.questionAt(i.currIndex) instanceof InterviewQuestion) {
            InterviewQuestion iq = (InterviewQuestion) (i.path.questionAt(i.currIndex));
            i = iq.getTargetInterview();
            i.currIndex = i.path.size() - 1;
          }
          else if (i.path.questionAt(i.currIndex) instanceof FinalQuestion) {
            i.currIndex--;
          }
          else
            break;            
      }

      Question q = i.path.questionAt(i.currIndex);
      notifyCurrentQuestionChanged(q);
    }

    /**
     * Advance to the last question in the interview.
     * Questions that have been {@link Question#isEnabled disabled} will
     * be skipped over.
     * @throws Interview.Fault if there are no more questions
     */
00615     public void last() throws Fault {
      ensurePathInitialized();

      Interview i = this;

      // first, step in until we get to the current question
      while (i.path.questionAt(i.currIndex) instanceof InterviewQuestion) {
          InterviewQuestion iq = (InterviewQuestion) (i.path.questionAt(i.currIndex));
          i = iq.getTargetInterview();
      }

      // navigate around the interview without upsetting any interview's currIndex
      int index = i.currIndex;

      // Scan forward looking for candidates for the last question.
      // The alternative is to advance i.currIndex to the end of this
      // interview and normalize the result, but that gets complicated
      // with the interaction between nested interviews and hidden 
      // questions.
      Question cq = i.path.questionAt(index);
      Question lq = cq;
      index++;

      while (index < i.path.size()) {
          Question q = i.path.questionAt(index);
          
          if (q instanceof InterviewQuestion) {
            i = ((InterviewQuestion) q).getTargetInterview();
            index = 0;
          }
          else if (q instanceof FinalQuestion && i.caller != null) {
            Interview callInterview = i.caller.getInterview();
            int callIndex = callInterview.path.indexOf(i);
            if (callIndex == -1)
                throw new IllegalStateException();
            i = callInterview;
            index = callIndex + 1;
          }
          else {
            // update candidate and move on
            lq = q;
            index++;
          }
      }
      
      if (lq == cq) {
          if ( !(lq instanceof FinalQuestion))  
            throw new Fault(i18n, "interview.noMoreQuestions");
      }
      else 
          setCurrentQuestion(lq);
    }

    /**
     * Check if the interview has been started. An interview is 
     * considered to be at the beginning if there is only one
     * question on the current path of a type that requires a response.
     * This indirectly implies it must be the last question on
     * the current path, and must only be preceded by 
     * {@link NullQuestion information-only} questions.
     * @return true if the first answerable question is unanswered.
     */
00677     public boolean isStarted() {
      Question[] path = root.getPath();
      for (int i = 0; i < path.length - 1; i++) {
          Question q = path[i];
          if (!(q instanceof NullQuestion))
            return true;
      }
      return false;
    }

    /**
     * Check if the interview has been completed. An interview is
     * considered to have been completed if the final question
     * on the current path is of type {@link FinalQuestion}.
     * @return true if the interview has been completed.
     */
00693     public boolean isFinishable() {
      ensurePathInitialized();

      Interview i = root;
      return (i.path.lastQuestion() instanceof FinalQuestion);
    }

    /**
     * Check if this subinterview has been completed. A subinterview is 
     * considered to have been completed if none of the questions from 
     * this subinterview on the current path return null as the result 
     * of getNext().
     *<em>Note:</em>compare this to isFinishable() which checks that the 
     * entire interview (of which this subinterview may be a part) is
     * complete.
     * @return true is this subinterview has been completed.
     */
00710     protected boolean isInterviewFinishable() {
      return (path != null && path.lastQuestion() instanceof FinalQuestion);
    }


    /**
     * Jump to a specific question in the interview. The question
     * must be on the current path, but can be either before or
     * after the current position at the time this is called.
     * @param q The question which is to become the current
     * question in the interview.
     * @throws Interview.Fault if the question given is not on the current path.
     * @see #getCurrentQuestion
     */
00724     public void setCurrentQuestion(Question q) throws Fault {
      if (q == null)
          throw new NullPointerException();

      if (q == getCurrentQuestion()) 
          return;

      boolean ok = root.setCurrentQuestion0(q);
      if (!ok)
          throw new NotOnPathFault(q);

      notifyCurrentQuestionChanged(q);
    }

    private boolean setCurrentQuestion0(Question q) {
      ensurePathInitialized();

      for (int i = 0; i < path.size(); i++) {
          Question qq = path.questionAt(i);
          if (qq.equals(q)) {
            currIndex = i;
            return true;
          }
          else if (qq instanceof InterviewQuestion) {
            if (((InterviewQuestion) qq).getTargetInterview().setCurrentQuestion0(q)) {
                currIndex = i;
                return true;
            }
          }
      }
      return false;
    }

    /**
     * Get the current question in the interview.
     * @return The current question.
     * @see #setCurrentQuestion
     */
00762     public Question getCurrentQuestion() {
      ensurePathInitialized();

      Interview i = root;
      Question q = i.path.questionAt(i.currIndex);
      while (q instanceof InterviewQuestion) {
          i = ((InterviewQuestion) q).getTargetInterview();
          q = i.path.questionAt(i.currIndex);
      }
      return q;
    }

    private void setCurrentQuestionFromPath(Question[] path) {
      root.setCurrentQuestionFromPath0(path);
    }

    private void setCurrentQuestionFromPath0(Question[] path) {
      for (int i = path.length - 1; i >= 0; i--) {
          if (setCurrentQuestion0(path[i])) {
            notifyCurrentQuestionChanged(path[i]);
            return;
          }
      }
    }

    //----- path stuff ----------------------------------------

    /**
     * Get the set of questions on the current path.
     * The first question is determined by the interview; after that,
     * each question in turn determines its successor. The path ends
     * when a question indicates no successor (or erroneously returns
     * a question that is already on the path, that would otherwise
     * form a cycle). The special type of question, {@link FinalQuestion},
     * never returns a successor.
     * Within a particular interview, a question may refer to a
     * nested interview, before continuing within the original interview.
     * Any such references to nested interviews are automatically 
     * expanded by this method, leaving just the complete set of basic 
     * questions on the path.
     * @return an array containing the list of questions on the current path.
     * @see #setFirstQuestion
     * @see Question#getNext
     * @see #getPathToCurrent
     */
00807     public Question[] getPath() {
      Vector v = new Vector();
      iteratePath0(v, true, true, true);
      Question[] p = new Question[v.size()];
      v.copyInto(p);
      return p;
    }

    /**
     * Get the set of questions on the current path up to and
     * including the current question.
     * @return an array containing the list of questions on the 
     * current path up to and including the current question
     * @see #getPath
     */
00822     public Question[] getPathToCurrent() {
      Vector v = new Vector();
      iteratePath0(v, true, false, true);
      Question[] p = new Question[v.size()];
      v.copyInto(p);
      return p;
    }

    /**
     * Get an iterator for the set of questions on the current path.
     * The first question is determined by the interview; after that,
     * each question in turn determines its successor. The path ends
     * when a question indicates no successor (or erroneously returns
     * a question that is already on the path, that would otherwise
     * form a cycle). The special type of question, {@link FinalQuestion},
     * never returns a successor.
     * Within a particular interview, a question may refer to a
     * nested interview, before continuing within the original interview.
     * Such nested interviews may optionally be expanded by this method,
     * depending on the arguments.
     * @param flattenNestedInterviews If true, any nested interviews will
     * be expanded in place and returned via the iterator; otherwise, the
     * the nested interview will be returned instead.
     * @return an Iterator for the questions on the current path
     * @see #iteratePathToCurrent
     */ 
00848     public Iterator iteratePath(boolean flattenNestedInterviews) {
      Vector v = new Vector();
      iteratePath0(v, flattenNestedInterviews, true, true);
      return v.iterator();
    }


    /**
     * Get an iterator for the set of questions on the current path
     * up to and including the current question.
     * @param flattenNestedInterviews If true, any nested interviews will
     * be expanded in place and returned via the iterator; otherwise, the
     * the nested interview will be returned instead.
     * @return an Iterator for the questions on the current path
     * up to and including the current question
     * @see #iteratePath
     */
00865     public Iterator iteratePathToCurrent(boolean flattenNestedInterviews) {
      Vector v = new Vector();
      iteratePath0(v, flattenNestedInterviews, false, true);
      return v.iterator();
    }

    private void iteratePath0(List l, boolean flattenNestedInterviews, boolean all, boolean addFinal) {
      ensurePathInitialized();

      int n = (all ? path.size() : currIndex + 1);
      for (int i = 0; i < n; i++) {
          Question q = path.questionAt(i);
          if (q instanceof InterviewQuestion) {
            if (flattenNestedInterviews) 
                ((InterviewQuestion) q).getTargetInterview().iteratePath0(l, true, all, false);
            else
                l.add(q);
          }
          else if (!addFinal && q instanceof FinalQuestion)
            return;
          else 
            l.add(q);
      }
    }

    /**
     * Verify that the current path contains a specified question, 
     * and throw an exception if it does not.
     * @param q the question to be checked
     * @throws Interview.NotOnPathFault if the current path does not contain
     * the specified question.
     */
00897     public void verifyPathContains(Question q) 
      throws NotOnPathFault
    {
      if (!pathContains(q))
          throw new NotOnPathFault(q);
    }

    /**
     * Check if the path contains a specific question.
     * @param q The question for which to check.
     * @return true if the question is found on the current path.
     */
00909     public boolean pathContains(Question q) {
      return root.pathContains0(q);
    }

    /**
     * Check if the path contains questions from a specific interview.
     * @param i The interview for which to check.
     * @return true if the interview is found on the current path.
     */
00918     public boolean pathContains(Interview i) {
      return      root.pathContains0(i);
    }

    private boolean pathContains0(Object o) {
      ensurePathInitialized();

      for (int index = 0; index < path.size(); index++) {
          Question q = path.questionAt(index);
          if (o == q)
            return true;
          
          if (q instanceof InterviewQuestion) {
            InterviewQuestion iq = (InterviewQuestion) q;
            Interview i = iq.getTargetInterview();
            if (o == i)
                return true;

            if (i.pathContains0(o))
                return true;
          }
      }

      return false;
    }

    /**
     * Get the complete set of questions in this interview and 
     * recursively, in all child interviews.
     * @return a set of all questions in this and every child interview.
     */
00949     public Set getQuestions() {
      Set s = new HashSet();
      getQuestions0(s);
      return s;
    }

    private void getQuestions0(Set s) {
      s.addAll(allQuestions.values());

      for (int i = 0; i < children.size(); i++) {
          Interview child = (Interview) (children.elementAt(i));
          child.getQuestions0(s);
      }
    }

    /**
     * Get all questions in this interview and 
     * recursively, in all child interviews.
     * @return a map containing all questions in this and every child interview.
     */
00969     public Map getAllQuestions() {
      Map m = new HashMap();
      getAllQuestions0(m);
      return m;
    }

    private void getAllQuestions0(Map m) {
      m.putAll(allQuestions);

      for (int i = 0; i < children.size(); i++) {
          Interview child = (Interview) (children.elementAt(i));
          child.getAllQuestions0(m);
      }
    }

    /**
     * Check whether any questions on the current path have any
     * associated checklist items.
     * @return true if no questions have any corresponding checklist
     * items, and false otherwise.
     */
00990     public boolean isChecklistEmpty() {
      for (Iterator iter = iteratePath(true); iter.hasNext(); ) {
          Question q = (Question) (iter.next());
          Checklist.Item[] items = q.getChecklistItems();
          if (items != null && items.length > 0)
            return false;
      }
      return true;
    }

    /**
     * Create a checklist composed of all checklist items
     * for questions on the current path.
     * @return a checklist composed of all checklist items
     * for questions on the current path.
     * @see #getPath
     * @see Question#getChecklistItems
     */
01008     public Checklist createChecklist() {
      Checklist c = new Checklist();
      for (Iterator iter = iteratePath(true); iter.hasNext(); ) {
          Question q = (Question) (iter.next());
          Checklist.Item[] items = q.getChecklistItems();
          if (items != null) {
            for (int i = 0; i < items.length; i++) 
                c.add(items[i]);
          }
      }
      return c;
    }

    /** 
     * Create a checklist item based on entries in the interview's resource bundle.
     * @param sectionKey A key to identify the section name within the interview's resource bundle
     * @param textKey A key to identify the checklist item text within the interview's resource bundle
     * @return a Checklist.Item object composed from the appropriate entries in the interview's resource bundle
     */
01027     public Checklist.Item createChecklistItem(String sectionKey, String textKey) {
      String section = getI18NString(sectionKey);
      String text = getI18NString(textKey);
      return new Checklist.Item(section, text);
    }


    /** 
     * Create a checklist item based on entries in the interview's resource bundle.
     * @param sectionKey A key to identify the section name within the interview's resource bundle
     * @param textKey A key to identify the checklist item text within the interview's resource bundle
     * @param textArg a single argument to be formatted into the checklist item text
     * @return a Checklist.Item object composed from the appropriate entries in the interview's resource bundle and the specified argument value
     */
01041     public Checklist.Item createChecklistItem(String sectionKey, String textKey, Object textArg) {
      String section = getI18NString(sectionKey);
      String text = getI18NString(textKey, textArg);
      return new Checklist.Item(section, text);
    }


    /** 
     * Create a checklist item based on entries in the interview's resource bundle.
     * @param sectionKey A key to identify the section name within the interview's resource bundle
     * @param textKey A key to identify the checklist item text within the interview's resource bundle
     * @param textArgs an array of arguments to be formatted into the checklist item text
     * @return a Checklist.Item object composed from the appropriate entries in the interview's resource bundle and the specified argument values
     */
01055     public Checklist.Item createChecklistItem(String sectionKey, String textKey, Object[] textArgs) {
      String section = getI18NString(sectionKey);
      String text = getI18NString(textKey, textArgs);
      return new Checklist.Item(section, text);
    }

    //----- markers ---------------------------------

    /**
     * Add a named marker for a question.
     * @param q The question for which to add the marker
     * @param name The name of the marker to be added.
     * @throws NullPointerException if the question is null.
     */
01069     void addMarker(Question q, String name) {
      if (root != this) {
          root.addMarker(q, name);
          return;
      }

      if (q == null)
          throw new NullPointerException();

      if (allMarkers == null)
          allMarkers = new HashMap();

      Set markersForName = (Set) (allMarkers.get(name));
      if (markersForName == null) {
          markersForName = new HashSet();
          allMarkers.put(name, markersForName);
      }

      markersForName.add(q);
    }

    /**
     * Remove a named marker for a question.
     * @param q The question for which to remove the marker
     * @param name The name of the marker to be removeded.
     * @throws NullPointerException if the question is null.
     */
01096     void removeMarker(Question q, String name) {
      if (root != this) {
          root.removeMarker(q, name);
          return;
      }

      if (q == null)
          throw new NullPointerException();

      if (allMarkers == null)
          return;

      Set markersForName = (Set) (allMarkers.get(name));
      if (markersForName == null) 
          return;

      markersForName.remove(q);

      if (markersForName.size() == 0)
          allMarkers.remove(name);
    }

    /**
     * Check if a question has a named marker.
     * @param q The question for which to check for the marker
     * @param name The name of the marker to be removed.
     * @throws NullPointerException if the question is null.
     */
01124     boolean hasMarker(Question q, String name) {
      if (root != this)
          return root.hasMarker(q, name);

      if (q == null)
          throw new NullPointerException();

      if (allMarkers == null)
          return false;

      Set markersForName = (Set) (allMarkers.get(name));
      if (markersForName == null) 
          return false;

      return markersForName.contains(q);
    }

    /**
     * Remove all the markers with a specified name.
     * @param name The name of the markers to be removed
     */
01145     public void removeMarkers(String name) {
      if (root != this) {
          root.removeMarkers(name);
          return;
      }

      // just have to remove the appropriate set of markers
      if (allMarkers != null)
          allMarkers.remove(name);
    }

    /**
     * Remove all the markers, whatever their name.
     */
01159     public void removeAllMarkers() {
      if (root != this) {
          root.removeAllMarkers();
          return;
      }

      allMarkers = null;
    }

    /**
     * Clear the response to marked questions.
     * @param name The name of the markers for the questions to be cleared.
     */
01172     public void clearMarkedResponses(String name) {
      if (root != this) {
          root.clearMarkedResponses(name);
          return;
      }

      if (allMarkers == null) // no markers at all
          return;
      
      Set markersForName = (Set) (allMarkers.get(name));
      if (markersForName == null) // no markers for this name
          return;

      updateEnabled = false;
      Question oldCurrentQuestion = getCurrentQuestion();

      for (Iterator iter = markersForName.iterator(); iter.hasNext(); ) {
          Question q = (Question) (iter.next());
          q.clear();
      }
      
      updateEnabled = true;
      updatePath(firstQuestion);

      Question newCurrentQuestion = getCurrentQuestion();
      if (newCurrentQuestion != oldCurrentQuestion)
          notifyCurrentQuestionChanged(newCurrentQuestion);
    }

    private void loadMarkers(Map data) {
      String s = (String) (data.get(MARKERS));
      int count = 0;
      if (s != null) {
          try {
            count = Integer.parseInt(s);
          }
          catch (NumberFormatException e) {
            // ignore
          }
      }

      allMarkers = null;

      for (int i = 0; i < count; i++) {
          String name = (String) (data.get(MARKERS_PREF + i + ".name"));
          String tags = (String) (data.get(MARKERS_PREF + i));
          if (tags != null) 
            loadMarkers(name, tags);
      }
    }

    private void loadMarkers(String name, String tags) {
      int start = -1;
      for (int i = 0; i < tags.length(); i++) {
          if (tags.charAt(i) == '\n') {
            if (start != -1) {
                String tag = tags.substring(start, i).trim();
                loadMarker(name, tag);
                start = -1;
            } 
          }
          else
            if (start == -1)
                start = i;
      }
      if (start != -1) {
          String tag = tags.substring(start).trim();
          loadMarker(name, tag);
      }
    }

    private void loadMarker(String name, String tag) {
      if (tag.length() > 0) {
          Question q = lookup(tag);
          if (q != null)
            addMarker(q, name);
      }
    }
    
    private void saveMarkers(Map data) {
      if (allMarkers == null)
          return;

      int i = 0;
      for (Iterator iter = allMarkers.entrySet().iterator(); iter.hasNext(); ) {
          Map.Entry e = (Map.Entry) (iter.next());
          String name = (String) (e.getKey());
          Set markersForName = (Set) (e.getValue());
          if (name != null)
            data.put(MARKERS_PREF + i + ".name", name);
          StringBuffer sb = new StringBuffer();
          for (Iterator qIter = markersForName.iterator(); qIter.hasNext(); ) {
            Question q = (Question) (qIter.next());
            if (sb.length() > 0)
                sb.append('\n');
            sb.append(q.getTag());
          }
          data.put(MARKERS_PREF + i, sb.toString());
          i++;
      }
      
      if (i > 0)
          data.put(MARKERS, String.valueOf(i));
    }


    //----- nested interview stuff ---------------------------------

    /**
     * Return a special type of question used to indicate that
     * a sub-interview interview should be called before proceeding
     * to the next question in this interview.
     * @param i The nested interview to be called next
     * @param q The next question to be asked when the nested
     * interview has completes with a {@link FinalQuestion final question}.
     * @return a pseudo-question that will call a nested interview before
     * continuing with the specified follow-on question.
     */
01290     protected Question callInterview(Interview i, Question q) {
      return new InterviewQuestion(this, i, q);
    }

    //----- load/save stuff ----------------------------------------

    /**
     * Clear any responses to all the questions in this interview, and then
     * recursively, in its child interviews.
     */
01300     public void clear() {
      updateEnabled = false;
      for (Iterator iter = allQuestions.values().iterator(); iter.hasNext(); ) {
          Question q = (Question) (iter.next());
          q.clear();
      }

      for (int i = 0; i < children.size(); i++) {
          Interview child = (Interview)children.elementAt(i);
          child.clear();
      }
      if (parent == null) {
            extraValues = null;
            templateValues = null;
          reset();
        }
    }

    /**
     * Load the state for questions from an archive map. The map
     * will be passed to each question in this interview and in any
     * child interviews, and each question should {@link Question#load load}
     * its state, according to its tag.
     * The data must normally contain a valid checksum, generated during {@link #save}.
     * @param data The archive map from which the state should be loaded.
     * @throws Interview.Fault if the checksum is found to be incorrect.
     */
01327     public void load(Map data) throws Fault {
      load(data, true);
    }

    /**
     * Load the state for questions from an archive map. The map
     * will be passed to each question in this interview and in any
     * child interviews, and each question should {@link Question#load load}
     * its state, according to its tag.
     * The data must normally contain a valid checksum, generated during {@link #save}.
     * @param data The archive map from which the state should be loaded.
     * @param checkChecksum If true, the checksum in the data will be checked.
     * @throws Interview.Fault if the checksum is found to be incorrect.
     */
01341     public void load(Map data, boolean checkChecksum) throws Fault {
      if (checkChecksum && !isChecksumValid(data, true)) 
          throw new Fault(i18n, "interview.checksumError");

      if (parent == null) {
          String iTag = (String)(data.get(INTERVIEW));
          if (iTag != null && !iTag.equals(getClass().getName()))
            throw new Fault(i18n, "interview.classMismatch");

            loadExternalValues(data);
            loadTemplateValues(data);
      }

      updateEnabled = false;

      // clear all the answers in this interview before loading an
      // responses from the archive
      for (Iterator iter = allQuestions.values().iterator(); iter.hasNext(); ) {
          Question q = (Question) (iter.next());
          q.clear();
      }

      for (Iterator iter = allQuestions.values().iterator(); iter.hasNext(); ) {
          Question q = (Question) (iter.next());
          q.load(data);
      }

      for (int i = 0; i < children.size(); i++) {
          Interview child = (Interview)children.elementAt(i);
          child.load(data, false);
      }

      if (parent == null) {
          String qTag = (String)(data.get(QUESTION));
          Question q = (qTag == null ? null : lookup(qTag));
          reset(q == null ? firstQuestion : q);
      }

      loadMarkers(data);
    }

    /**
     * Check if the checksum is valid for a set of responses.
     * When responses are saved to a map, they are checksummed,
     * so that they can be checked for validity when reloaded.
     * This method verifies that a set of responses are acceptable
     * for loading.
     * @param data The set of responses to be checked.
     * @param okIfOmitted A boolean determining the response if
     * there is no checksum available in the data
     * @return true if the responses have a valid checksum, if given,
     * or whether it is OK to monit the checksum, if not.
     */
01394     public static boolean isChecksumValid(Map data, boolean okIfOmitted) {
      try {
          String s = (String)(data.get(CHECKSUM));
          if (s == null)
            return okIfOmitted;
          else
            return (Long.parseLong(s, 16) == computeChecksum(data));
      }
      catch (NumberFormatException e) {
          return false;
      }
    }

    /**
     * Save the state for questions in an archive map. The map
     * will be passed to each question in this interview and in any
     * child interviews, and each question should {@link Question#save save}
     * its state, according to its tag.
     * @param data The archive in which the values should be saved.
     */
01414     public void save(Map data) {
        // only in the root interview
      if (parent == null) {
          data.put(INTERVIEW, getClass().getName());
          data.put(QUESTION, getCurrentQuestion().getTag());
          data.put(LOCALE, Locale.getDefault().toString());

            if (extraValues != null && extraValues.size() > 0) {
                Set keys = getPropertyKeys();
                Iterator it = keys.iterator();
                while (it.hasNext()) {
                    Object key = it.next();
                    data.put(EXTERNAL_PREF + key, extraValues.get(key));
                }   // while
            }
            
            if (templateValues != null && templateValues.size() > 0) {
                Set keys = templateValues.keySet();
                Iterator it = keys.iterator();
                while (it.hasNext()) {
                    String key = (String)it.next();
                    data.put(TEMPLATE_PREF + key, retrieveTemplateProperty(key));
                }   // while
            }
            
      }

      for (Iterator iter = allQuestions.values().iterator(); iter.hasNext(); ) {
          Question q = (Question) (iter.next());
          try {
            q.save(data);
          }
          catch (RuntimeException ex) {
            System.err.println("warning: " + ex.toString());
            System.err.println("while saving value for question " + q.getTag() + " in interview " + getTag());
          }
      }

      for (int i = 0; i < children.size(); i++) {
          Interview child = (Interview)children.elementAt(i);
          child.save(data);
      }
      
      saveMarkers(data);

      data.put(CHECKSUM, Long.toString(computeChecksum(data), 16));
    }

    private static long computeChecksum(Map data) {
      long cs = 0;
      for (Iterator iter = data.entrySet().iterator(); iter.hasNext(); ) {
          Map.Entry e = (Map.Entry) (iter.next());
          String key = (String) (e.getKey());
          String value = (String)(e.getValue());
          if (!key.equals(CHECKSUM)) {
            cs += computeChecksum(key) * computeChecksum(value);
          }
      }
      // ensure result is >= 0 to avoid problems with signed hex numbers
      return (cs == Long.MIN_VALUE ? 0 : cs < 0 ? -cs : cs);
    }

    private static long computeChecksum(String s) {
      if (s == null)
          return 1;
      else {
          long cs = 0;
          for (int i = 0; i < s.length(); i++) {
            cs = cs * 37 + s.charAt(i);
          }
          return cs;
      }
    }

    /**
     * Export values for questions on the current path, by calling {@link Question#export}
     * for each question returned by {@link #getPath}. 
     * It should  be called on the root interview to export the values for all
     * questions on the current path, or it can be called on a sub-interview
     * to export just the values from the question in that sub-interview (and in turn,
     * in any further sub-interviews for which there are questions on the path.)
     * Unchecked exceptions that arise from each question's export method are treated
     * according to the policy set by setExportIgnoreExceptionPolicy for the interview
     * for which export was called.  
     * It may be convenient to ignore runtime exceptions during export, if exceptions
     * may be thrown when the interview is incomplete.  It may be preferred not
     * to ignore any exceptions, if no exceptions are expected.  
     * @param data The map in which the values will be placed.
     * @see #getPath
     * @see #setExportIgnoreExceptionPolicy
     * @see #EXPORT_IGNORE_ALL_EXCEPTIONS
     * @see #EXPORT_IGNORE_RUNTIME_EXCEPTIONS 
     * @see #EXPORT_IGNORE_NO_EXCEPTIONS
     */
01508     public void export(Map data) {
      Question[] path = getPath();
      for (int i = 0; i < path.length; i++) {
          try {
            path[i].export(data);
          }
          catch (RuntimeException e) {
            switch (exportIgnoreExceptionPolicy) {
            case EXPORT_IGNORE_ALL_EXCEPTIONS:
            case EXPORT_IGNORE_RUNTIME_EXCEPTIONS:
                break;
            case EXPORT_IGNORE_NO_EXCEPTIONS:
                throw e;
            }
          }
          catch (Error e) {
            switch (exportIgnoreExceptionPolicy) {
            case EXPORT_IGNORE_ALL_EXCEPTIONS:
                break;
            case EXPORT_IGNORE_RUNTIME_EXCEPTIONS:
            case EXPORT_IGNORE_NO_EXCEPTIONS:
                throw e;
            }
          }
      }
    }

    /**
     * Get a value representing the policy regarding how to treat
     * exceptions that may arise during export.
     * @see #setExportIgnoreExceptionPolicy
     * @return a value representing the policy regarding how to treat
     * exceptions that may arise during export.
     * @see #export
     * @see #EXPORT_IGNORE_ALL_EXCEPTIONS
     * @see #EXPORT_IGNORE_RUNTIME_EXCEPTIONS 
     * @see #EXPORT_IGNORE_NO_EXCEPTIONS
     */
01546     public int getExportIgnoreExceptionPolicy() {
      return exportIgnoreExceptionPolicy;
    }


    /**
     * Set the policy regarding how to treat exceptions that may arise during export.
     * The default value is to ignore runtime exceptions.
     * @param policy a value representing the policy regarding how to treat exceptions that may arise during export
     * @see #getExportIgnoreExceptionPolicy
     * @see #export
     * @see #EXPORT_IGNORE_ALL_EXCEPTIONS
     * @see #EXPORT_IGNORE_RUNTIME_EXCEPTIONS 
     * @see #EXPORT_IGNORE_NO_EXCEPTIONS
     */
01561     public void setExportIgnoreExceptionPolicy(int policy) {
      if (policy < 0 || policy >= EXPORT_NUM_IGNORE_POLICIES)
          throw new IllegalArgumentException();
      exportIgnoreExceptionPolicy = policy;
    }

    /**
     * A value indicating that export should ignore all exceptions that arise
     * while calling each question's export method.
     * @see #setExportIgnoreExceptionPolicy
     * @see #export
     */
01573     public static final int EXPORT_IGNORE_ALL_EXCEPTIONS = 0;

    /**
     * A value indicating that export should ignore runtime exceptions that arise
     * while calling each question's export method.
     * @see #setExportIgnoreExceptionPolicy
     * @see #export
     */
01581     public static final int EXPORT_IGNORE_RUNTIME_EXCEPTIONS = 1;

    /**
     * A value indicating that export should not ignore any exceptions that arise
     * while calling each question's export method.
     * @see #setExportIgnoreExceptionPolicy
     * @see #export
     */
01589     public static final int EXPORT_IGNORE_NO_EXCEPTIONS = 2;
    private static final int EXPORT_NUM_IGNORE_POLICIES = 3;
    private int exportIgnoreExceptionPolicy = EXPORT_IGNORE_RUNTIME_EXCEPTIONS;


    //----- observers ----------------------------------------

    /**
     * Add an observer to monitor updates to the interview.
     * @param o an observer to be notified as changes occur
     */
01600     synchronized public void addObserver(Observer o) {
      if (o == null)
          throw new NullPointerException();

      // we take the hit here of shuffling arrays to make the
      // notification faster and more convenient (no casting)
      Observer[] newObs = new Observer[observers.length + 1];
      System.arraycopy(observers, 0, newObs, 0, observers.length);
      newObs[observers.length] = o;
      observers = newObs;
    }

    /**
     * Remove an observer previously registered to monitor updates to the interview.
     * @param o the observer to be removed from the list taht are notified
     */
01616     synchronized public void removeObserver(Observer o) {
      if (o == null)
          throw new NullPointerException();

      // we take the hit here of shuffling arrays to make the
      // notification faster and more convenient (no casting)
      for (int i = 0; i < observers.length; i++) {
          if (observers[i] == o) {
            Observer[] newObs =
                new Observer[observers.length - 1];
            System.arraycopy(observers, 0, newObs, 0, i);
            System.arraycopy(observers, i + 1, newObs, i, observers.length - i - 1);
            observers = newObs;
            return;
          }
      }
    }

    private void notifyCurrentQuestionChanged(Question q) {
      for (int i = 0; i < observers.length && q == getCurrentQuestion(); i++)
          observers[i].currentQuestionChanged(q);
    }

    private void notifyPathUpdated() {
      for (int i = 0; i < observers.length; i++)
          observers[i].pathUpdated();
    }

    private Observer[] observers = new Observer[0];   

    //----- tag stuff ----------------------------------------

    /**
     * Change the base tag for this interview. 
     * This should not be done for most interviews, since the base tag
     * is the basis for storing loading and storing values, and changing
     * the base tag may lead to unexpected results. 
     * Changing the base tag will caused the tags in all statically
     * nested interviews and questions to be updated as well.
     * This method is primarily intended to be used when renaming 
     * dynamically allocated loop bodies in ListQuestion.
     * @param newBaseTag the new value for the base tag.
     */
01659     protected void setBaseTag(String newBaseTag) {
      baseTag = newBaseTag;
      updateTags();
    }

    private void updateTags() {
      // update our own tag
      if (parent == null || parent.tag == null)
          tag = baseTag;
      else if (baseTag == null) // should we allow this?
          tag = parent.getTag();
      else
          tag = parent.getTag() + "." + baseTag;

      // update the tags for the questions in the interview
      // and rebuild the tag map
      Map newAllQuestions = new HashMap();
      for (Iterator iter = allQuestions.values().iterator(); iter.hasNext(); ) {
          Question q = (Question) (iter.next());
          q.updateTag();
          newAllQuestions.put(q.getTag(), q);
      }
      allQuestions = newAllQuestions;

      // recursively update children
      for (Iterator iter = children.iterator(); iter.hasNext(); ) {
          Interview i = (Interview) (iter.next());
          i.updateTags();
      }
    }
    
    //----- adding subinterviews and questions ------------------------

    void add(Interview child) {
      children.add(child);
    }

    void add(Question question) {
      String qTag = question.getTag();
      Question prev = (Question) allQuestions.put(qTag, question);
      if (prev != null)
          throw new IllegalArgumentException("duplicate questions for tag: " + qTag);
    }
    
    //----- versioning ----------------------------------------

    /**
     * This method is being used to toggle changes which are not
     * backwards compatible with existing interviews.  Changing this value
     * after you first initialize the top-level interview object is not
     * recommended or supported.  This method should be called as soon as
     * possible during construction.  It is recommended that you select
     * the most recent version possible when developing a new interview.
     * As this interview ages and the harness libraries progress, the
     * interview should remain locked at the current behavior.
     *
     * @param value Which semantics the interview should use.
     * @see #SEMANTIC_PRE_32
     * @see #SEMANTIC_VERSION_32
     * @see #SEMANTIC_MAX_VERSION
     * @since 3.2
     * @see #getInterviewSemantics
     */
01722     protected void setInterviewSemantics(int value) {
      if (value <= SEMANTIC_MAX_VERSION)
          semantics = value;
    }

    /**
     * Determine which semantics are being used for interview and question
     * behavior.  This is important because new behavior in future versions
     * can cause unanticipated code flow, resulting in incorrect behavior of
     * existing code.
     * @return The semantics that the interview is currently using.
     * @see #setInterviewSemantics
     * @since 3.2
     */
01736     public int getInterviewSemantics() {
      return semantics;
    }

    //----- external value management ----------------------------------------

    /**
     * Store an "external" value into the configuration.  This is a value
     * not associated with any interview question and in a seperate namespace
     * than all the question keys.
     * @param key The name of the key to store.
     * @param value The value associated with the given key.
     * @return The old value of this property, null if not previously set.
     * @see #retieveProperty
     */
01751     public String storeProperty(String key, String value) {
        if (getParent() != null)
            return getParent().storeProperty(key, value);
        else {
            if (extraValues == null)
                extraValues = new HashMap();

            return (String)(extraValues.put(key, value));
        }
    }

    /**
     * Store a template value into the configuration.  
     * @param key The name of the key to store.
     * @param value The value associated with the given key.
     * @return The old value of this property, null if not previously set.
     */
01768     public String storeTemplateProperty(String key, String value) {
        if (getParent() != null)
            return getParent().storeTemplateProperty(key, value);
        else {
            ensureTemValuesInitialized();
            return (String)(templateValues.put(key, value));
        }
    }
    
    /**
     * Clear a previous template properties and store the new into the configuration.  
     * @param props The properties to store.
     */
01781     public void storeTemplateProperties(Properties props) {
        if (getParent() != null)
            getParent().storeTemplateProperties(props);
        else {
            ensureTemValuesInitialized();
            templateValues.clear();
            templateValues.putAll(props);
        }
    }
    
    
    /**
     * Retrieve a property from the collection of "external" values being
     * stored in the configuration.
     * @param key The key which identifies the property to retrieve.
     * @return The value associated with the given key, or null if it is not
     *         found.
     * @see #storeProperty
     */
01800     public String retrieveProperty(String key) {
        if (getParent() != null)
            return getParent().retrieveProperty(key);
        else {
            if (extraValues == null)
                return null;

            return (String)(extraValues.get(key));
        }
    }

    /**
     * Retrieve a template property.
     * @param key The key which identifies the property to retrieve.
     * @return The value associated with the given key, or null if it is not
     *         found.
     */
01817     public String retrieveTemplateProperty(String key) {
        if (getParent() != null)
            return getParent().retrieveTemplateProperty(key);
        else {
            ensureTemValuesInitialized();
            return (String)(templateValues.get(key));
        }
    }
    
    public Set retrieveTemplateKeys() {
        if (getParent() != null)
            return getParent().retrieveTemplateKeys();
        else {
            ensureTemValuesInitialized();
            return templateValues.keySet();
        }
    }

    
    
    /**
     * Retrieve set of keys for the "external" values being stored in the
     * configuration.
     * @return The set of keys currently available.  Null if there are none.
     *         All values in the Set are Strings.
     * @see #storeProperty
     * @see #retrieveProperty
     */
01845     public Set getPropertyKeys() {
        if (getParent() != null)
            return parent.getPropertyKeys();
        else {
            if (extraValues == null || extraValues.size() == 0)
                return null;

            return extraValues.keySet();
        }
    }

    /**
     * @see #load(Map)
     */
01859     private void loadExternalValues(Map data) {
        Set keys = data.keySet();
        Iterator it = keys.iterator();

        while (it.hasNext()) {
            // look for special external value keys
            // should consider removing it from data, is it safe to alter
            // that object?
            String key = (String)(it.next());
            if (key.startsWith(EXTERNAL_PREF)) {
                if (extraValues == null)
                    extraValues = new HashMap();

                String val = (String)(data.get(key));

                // store it, minus the special prefix
                extraValues.put(key.substring(EXTERNAL_PREF.length()), val);
            }
        }   // while
    }

    private void loadTemplateValues(Map data) {
        Set keys = data.keySet();
        Iterator it = keys.iterator();

        while (it.hasNext()) {
            String key = (String)(it.next());
            if (key.startsWith(TEMPLATE_PREF)) {
                ensureTemValuesInitialized();
                String val = (String)(data.get(key));
                // store it, minus the special prefix
                templateValues.put(key.substring(TEMPLATE_PREF.length()), val);
            }
        }   
    }
    
    public void propagateTemplateForAll() {
        ensureTemValuesInitialized();
      for (Iterator iter = getAllQuestions().values().iterator(); iter.hasNext(); ) {
          Question q = (Question) (iter.next());
            q.load(templateValues);
      }
    }
    
    //----- internal utilities ----------------------------------------

    private void ensureTemValuesInitialized() {
      if (templateValues  == null) {
          templateValues  = new HashMap();
        }
    }

    private void ensurePathInitialized() {
      if (path == null) {
          path = new Path();
          reset();
      }
    }
    
    
    private Question lookup(String tag) {
      Question q = (Question)(allQuestions.get(tag));
      // if q is null, search children till we find it
      for (int i = 0; i < children.size() && q == null; i++) {
          Interview child = (Interview)children.elementAt(i);
          q = child.lookup(tag);
      }

      return q;       
    }

    // Some of the interview state is anchored off the root interview:
    // the current execution path, markers etc. This provides access to the
    // root. Arguably we could give each interview a root field as well...
    private Interview getRoot() {
      /*
      Interview i = this;
      while (i.root != this)
          i = i.parent;
      return i;
      */
      return root;
    }

    /**
     * Update the current path, typically because a response to
     * a question has changed.
     */
01947     public void updatePath() {
      root.updatePath0(root.firstQuestion);
    }

    /**
     * Update the current path, typically because a response to
     * a question has changed.
     * @param q The question that was changed. 
     */
01956     public void updatePath(Question q) {
       root.updatePath0(q);
    }

    private void updatePath0(Question q) {
      ASSERT(root == this);

      if (!updateEnabled) { 
          // avoid frequent updates during load
          return;
      }

      if (path == null) {
          // path has not been initialized yet, so no need to update it
          return;
      }

      if (!pathContains(q)) {
          // question is not on the path, so nothing to do
          return;
      }

      // keep a copy of the current path so that if the current
      // question is no longer on the path at the end of the update
      // we can adjust it as best we can.
      Question[] currPath = getPathToCurrent();

      trimPath(q);
      predictPath(q);
      //showPath(this, q, 0);

      if (!pathContains(currPath[currPath.length - 1]))
          setCurrentQuestionFromPath(currPath);

      notifyPathUpdated();
    }

    /* useful debug routine
    private void showPath(Interview i, Question q, int depth) {
      for (int d = 0; d < depth; d++) 
          System.err.print("  ");
      System.err.println(i.getClass().getName() + " " + i.getTag());
      for (int p = 0; p < i.path.size(); p++) {
          for (int d = 0; d < depth; d++) 
            System.err.print("  ");
          Question pq = i.path.questionAt(p);
          System.err.print(p + ": " + pq.getClass().getName() + " " + pq.getTag());
          if (pq == q)
            System.err.print(" *");
          System.err.println();
          if (pq instanceof InterviewQuestion)
            showPath(((InterviewQuestion)pq).getTargetInterview(), q, depth+1);
      } 
    }
    */

    private void trimPath(Question q) {
      Object o = q;
      Interview i = q.getInterview();
      while (i != null) {
          // try to find o within i's path
          Path iPath = i.path;
          int oIndex = -1;
          for (int pi = 0; pi < iPath.size(); pi++) {
            Question qq = iPath.questionAt(pi);
            if (qq == o 
                || (qq instanceof InterviewQuestion
                  && ((InterviewQuestion) qq).getTargetInterview() == o)) {
                oIndex = pi;
                break;
            }
          } 
          // if not found, this question is not on path
          // otherwise, trim i's path to end with o
          if (oIndex == -1)
            return;
          else
            iPath.setSize(oIndex + 1);

          // repeat with caller, all the way up the call stack
          o = i;
          i = (i.caller == null ? null : i.caller.getInterview());
      }
    }

    private void predictPath(Question q) {
      // start filling out path
      Interview i = q.getInterview();
      q = predictNext(q);
      while (true) {
          // note: multiple exit conditions within loop body
          if (q == null || pathContains(q))
            break;
          else if (q instanceof FinalQuestion) {
            // end of an interview; continue in caller if available
            i.path.addQuestion(q);
            if (i.caller == null) {
                break;
            }
            else {
                q = i.caller.getNext();
                i = i.caller.getInterview();
            }
          }
          else if (q instanceof InterviewQuestion) {
            InterviewQuestion iq = (InterviewQuestion)q;
            Interview i2 = iq.getTargetInterview();
            if (pathContains(i2))
                break;
            else if (i2 instanceof ListQuestion.Body) {
                // no need to predict the body, right?
                // because it was done in predictNext()
                i2.caller = iq;
                i.path.addQuestion(iq);
                q = (i2.path.lastQuestion() instanceof FinalQuestion ? iq.getNext() : null);
            }
            else {
                i2.caller = iq;
                if (i2.path == null)
                  i2.path = new Path();
                else
                  i2.path.clear();
                i.path.addQuestion(iq);
                i = i2;
                q = i2.firstQuestion;
            }
          }
          else {
            if (q.isEnabled())
                i.path.addQuestion(q);
            q = predictNext(q);
          }
      }
    }

    private Question predictNext(Question q) {
      if (!q.isValueValid())
          return null;

      if (q instanceof ListQuestion && q.isEnabled()) {
          final ListQuestion lq = (ListQuestion) q;
          if (lq.isEnd()) 
            return q.getNext();

          for (int index = 0; index < lq.getBodyCount(); index++) {
            Interview b = lq.getBody(index);
            if (b.path == null)
                b.path = new Path();
            else
                b.path.clear();
            b.path.addQuestion(b.firstQuestion);
            b.caller = null;
            b.predictPath(b.firstQuestion);
          }
          

          Interview lqBody = lq.getSelectedBody();
          Question lqOther = lq.getOther();
          if (lqBody == null) 
            return lqOther.getNext();
          else {
            Interview lqInt = lq.getInterview();
            return new InterviewQuestion(lqInt, lqBody, lqOther);
          }
      }

      return q.getNext();
    }

    /**
     * Get an entry from the resource bundle.
     * If the resource cannot be found, a message is printed to the console
     * and the result will be a string containing the method parameters.
     * @param key the name of the entry to be returned
     * @param arg an argument to be formatted into the result using
     * {@link java.text.MessageFormat#format}
     * @return the formatted string 
     */
02134     private String getI18NString(String key) {
      return getI18NString(key, empty);
    }

    private static final Object[] empty = { };

    /**
     * Get an entry from the resource bundle.
     * If the resource cannot be found, a message is printed to the console
     * and the result will be a string containing the method parameters.
     * @param key the name of the entry to be returned
     * @param arg an argument to be formatted into the result using
     * {@link java.text.MessageFormat#format}
     * @return the formatted string 
     */
02149     private String getI18NString(String key, Object arg) {
      return getI18NString(key, new Object[] { arg });
    }

    /**
     * Get an entry from the resource bundle.
     * If the resource cannot be found, a message is printed to the console
     * and the result will be a string containing the method parameters.
     * @param key the name of the entry to be returned
     * @param args an array of arguments to be formatted into the result using
     * {@link java.text.MessageFormat#format}
     * @return the formatted string
     */
02162     private String getI18NString(String key, Object[] args) {
      try {
          ResourceBundle b = getResourceBundle();
          if (b != null)
            return MessageFormat.format(b.getString(key), args);
      }
      catch (MissingResourceException e) {
          // should msgs like this be i18n and optional?
          System.err.println("WARNING: missing resource: " + key);
      }

      StringBuffer sb = new StringBuffer(key);
      for (int i = 0; i < args.length; i++) {
          sb.append('\n');
          sb.append(args.toString());
      }
      return sb.toString();
    }

    /**
     * Get an entry from the resource bundle. 
     * The parent and other ancestors bundles will be checked first before
     * this interview's bundle, allowing the root interview a chance to override
     * the default value provided by this interview.
     * @param key the name of the entry to be returned
     * @return the value of the resource, or null if not found
     */
02189     protected String getResourceString(String key) {
      return getResourceString(key, true);
    }

    /**
     * Get an entry from the resource bundle. If checkAncestorsFirst is true,
     * then the parent and other ancestors bundles will be checked first before
     * this interview's bundle, allowing the root interview a chance to override
     * the default value provided by this interview. Otherwise, the parent bundles
     * will only be checked if this bundle does not provide a value.
     * @param key the name of the entry to be returned
     * @param checkAncestorsFirst whether to recursively call this method on the
     * parent (if any) before checking this bundle, or only afterwards, if this
     * bundle does not provide a value
     * @return the value of the resource, or null if not found
     */
02205     protected String getResourceString(String key, boolean checkAncestorsFirst) {
      try {
          String s = null;
          if (checkAncestorsFirst) {
            if (parent != null)
                s = parent.getResourceString(key, checkAncestorsFirst);
            if (s == null) {
                ResourceBundle b = getResourceBundle();
                if (b != null)
                  s = b.getString(key);
            }
          }
          else {
            ResourceBundle b = getResourceBundle();
            if (b != null)
                s = b.getString(key);
            if (s == null && parent != null)
                s = parent.getResourceString(key, checkAncestorsFirst);
          }
          return s;
      }
      catch (MissingResourceException e) {
          return null;
      }
    }

    // can change this to "assert(b)" in JDK 1.5
    private static final void ASSERT(boolean b) {
      if (!b)
          throw new IllegalStateException();
    }

    /**
     * The parent interview, if applicable; otherwise null.
     */
02240     private final Interview parent;

    /**
     * The root (most parent) interview; never null
     */
02245     private final Interview root;

    private String baseTag; // tag relative to parent

    private String tag; // full tag: parent tag + baseTag

    /**
     * A descriptive title for the interview.
     */
02254     private String title;
    
    /**
     * The first question of the interview.
     */
02259     private Question firstQuestion;


    /**
     * Any child interviews.
     */
02265     private Vector children = new Vector();

    /**
     * An index of the questions in this interview.
     */
02270     private Map allQuestions = new HashMap();

    /**
     * The default image for questions in the interview.
     */
02275     private URL defaultImage;

    private String bundleName;
    private ResourceBundle bundle;

    private Path path;
    private int currIndex;
    private InterviewQuestion caller;
    private boolean updateEnabled;
    private boolean edited;

    private Map allMarkers;
    private HashMap extraValues;        // used in top-level interview only
    private HashMap templateValues;     

    private int semantics = SEMANTIC_PRE_32;

    static final ResourceBundle i18n = ResourceBundle.getBundle("com.sun.interview.i18n");

    /**
     * Where necessary, the harness interview should behave as it did before the
     * 3.2 release.  This does not control every single possible change in
     * behavior, but does control certain behaviors which may cause problems with
     * interview code written against an earlier version of the harness.
     * @see #setInterviewSemantics
     */
02301     public static final int SEMANTIC_PRE_32 = 0;

    /**
     *
     * Where necessary, the harness interview should behave as it did for the
     * 3.2 release.  This does not control every single possible change in
     * behavior, but does control certain behaviors which may cause problems with
     * interview code written against an earlier version of the harness.
     * @see #setInterviewSemantics
     */
02311     public static final int SEMANTIC_VERSION_32 = 1;

    /**
     * The highest version number currently in use.  Note that the compiler
     * will probably inline this during compilation, so you will be locked at
     * the version which you compile against.  This is probably a useful
     * behavior in this case.
     * @see #setInterviewSemantics
     */
02320     public static final int SEMANTIC_MAX_VERSION = 1;

    static class Path {
      void addQuestion(Question q) {
          if (questions == null) 
            questions = new Question[10];
          else if (numQuestions == questions.length) {
            Question[] newQuestions = new Question[2 * questions.length];
            System.arraycopy(questions, 0, newQuestions, 0, questions.length);
            questions = newQuestions;
          }

          questions[numQuestions++] = q;
      }

      Question questionAt(int index) throws ArrayIndexOutOfBoundsException {
          if (index < 0 || index >= numQuestions)
            throw new ArrayIndexOutOfBoundsException();
          return questions[index];
      }
      
      Question lastQuestion() {
          return questionAt(numQuestions - 1);
      }

      int indexOf(Interview interview) {
          for (int index = 0; index < numQuestions; index++) {
            Question q = questions[index];
            if (q instanceof InterviewQuestion
                && ((InterviewQuestion) q).getTargetInterview() == interview)
                return index;
          }
          return -1;
      }

      int size() {
          return numQuestions;
      }

      void setSize(int newSize) {
          // expected case is only to shrink size, so questions != null && newSize < questions.length
          if (questions != null) {
            if (newSize > questions.length) {
                Question[] newQuestions = new Question[newSize];
                System.arraycopy(questions, 0, newQuestions, 0, questions.length);
                questions = newQuestions;
                
            }
            for (int i = newSize; i < numQuestions; i++)
                questions[i] = null;
          }
          else if (newSize > 0)
            questions = new Question[newSize];

          numQuestions = newSize;
      }
            

      void clear() {
          for (int i = 0; i < numQuestions; i++)
            questions[i] = null;
          numQuestions = 0;
      }

      private Question[] questions;
      private int numQuestions;
    }
    
    protected final static String QUESTION = "QUESTION";
    protected final static String INTERVIEW = "INTERVIEW";
    protected final static String LOCALE = "LOCALE";
    protected final static String CHECKSUM = "CHECKSUM";
    protected final static String MARKERS = "MARKERS";
    protected final static String MARKERS_PREF = "MARKERS.";
    protected static final String EXTERNAL_PREF = "EXTERNAL.";
    protected static final String TEMPLATE_PREF = "TEMPLATE.";
    
}

Generated by  Doxygen 1.6.0   Back to index