/* * $Id$ * * Copyright (c) 1996, 2015, Oracle and/or its affiliates. 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. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.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.security.AccessController; import java.security.PrivilegedAction; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; 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. */ public class Interview { //----- inner classes ---------------------------------------- /** * This exception is to report problems that occur while updating an interview. */ 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. */ 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} */ public Fault(ResourceBundle i18n, String s, Object o) { super(MessageFormat.format(i18n.getString(s), 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} */ 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. */ public static class NotOnPathFault extends Fault { NotOnPathFault(Question q) { super(i18n, "interview.questionNotOnPath", q.getTag()); } } /** * Not for use, provided for backwards binary compatibility. * @deprecated No longer used in this API, direct JavaHelp usage was removed. */ @Deprecated public static class BadHelpFault extends Fault { public BadHelpFault(ResourceBundle i18n, String s, Object e) { super(i18n, s, e); } }; /** * Not for use, provided for backwards binary compatibility. * @deprecated No longer used in this API, direct JavaHelp usage was removed. */ @Deprecated public static class HelpNotFoundFault extends Fault { public HelpNotFoundFault(ResourceBundle i18n, String s, String name) { super(i18n, s, name); } }; /** * An observer interface for receiving notifications as the state of * the interview is updated. */ 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. */ 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. */ 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. */ public Interview getParent() { return parent; } /** * Get a tag used to qualify the tags of questions in this interview. * @return the title */ public String getTag() { return tag; } /** * Set a descriptive title to be used to annotate this interview. * @param title A short descriptive title. * @see #getTitle */ 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 title. * of the interview. * @return the title * @see #setTitle */ 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 */ 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 */ 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 * internationalized 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 */ protected void setResourceBundle(String name) throws MissingResourceException { // name is not null if (!name.equals(bundleName)) { Class c = getClass(); final ClassLoader cl = c.getClassLoader(); final 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 = AccessController.doPrivileged( new PrivilegedAction() { public ResourceBundle run() { return ResourceBundle.getBundle(rn, Locale.getDefault(), cl); } }); bundleName = name; } } /** * Set the base name of the resource bundle used to look up * internationalized 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 */ protected void setResourceBundle(final String name, File file) throws MissingResourceException { if (bundleName != null && bundleName.equals(name)) { return; } try { URL[] url = {new URL("file:" + file.getAbsolutePath() + "/")}; final URLClassLoader cl = new URLClassLoader(url); bundle = AccessController.doPrivileged( new PrivilegedAction() { public ResourceBundle run() { return ResourceBundle.getBundle(name, Locale.getDefault(), cl); } }); bundleName = name; } 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 */ public ResourceBundle getResourceBundle() { if (bundle == null && parent != null) return parent.getResourceBundle(); else return bundle; } /** * Set the name of the help set used to locate the "more info" * for each question. The name should identify a resource containing * a JavaHelp helpset file. 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. * If help sets are specified for child interviews, they will * automatically be added into the help set for the root interview. * @param name The name of the help set containing the "more info" * for each question. * @throws Interview.HelpNotFoundFault if the help set could not be located * @throws Interview.BadHelpFault if some problem occurred while opening the help set * @see #getHelpSet * @see #setHelpSet(Object) */ protected void setHelpSet(String name) throws Interview.Fault { setHelpSet(helpSetFactory.createHelpSetObject(name, getClass())); } /** * Set the help set used to locate the "more info" for each question. * If help sets are specified for child interviews, they will * automatically be added into the help set for the root interview. * @param hs The help set containing the "more info" for each question * in this interview. * @see #getHelpSet * @see #setHelpSet(String) */ protected void setHelpSet(Object hs) { helpSet = helpSetFactory.updateHelpSetObject(this, hs); } /** * Set the name of the help set used to locate the "more info" * for each question. The name should identify a resource containing * a JavaHelp helpset file. If the name is treated as filename of file * which is located in directory file. * If help sets are specified for child interviews, they will * automatically be added into the help set for the root interview. * @param name The name of the help set containing the "more info" * for each question. * @param file The directory to find help set. * @throws Interview.HelpNotFoundFault if the help set could not be located * @throws Interview.BadHelpFault if some problem occurred while opening the help set * @see #getHelpSet * @see #setHelpSet(Object) * @see #setHelpSet(String) */ protected void setHelpSet(String name, File file) throws Interview.Fault { setHelpSet(helpSetFactory.createHelpSetObject(name, file)); } /** * Get the help set used to locate the "more info" for each question. If the * help set has not been set explicitly, it defaults to the parent's help * set. * * @return the help set used to locate "more info" for questions in this * interview. * @see #setHelpSet */ public Object getHelpSet() { if (helpSet == null && parent != null) return parent.getHelpSet(); return helpSet; } /** * Initializes the help factory - generally only called once per instance of the * system. * @return Create the help factory for the interview system. */ private static HelpSetFactory createHelpFactory() { try { Class factoryClass = Class.forName("com.sun.interview.JavaHelpFactory").asSubclass(HelpSetFactory.class); return factoryClass.getDeclaredConstructor().newInstance(); } catch (ClassNotFoundException e) { return HelpSetFactory.DEFAULT; } catch (Exception e) { e.printStackTrace(System.err); return HelpSetFactory.DEFAULT; } } /** * Mark this interview as having been edited or not. * @param edited whether or not this interview is marked as edited */ 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 */ 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 */ 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 */ 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. */ 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 = children.elementAt(i); Interview iv = c.getInterview0(t); if (iv != null) return iv; } return null; } 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 = 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. */ 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. */ 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. */ 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 */ 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. */ public void reset() { ensurePathInitialized(); // first, reset back to the beginning updateEnabled = true; caller = null; currIndex = 0; path.clear(); path.addQuestion(firstQuestion); if (root == this) { rawPath.clear(); rawPath.addQuestion(firstQuestion); } hiddenPath.clear(); 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. */ private void reset(Question q) { ensurePathInitialized(); // first, reset back to the beginning updateEnabled = true; caller = null; currIndex = 0; path.clear(); hiddenPath.clear(); if (root == this) { rawPath.clear(); rawPath.addQuestion(firstQuestion); } 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 */ 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. */ 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 */ 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. */ 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. */ 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(). *Note: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. */ 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 */ 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 */ 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 */ 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 */ public Question[] getPathToCurrent() { Vector v = new Vector<>(); iteratePath0(v, true, false, true); Question[] p = new Question[v.size()]; v.copyInto(p); return p; } /** * Get the current set path of questions, including some things normally * hidden. Hidden, disabled and final questions are included upon demand. * The list of questions is flattend to only include questions, no * representation of the interview structure is given. * @param includeFinals Should FinalQuestions be included. * @return The current active path of questions, based on the requested * options. Returns null if no path information is available. */ public Question[] getRawPath(boolean includeFinals) { if (rawPath == null) return null; else return rawPath.getQuestions(); } /** * 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 */ 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 */ 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. */ 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. */ 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. */ 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. */ 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 = 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. */ public Map getAllQuestions() { Map m = new LinkedHashMap<>(); getAllQuestions0(m); return m; } private void getAllQuestions0(Map m) { m.putAll(allQuestions); for (int i = 0; i < children.size(); i++) { Interview child = 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. */ public boolean isChecklistEmpty() { for (Iterator iter = iteratePath(true); iter.hasNext(); ) { Question q = 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 */ public Checklist createChecklist() { Checklist c = new Checklist(); for (Iterator iter = iteratePath(true); iter.hasNext(); ) { Question q = iter.next(); Checklist.Item[] items = q.getChecklistItems(); if (items != null) { for (Checklist.Item item : items) c.add(item); } } 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 */ 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 */ 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 */ 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. */ 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 = 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. */ 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 = 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. */ 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 = 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 */ 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. */ 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. */ public void clearMarkedResponses(String name) { if (root != this) { root.clearMarkedResponses(name); return; } if (allMarkers == null) // no markers at all return; Set markersForName = allMarkers.get(name); if (markersForName == null) // no markers for this name return; updateEnabled = false; Question oldCurrentQuestion = getCurrentQuestion(); for (Question q : markersForName) { q.clear(); } updateEnabled = true; updatePath(firstQuestion); Question newCurrentQuestion = getCurrentQuestion(); if (newCurrentQuestion != oldCurrentQuestion) notifyCurrentQuestionChanged(newCurrentQuestion); } private void loadMarkers(Map data) { String s = 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 = data.get(MARKERS_PREF + i + ".name"); String tags = 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 (Map.Entry> e : allMarkers.entrySet()) { String name = e.getKey(); Set markersForName = e.getValue(); if (name != null) data.put(MARKERS_PREF + i + ".name", name); StringBuffer sb = new StringBuffer(); for (Question q : markersForName) { 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. */ 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. */ public void clear() { updateEnabled = false; for (Question q : allQuestions.values()) { q.clear(); } for (int i = 0; i < children.size(); i++) { Interview child = 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. */ 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. */ 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 = 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 (Question q : allQuestions.values()) { q.clear(); } for (Question q : allQuestions.values()) { q.load(data); } for (int i = 0; i < children.size(); i++) { Interview child = children.elementAt(i); child.load(data, false); } if (parent == null) { String qTag = 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 Always true. * @deprecated As of version 4.4.1, checksums are no longer * calculated or checked. True is always returned. */ public static boolean isChecksumValid(Map data, boolean okIfOmitted) { return true; } /** * 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. */ public void save(Map data) { // only in the root interview if (parent == null) { data.put(INTERVIEW, getClass().getName()); data.put(QUESTION, getCurrentQuestion().getTag()); writeLocale(data); if (extraValues != null && extraValues.size() > 0) { Set keys = getPropertyKeys(); for (String key : keys) { data.put(EXTERNAL_PREF + key, extraValues.get(key)); } // while } if (templateValues != null && templateValues.size() > 0) { Set keys = templateValues.keySet(); for (String key : keys) { data.put(TEMPLATE_PREF + key, retrieveTemplateProperty(key)); } // while } } for (Question q : allQuestions.values()) { 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 (Interview child : children) { child.save(data); } saveMarkers(data); //data.put(CHECKSUM, Long.toString(computeChecksum(data), 16)); } /** * Writes information about current locale to the given map. *
* This information is used later to properly restore locale-sensitive values, * like numerics. * @param data target map to write data to * @see #LOCALE * @see #readLocale(Map) */ protected static void writeLocale(Map data) { data.put(LOCALE, Locale.getDefault().toString()); } /** * Reads information about locale from the given map.
* Implementation looks for the string keyed by {@link #LOCALE} and then * tries to decode it to valid locale object. * @param data map with interview values * @return locale, decoded from value taken from map; or default (current) locale * @see #LOCALE * @see #writeLocale(Map) */ protected static Locale readLocale(Map data) { Locale result = null; Object o = data.get(LOCALE); if (o != null) { if (o instanceof Locale) { result = (Locale) o; } else if (o instanceof String) { /* try to decode Locale object from its string representation * @see java.util.Locale#toString() * Examples: "", "en", "de_DE", "_GB", "en_US_WIN", "de__POSIX", "fr__MAC" */ String s = ((String) o).trim(); String language = "", country = "", variant = ""; if (s.length() != 0) { try { // decode language int i = s.indexOf('_'); if (i == -1) { // there's no separator in the string. This can be // only the language language = s; } else if (i == 0) { language = ""; } else { language = s.substring(0, i); } // now decode country if (i < s.length() - 1) { s = s.substring(i + 1); i = s.indexOf('_'); if (i == -1) { // there's no separator in the remaining string. // This is a country country = s; } else if (i == 0) { country = ""; } else { country = s.substring(0, i); } // now decode variant if (i < s.length() - 1) { variant = s.substring(i + 1); } } result = new Locale(language, country, variant); } catch (Exception e) { // suppress exception and use default locale result = null; } } } } if (result == null) { result = Locale.getDefault(); } return result; } /* private static void testReadLocale() { String[] samples = new String[] { "", "en", "de_DE", "_GB", "en_US_WIN", "de__POSIX", "fr__MAC" }; Map data = new HashMap(1); for (String s : samples) { data.put(LOCALE, s); System.out.println(s + " -> " + readLocale(data)); data.clear(); } } 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 */ public void export(Map data) { ArrayList path = new ArrayList<>(); // new 4.3 semantics allow the path to contain InterviewQuestions, which // in turn allows sub-interviews to export data. if (semantics >= SEMANTIC_VERSION_43) iteratePath0(path, false, true, true); else iteratePath0(path, true, true, true); export0(data, path, false); // note - hiddenPath only used in root interview, null hiddenPaths // are expected in this case if (semantics >= SEMANTIC_VERSION_43 && hiddenPath != null) export0(data, hiddenPath, true); } private void export0(Map data, ArrayList paths, boolean processHidden) { for (Question path : paths) { try { if (path instanceof InterviewQuestion) { if (!processHidden) ((InterviewQuestion) path).getTargetInterview().export(data); else continue; } else { path.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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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 */ 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; } } } synchronized public boolean containsObserver(Observer o) { if (o == null) throw new NullPointerException(); for (Observer observer : observers) { if (observer == o) { return true; } } return false; } private void notifyCurrentQuestionChanged(Question q) { for (int i = 0; i < observers.length && q == getCurrentQuestion(); i++) observers[i].currentQuestionChanged(q); } private void notifyPathUpdated() { for (Observer observer : observers) observer.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. */ 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 LinkedHashMap<>(); for (Question q : allQuestions.values()) { q.updateTag(); newAllQuestions.put(q.getTag(), q); } allQuestions = newAllQuestions; // recursively update children for (Interview i : children) { i.updateTags(); } } //----- adding subinterviews and questions ------------------------ void add(Interview child) { children.add(child); } void add(Question question) { String qTag = question.getTag(); Question prev = 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. Each subsequent * version works under the assumption that the behavior of all previous * versions is also enabled, there is no way to select individual behaviors. * *

* The version numbers are effectively an indication of the harness version * where the behavior was added (32 = 3.2, 50 = 5.0). Gaps in numbering * would indicate that no incompatible behavior changes occurred. *

* *

* Select PRE_32 behavior to select behaviors prior to 3.2. *

* *

* In Version 32, the first versioned semantic change was introduced. * Interviews will generally request SEMANTIC_PRE_32 for old semantics. * This version has the behavioral changes: *

*
ChoiceQuestion
*
If the value is reset to null, resulting in the value being * "cleared", new behavior simply calls clear() to do this. Old * behavior was to select either the last value or first possible * choice (from the array of possible choices) THEN call updatePath() * and setEdited().
*
FileListQuestion
*
During construction, clear() will be called before the * default value is set, in older implementations it was not called.
*
FileQuestion
*
During construction, clear() will be called before the * default value is set, in older implementations it was not called.
*
*

* *

* In Version 43 changes to the way in which export() works were * introduced. Earlier than this version, the list of questions to call * export() upon with pre-generated as a flattened list of * Questions (with all sub-interviews removed). In 43 and later, the * structure is NOT flattened, but instead exporting will recurse into * sub-interviews by calling its (the Interview) export(). * Additionally, questions which are on the path but hidden will be exported. * Note that being hidden is not the same at being disabled. *

* *

* In Version 50, FileListQuestion has significantly altered processing * of filters. See the {@link com.sun.interview.FileListQuestion#isValueValid} * for an explanation. *

* * @param value Which semantics the interview should use. * @see #SEMANTIC_PRE_32 * @see #SEMANTIC_VERSION_32 * @see #SEMANTIC_VERSION_43 * @see #SEMANTIC_VERSION_50 * @see #SEMANTIC_MAX_VERSION * @since 3.2 * @see #getInterviewSemantics */ public 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 */ 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 separate namespace * than all the question keys. * @param key The name of the key to store. * @param value The value associated with the given key. Null to remove * the property from the current set of properties for this interview. * @return The old value of this property, null if not previously set. * @see #retrieveProperty */ public String storeProperty(String key, String value) { if (getParent() != null) return getParent().storeProperty(key, value); else { if (value == null) { // remove if (extraValues == null) return null; else return extraValues.remove(key); } if (extraValues == null) extraValues = new HashMap<>(); return 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. */ public String storeTemplateProperty(String key, String value) { if (getParent() != null) return getParent().storeTemplateProperty(key, value); else { ensureTemValuesInitialized(); return templateValues.put(key, value); } } /** * Clear a previous template properties and store the new into the configuration. * @param props The properties to store. */ public void storeTemplateProperties(Map 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 */ public String retrieveProperty(String key) { if (getParent() != null) return getParent().retrieveProperty(key); else { if (extraValues == null) return null; return 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. */ public String retrieveTemplateProperty(String key) { if (getParent() != null) return getParent().retrieveTemplateProperty(key); else { ensureTemValuesInitialized(); return 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 */ public Set getPropertyKeys() { if (getParent() != null) return parent.getPropertyKeys(); else { if (extraValues == null || extraValues.size() == 0) return null; return extraValues.keySet(); } } /** * Get a (shallow) copy of the current "external" values. * @see #storeProperty * @see #retrieveProperty * @return The copy of the properties, null if there are none. */ public Map getExternalProperties() { if (extraValues != null) return new HashMap<>(extraValues); else return null; } /** * @see #load(Map) */ private void loadExternalValues(Map data) { if (extraValues != null) { extraValues.clear(); } Set keys = data.keySet(); for (String key : keys) { // look for special external value keys // should consider removing it from data, is it safe to alter // that object? if (key.startsWith(EXTERNAL_PREF)) { if (extraValues == null) extraValues = new HashMap<>(); String val = data.get(key); // store it, minus the special prefix extraValues.put(key.substring(EXTERNAL_PREF.length()), val); } } // while } private void loadTemplateValues(Map data) { if (templateValues != null) { templateValues.clear(); } Set keys = data.keySet(); for (String key : keys) { if (key.startsWith(TEMPLATE_PREF)) { ensureTemValuesInitialized(); String val = data.get(key); // store it, minus the special prefix templateValues.put(key.substring(TEMPLATE_PREF.length()), val); } } } public void propagateTemplateForAll() { ensureTemValuesInitialized(); for (Question q : getAllQuestions().values()) { q.load(templateValues); } } //----- internal utilities ---------------------------------------- private void ensureTemValuesInitialized() { if (templateValues == null) { templateValues = new HashMap<>(); } } private void ensurePathInitialized() { if (path == null) { path = new Path(); hiddenPath = new ArrayList<>(); if (parent == null) rawPath = new Path(); reset(); } if (parent == null && rawPath == null) rawPath = new Path(); } private Question lookup(String tag) { Question q = 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 = children.elementAt(i); q = child.lookup(tag); } return q; } /** * Determine if this is the root interview. * @return True if this is the root interview. */ public boolean isRoot() { return parent == null; } /** * Get the root interview object for an interview series. * Some parts of the data are associated only with the root interview, such * as tags, external values and the {@link Interview#getRawPath} information. */ public 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. */ 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. */ 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; } // version 4.3 and later allow path reevaluation even if the question // isn't an active question (probably disabled) if (semantics < SEMANTIC_VERSION_43 && !pathContains(q)) { 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); verifyPredictPath(); if (!pathContains(currPath[currPath.length - 1])) setCurrentQuestionFromPath(currPath); notifyPathUpdated(); } private void verifyPredictPath(){ for (Interview i : getInterviews()) { if (i.path != null && i.currIndex >= i.path.size()) { i.currIndex = i.path.size() - 1; } } } /* 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 i.ensurePathInitialized(); 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(); i.ensurePathInitialized(); 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); i.root.rawPath.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); i.root.rawPath.addQuestion(iq); q = (i2.path.lastQuestion() instanceof FinalQuestion ? iq.getNext() : null); } else { i2.caller = iq; if (i2.path == null) { i2.path = new Path(); i2.hiddenPath = new ArrayList<>(); } else { i2.path.clear(); i2.hiddenPath.clear(); } i.path.addQuestion(iq); i.root.rawPath.addQuestion(iq); i = i2; q = i2.firstQuestion; } } else { if (q.isEnabled()) i.path.addQuestion(q); else if (q.isHidden() && !root.hiddenPath.contains(q)) root.hiddenPath.add(q); if (root.rawPath.indexOf(q) == -1) i.root.rawPath.addQuestion(q); q = predictNext(q); } } } private Question predictNext(Question q) { if (q.isEnabled() && !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 * {@link java.text.MessageFormat#format} * @return the formatted string */ 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 */ 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 */ 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(Arrays.toString(args)); } 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 */ 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 */ 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. */ private final Interview parent; /** * The root (most parent) interview; never null */ 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. */ private String title; /** * The first question of the interview. */ private Question firstQuestion; /** * Any child interviews. */ private Vector children = new Vector<>(); /** * An index of the questions in this interview. */ private Map allQuestions = new LinkedHashMap<>(); /** * The default image for questions in the interview. */ private URL defaultImage; private Object helpSet; // object to create HelpSet and Help ID // in batch mode, this factory should return stubs. protected final static HelpSetFactory helpSetFactory = createHelpFactory(); private String bundleName; private ResourceBundle bundle; private Path path; private Path rawPath; private ArrayList hiddenPath; private int currIndex; private InterviewQuestion caller; private boolean updateEnabled; private boolean edited; private Map> allMarkers; private Map extraValues; // used in top-level interview only private Map 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 */ 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 */ public static final int SEMANTIC_VERSION_32 = 1; /** * * Where necessary, the harness interview should behave as it did for the * 4.3 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 */ public static final int SEMANTIC_VERSION_43 = 2; /** * * Where necessary, the harness interview should behave as it did for the * 4.3 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 */ public static final int SEMANTIC_VERSION_50 = 3; /** * 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 */ public static final int SEMANTIC_MAX_VERSION = 3; 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); } // return shallow copy Question[] getQuestions() { if (questions == null) return null; Question[] copy = new Question[questions.length]; System.arraycopy(questions, 0, copy, 0, questions.length); return copy; } 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 indexOf(Question target) { for (int index = 0; index < numQuestions; index++) { Question q = questions[index]; if (q == target) 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."; }