/*
 * $Id$
 *
 * Copyright (c) 2001, 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.javatest;

import com.sun.interview.ErrorQuestion;
import com.sun.interview.FinalQuestion;
import com.sun.interview.Interview;
import com.sun.interview.Question;
import com.sun.javatest.tool.CustomPropagationController;
import com.sun.javatest.tool.FileHistory;
import com.sun.javatest.util.BackupPolicy;
import com.sun.javatest.util.I18NResourceBundle;
import com.sun.javatest.util.PropertyUtils;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.OptionalLong;
import java.util.ResourceBundle;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Vector;

/**
 * Configuration parameters provided via an interview.
 *
 * @since 3.0
 */
public abstract class InterviewParameters
        extends Interview
        implements Parameters {

    static final String TESTSUITE = "TESTSUITE";
    static final String WORKDIR = "WORKDIR";
    static final String NAME = "NAME";
    static final String DESC = "DESCRIPTION";
    static final String MARKERS_ENABLED = "MARKERS.enabled";
    static final String MARKERS_FILTER = "MARKERS.filter";
    static final String IS_TEMPLATE = "IS_TEMPLATE";
    static final String TEMPLATE_PATH = "TEMPLATE_PATH";
    static final String TRUE = "true";
    private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(InterviewParameters.class);
    //----------------------------------------------------------------------------
    private final String[] ignorableProps = {
            INTERVIEW,
            LOCALE, TESTSUITE, WORKDIR, MARKERS,
            IS_TEMPLATE, TEMPLATE_PATH, QUESTION};
    private final String[] ignorablePrefs = {MARKERS_PREF, EXTERNAL_PREF, TEMPLATE_PREF};
    protected File[] kflFiles;

    //----------------------------------------------------------------------------
    private TemplateManager templateManager = null;
    private boolean inGetName;
    private ExcludeListFilter cachedExcludeListFilter;


    //----------------------------------------------------------------------------
    private KeywordsFilter cachedKeywordsFilter;

    //----------------------------------------------------------------------------
    private StatusFilter cachedStatusFilter;
    private TestFilter cachedRelevantTestFilter;
    private TestSuite cachedRelevantTestFilterTestSuite; // do we need this?
    private TestEnvironment cachedRelevantTestFilterEnv;
    private TestFilter[] cachedTestFilters;
    private BackupPolicy backupPolicy;
    private boolean markersEnabled;
    private boolean markersFilterEnabled;
    private File currFile;
    private boolean isTemplate;
    private String templatePath;
    private OptionalLong currFileLastModified = OptionalLong.empty();
    private boolean currFileLoaded;
    private CustomPropagationController pc = new CustomPropagationController();

    /**
     * Create an InterviewParameters object.
     *
     * @param tag The tag used to qualify questions in this interview
     */
    protected InterviewParameters(String tag) {
        super(tag);
    }

    private static boolean equal(boolean[] b1, boolean... b2) {
        if (b1 == null || b2 == null) {
            return b1 == b2;
        }

        if (b1.length != b2.length) {
            return false;
        }

        for (int i = 0; i < b1.length; i++) {
            if (b1[i] != b2[i]) {
                return false;
            }
        }

        return true;
    }

    private static boolean equal(Vector<TestFilter> v, TestFilter... f) {
        if (f == null || v.size() != f.length) {
            return false;
        }
        for (int i = 0; i < v.size(); i++) {
            if (!v.get(i).equals(f[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * Determine if the specified file is an interview file,
     * as determined by whether its extension is .jti or not.
     *
     * @param f the file to be checked
     * @return true if the specified file is an interview file,
     * and false otherwise
     */
    public static boolean isInterviewFile(File f) {
        return f.getName().endsWith(".jti");
    }

    /**
     * Create an InterviewParameters as determined by the contents of an
     * interview file.
     *
     * @param file the file to be read
     * @return an InterviewParameters as determined by the contents of an
     * interview file.
     * @throws IOException     is there is a problem reading the file
     * @throws Interview.Fault if there is a problem instantiating the
     *                         interview
     */
    public static InterviewParameters open(File file)
            throws IOException, Fault {
        return open(file, (TestSuite) null, (WorkDirectory) null);
    }

    /**
     * Create an InterviewParameters by populating the interview for a specified
     * test suite with responses from a given file.
     *
     * @param file      the file to be read
     * @param testSuite the test suite for which to create the interview
     * @return an InterviewParameters as determined by the test suite
     * and the contents of an interview file
     * @throws IOException     is there is a problem reading the file
     * @throws Interview.Fault if there is a problem instantiating the
     *                         interview
     */
    public static InterviewParameters open(File file, TestSuite testSuite)
            throws IOException, Fault {
        if (testSuite == null) {
            throw new NullPointerException();
        }

        return open(file, testSuite, null);
    }

    /**
     * Create an InterviewParameters by populating the interview for a specified
     * work directory with responses from a given file.
     *
     * @param file    the file to be read
     * @param workDir the work directory (implying the test suite) for which
     *                to create the interview
     * @return an InterviewParameters as determined by the work directory
     * and the contents of an interview file
     * @throws IOException     is there is a problem reading the file
     * @throws Interview.Fault if there is a problem instantiating the
     *                         interview
     */
    public static InterviewParameters open(File file, WorkDirectory workDir)
            throws IOException, Fault {
        if (workDir == null) {
            throw new NullPointerException();
        }

        return open(file, workDir.getTestSuite(), workDir);
    }

    /**
     * @throws WorkDirFault   If there is a problem finding the work directory.
     * @throws TestSuiteFault If there is a problem finding the test suite.
     * @throws JTIFault       If there is a problem finding the JTI file.  Not thrown
     *                        if the file is corrupt or incompatible though.
     * @throws Fault          If there is any other problem opening the interview params, such as
     *                        problems with data in the JTI, incompatibilities between the workdir,
     *                        test suite or work dir.
     */
    private static InterviewParameters open(File file, TestSuite testSuite, WorkDirectory workDir)
            throws IOException, Fault {
        // note: the additional Fault types were introduced in JT 3.2.1

        // read the .jti data
        Map<String, String> data;

        try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
            data = PropertyUtils.load(in);
        } catch (RuntimeException e) {
            // can get IllegalArgumentException if the file is corrupt
            throw new JTIFault(i18n, "ip.errorReadingFile", file, e);
        }

        // if the test suite has not been given, set it from the .jti data
        if (testSuite == null) {
            String s = data.get(TESTSUITE);
            if (s == null) {
                throw new Fault(i18n, "ip.noTestSuiteInFile", file);
            }

            try {
                testSuite = TestSuite.open(new File(s));
            } catch (FileNotFoundException e) {
                throw new TestSuiteFault(i18n, "ip.cantFindTestSuiteInFile",
                        s, file);
            } catch (TestSuite.Fault e) {
                throw new Fault(i18n, "ip.cantOpenTestSuiteInFile",
                        s, file, e.getMessage());
            }
        }

        // if the work directory has not been given,
        // set it from the .jti data if given
        if (workDir == null) {
            String s = data.get(WORKDIR);
            if (s != null) {
                try {
                    workDir = WorkDirectory.open(new File(s), testSuite);
                } catch (FileNotFoundException e) {
                    throw new WorkDirFault(i18n, "ip.cantFindWorkDirInFile",
                            s, file);
                } catch (WorkDirectory.Fault e) {
                    throw new Fault(i18n, "ip.cantOpenWorkDirInFile",
                            s, file, e.getMessage());
                }
            }
        }

        InterviewParameters parameters;

        // create the parameters object
        try {
            parameters = testSuite.createInterview();
        } catch (TestSuite.Fault e) {
            throw new Fault(i18n, "ip.cantCreateInterviewForTestSuite",
                    testSuite.getPath(), e.getMessage());
        }

        // set the work dir in the parameters object
        if (workDir != null) {
            parameters.setWorkDirectory(workDir);
        }

        // load the .jti data into the parameters object
        try {
            parameters.load(data, file);
        } catch (InterviewParameters.Fault e) {
            throw new Fault(i18n, "ip.cantLoadInterview",
                    file, e.getMessage());
        }

        return parameters;
    }

    /**
     * Open a a configuration file, based on paths for the configuration file,
     * test suite and work directory. Any, but not all, of these paths may be null.
     * Any non-null path must specify an appropriate existing file, otherwise
     * an exception will be thrown.
     *
     * @param testSuitePath  the path for the test suite; if not specified,
     *                       the test suite will default from the work directory (if specified) or
     *                       the configuration file.
     * @param workDirPath    the path for the work directory; if not specified,
     *                       the work directory will the default from the config file (if specified),
     *                       or will be null if no configuration file is given
     * @param configFilePath the path for the configuration file; if not specified,
     *                       the result will be a blank interview as created by the test suite.
     * @return an InterviewParameters object created from the given arguments
     * @throws Interview.Fault if there is any problem creating the
     *                         result
     */
    public static InterviewParameters open(String testSuitePath, String workDirPath, String configFilePath)
            throws InterviewParameters.Fault {
        File ts = testSuitePath != null && !testSuitePath.isEmpty()
                ? new File(testSuitePath) : null;
        File wd = workDirPath != null && !workDirPath.isEmpty()
                ? new File(workDirPath) : null;
        File cf = configFilePath != null && !configFilePath.isEmpty()
                ? new File(configFilePath) : null;
        return open(ts, wd, cf);
    }

    /**
     * Open a a configuration file, based on paths for the configuration file,
     * test suite and work directory. Any, but not all, of these paths may be null.
     * Any non-null path must specify an appropriate existing file, otherwise
     * an exception will be thrown.
     *
     * @param testSuitePath  the path for the test suite; if not specified,
     *                       the test suite will default from the work directory (if specified) or
     *                       the configuration file.
     * @param workDirPath    the path for the work directory; if not specified,
     *                       the work directory will bdefault from the config file (if specified),
     *                       or will be null if no configuration file is given
     * @param configFilePath the path for the configuration file; if not specified,
     *                       the result will be a blank interview as created by the test suite.
     * @return an InterviewParameters object created from the gievn arguments
     * @throws Interview.Fault if there is any problem creating the
     *                         result
     */
    public static InterviewParameters open(File testSuitePath, File workDirPath, File configFilePath)
            throws InterviewParameters.Fault {

        // open test suite if specified

        TestSuite testSuite;

        if (testSuitePath != null) {
            try {
                testSuite = TestSuite.open(testSuitePath);
            } catch (FileNotFoundException e) {
                throw new Fault(i18n, "ip.cantFindTestSuite", testSuitePath, e);
            } catch (TestSuite.Fault e) {
                throw new Fault(i18n, "ip.cantOpenTestSuite", testSuitePath, e.getMessage());
            }
        } else {
            testSuite = null;
        }

        // open work directory if specified, defaulting test suite if appropriate

        WorkDirectory workDir;

        if (workDirPath != null) {
            try {
                if (testSuite == null) {
                    workDir = WorkDirectory.open(workDirPath);
                    testSuite = workDir.getTestSuite();
                } else {
                    workDir = WorkDirectory.open(workDirPath, testSuite);
                }
            } catch (FileNotFoundException e) {
                throw new Fault(i18n, "ip.cantFindWorkDir", workDirPath, e);
            } catch (WorkDirectory.Fault e) {
                throw new Fault(i18n, "ip.cantOpenWorkDir", workDirPath, e.getMessage());
            }
        } else {
            workDir = null;
        }

        // open config file if specified, defaulting work dir and test suite if appropriate
        // default config from test suite if appropriate

        InterviewParameters config;

        if (configFilePath == null) {
            if (testSuite != null) {
                try {
                    config = testSuite.createInterview();
                } catch (TestSuite.Fault e) {
                    throw new Fault(i18n, "ip.cantCreateInterviewForTestSuite", testSuitePath, e);
                }

                if (workDir != null) {
                    config.setWorkDirectory(workDir);
                    FileHistory h = FileHistory.getFileHistory(workDir, "configHistory.jtl");
                    File latestConfigFile = h.getLatestEntry();

                    if (latestConfigFile != null) {
                        try {
                            config.load(latestConfigFile);
                        } catch (IOException e) {
                            // ignore?
                        }   // catch
                    }
                }   // workdir != null
            } else {
                throw new Fault(i18n, "ip.noPaths");
            }
        } else {
            try {
                if (workDir == null) {
                    if (testSuite == null) {
                        config = open(configFilePath);
                        testSuite = config.getTestSuite();
                    } else {
                        config = open(configFilePath, testSuite);
                    }
                    workDir = config.getWorkDirectory();
                } else {
                    config = open(configFilePath, workDir);
                }
            } catch (FileNotFoundException e) {
                throw new Fault(i18n, "ip.cantFindConfigFile", configFilePath);
            } catch (IOException e) {
                throw new Fault(i18n, "ip.cantOpenConfigFile", configFilePath, e);
            }
        }

        // if still here, and had sufficient args, config should be open
        // and fully initialized
        return config;
    }

    public CustomPropagationController getPropagationController() {
        return pc;
    }

    public void setPropagationController(CustomPropagationController pc) {
        this.pc = pc;
    }

    /**
     * Set the work directory to be used for this test run.
     *
     * @param workDir the work directory to be used for this test run.
     *                It must match the test suite to be used for this test run
     */
    @Override
    public abstract void setWorkDirectory(WorkDirectory workDir);

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

    /**
     * Return the template manager for this InterviewParameters.
     */
    public TemplateManager getTemplateManger() {
        return templateManager;
    }

    /**
     * Set given template manager for this InterviewParameters.
     *
     * @param tm new template manager
     */
    public void setTemplateManger(TemplateManager tm) {
        this.templateManager = tm;
    }

    /**
     * Initialize an InterviewParameters object.
     * This method is called when the object is created
     * from an entry in a .jtt file.
     * By default, the method throws an exception if any arguments
     * are given. It should be redefined by any test suites that wish
     * to support this type of initialization.
     *
     * @param args test suite specific args with which to initialize
     *             this InterviewParameters object
     * @throws Interview.Fault if any problems occurred while processing the arguments
     */
    public void init(String... args) throws Fault {
        if (args != null && args.length > 0) {
            throw new Fault(i18n, "ip.unknownArgs");
        }
    }


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

    /**
     * Clean up an InterviewParameters object.
     * This method should be invoked at the moment InterviewParameters object
     * becomes useless by the code, that controls it's lifecycle. For example,
     * at the end of the method which created it's local instance.
     * <p>
     * Any following invocations on this object may result in unpredictable
     * exceptions because of object inconsistence.
     */
    public void dispose() {
        kflFiles = null;
        backupPolicy = null;
        cachedExcludeListFilter = null;
        cachedKeywordsFilter = null;
        cachedRelevantTestFilter = null;
        cachedRelevantTestFilterEnv = null;
        cachedStatusFilter = null;
        cachedTestFilters = null;
        pc = null;
        templateManager = null;
    }

    /**
     * Get the name for this configuration.
     * By default and for backwards compatibility, this defaults to the
     * name of the test environment, which means that the whole environment
     * may need to be evaluated to get the required value. Subtypes may
     * choose to override this method to provide a more efficient
     * implementation.
     *
     * <p> Since the default implementation gets the name from the test
     * environment, clients should not use this method to determine
     * the name for the test environment, unless this method is redefined.
     * The default implementation detects such a circular usage, and
     * returns null in this case.
     *
     * @return the name for this configuration, or null if not known.
     */
    public synchronized String getName() {
        if (inGetName) {
            return null;
        }

        try {
            inGetName = true;

            EnvParameters eParams = getEnvParameters();
            if (eParams != null) {
                // getName to get the name for the environment
                TestEnvironment e = eParams.getEnv();
                if (e != null) {
                    return e.getName();
                }
            }
            return null;
        } finally {
            inGetName = false;
        }
    }

    /**
     * Get a description for this configuration.
     * By default and for backwards compatibility, this defaults to the
     * description entry in the test environment, which means that the
     * whole environment may need to be evaluated to get the required value.
     * Subtypes may choose to override this method to provide a more efficient
     * implementation.
     *
     * @return a description for this configuration, or null if not known
     */
    public String getDescription() {
        EnvParameters eParams = getEnvParameters();
        if (eParams != null) {
            TestEnvironment e = eParams.getEnv();
            if (e != null) {
                return e.getDescription();
            }
        }
        return null;
    }

    /**
     * Get the next question to the asked after the initial prolog
     * of questions.
     * The default value is the result of getEnvFirstQuestion.
     *
     * @return the next question to be asked after the initial prolog
     * of questions.
     * @see #setFirstQuestion
     */
    protected Question getPrologSuccessorQuestion() {
        return getEnvFirstQuestion();
    }

    @Override
    public TestEnvironment getEnv() {
        EnvParameters eParams = getEnvParameters();
        if (eParams == null) {
            throw new NullPointerException();
        } else {
            return eParams.getEnv();
        }
    }

    /**
     * Get the first question to be asked concerning the environment to be
     * set up and used for each test to be run. If these questions are
     * contained in an interview, this method can be simply implemented as:<br>
     * {@code return callInterview(}<i>envInterview</i>{@code , getEnvSuccessorQuestion);}<br>
     *
     * @return the first question to be asked concerning the environment to be
     * set up and used for each test to be run.
     * @see #getEnvSuccessorQuestion
     */
    protected abstract Question getEnvFirstQuestion();

    /**
     * Get the next question to be asked after those concerning
     * the environment to be set up and used for each test to be run.
     * The default value is the result of getTestsFirstQuestion.
     *
     * @return the next question to be asked after those concerning
     * the environment to be set up and used for each test to be run.
     * @see #getEnvFirstQuestion
     */
    protected Question getEnvSuccessorQuestion() {
        return getTestsFirstQuestion();
    }

    @Override
    public String[] getTests() {
        TestsParameters iParams = getTestsParameters();
        return iParams == null ? null : iParams.getTests();
    }

    /**
     * Get the first question to be asked concerning the set of tests
     * and folders of tests to be run.
     *
     * @return the first question to be asked concerning the set of tests
     * and folders of tests to be run.
     * @see #getTestsSuccessorQuestion
     */
    protected abstract Question getTestsFirstQuestion();

    /**
     * Get the next question to be asked after those concerning
     * the tests and folders of tests to be run.
     * The default value is the result of getExcludeListFirstQuestion.
     *
     * @return the next question to be asked after those concerning
     * the tests and folders of tests to be run.
     * @see #getTestsFirstQuestion
     */
    protected Question getTestsSuccessorQuestion() {
        return getExcludeListFirstQuestion();
    }

    @Override
    public ExcludeList getExcludeList() {
        ExcludeListParameters eParams = getExcludeListParameters();
        return eParams == null ? new ExcludeList() : eParams.getExcludeList();
    }

    /**
     * Get the combined known failures list.
     * Interviews expecting to use known failures lists should generally override
     * this method and add support for users to change it.
     *
     * @return Current known failures list - combined from the one or more
     * file specified by the user.
     * @see #setKnownFailureFiles(java.io.File[])
     * @see com.sun.javatest.interview.BasicInterviewParameters
     * @since 4.4
     */
    public KnownFailuresList getKnownFailuresList() {
        try {
            if (kflFiles != null) {
                return new KnownFailuresList(getKnownFailureFiles());
            } else {
                return null;
            }

        } catch (IOException | KnownFailuresList.Fault e) {
            return null;
        }
    }

    /**
     * Get the current set of known failures list files.
     * The default implementation will return the value in the kflFiles
     * field, which subclasses may set.
     *
     * @return The list of known failure list files.  Null if none.
     * @see #setKnownFailureFiles(java.io.File[])
     * @since 4.4
     */
    public File[] getKnownFailureFiles() {
        return kflFiles;
    }

    /**
     * Set the set of KFL files.
     *
     * @param files The known failures list files.  The array should contain
     *              one or more elements.
     * @since 4.4
     */
    public void setKnownFailureFiles(File... files) {
        kflFiles = files;
    }

    /**
     * Get the first question to be asked concerning the exclude list
     * to be used to exclude tests from the test run.
     *
     * @return the first question to be asked concerning the exclude list
     * to be used to exclude tests from the test run.
     * @see #getExcludeListSuccessorQuestion
     */
    protected abstract Question getExcludeListFirstQuestion();

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

    /**
     * Get the first question to be asked concerning the exclude list
     * to be used to exclude tests from the test run.
     *
     * @return the first question to be asked concerning the exclude list
     * to be used to exclude tests from the test run
     * @see #getExcludeListFirstQuestion
     * @deprecated Use getExcludeListFirstQuestion().
     */
    @java.lang.Deprecated
    protected Question getExcludeTableFirstQuestion() {
        return getExcludeListFirstQuestion();
    }

    /**
     * Get the next question to be asked after those concerning
     * the exclude list to be used to exclude tests from the test run.
     * The default value is the result of getKeywordsFirstQuestion,
     *
     * @return the next question to be asked after those concerning
     * the exclude list to be used to exclude tests from the test run.
     * @see #getExcludeListFirstQuestion
     */
    protected Question getExcludeListSuccessorQuestion() {
        return getKeywordsFirstQuestion();
    }

    /**
     * Get the next question to be asked after those concerning
     * the exclude list to be used to exclude tests from the test run.
     *
     * @return the next question to be asked after those concerning
     * the exclude list to be used to exclude tests from the test run
     * @see #getExcludeListSuccessorQuestion
     * @deprecated Use getExcludeListFirstQuestion().
     */
    @java.lang.Deprecated
    protected Question getExcludeTableSuccessorQuestion() {
        return getExcludeListSuccessorQuestion();
    }

    @Override
    public Keywords getKeywords() {
        KeywordsParameters kParams = getKeywordsParameters();
        return kParams == null ? null : kParams.getKeywords();
    }

    /**
     * Get the first question to be asked concerning the keywords
     * that may be used to select tests for the test run.
     *
     * @return the first question to be asked concerning the keywords
     * that may be used to select tests for the test run.
     * @see #getKeywordsSuccessorQuestion
     */
    protected abstract Question getKeywordsFirstQuestion();

    /**
     * Get the next question to be asked after those concerning
     * the keywords that may be used to select tests for the test run.
     * The default value is the result of getPriorStatusQuestion.
     *
     * @return the next question to be asked after those concerning
     * the keywords that may be used to select tests for the test run.
     * @see #getKeywordsFirstQuestion
     */
    protected Question getKeywordsSuccessorQuestion() {
        return getPriorStatusFirstQuestion();
    }

    @Override
    public boolean[] getPriorStatusValues() {
        PriorStatusParameters sParams = getPriorStatusParameters();
        return sParams == null ? null : sParams.getPriorStatusValues();
    }

    /**
     * Get the first question to be asked concerning whether tests should
     * be selected for execution according to their prior execution status.
     *
     * @return the first question to be asked concerning whether tests should
     * be selected for execution according to their prior execution status.
     * @see #getPriorStatusSuccessorQuestion
     */
    protected abstract Question getPriorStatusFirstQuestion();

    /**
     * Get the next question to be asked after those concerning
     * whether tests should be selected for execution according to their
     * prior execution status.
     * The default value is the result of getConcurrencyFirstQuestion
     *
     * @return the next question to be asked after those concerning
     * whether tests should be selected for execution according to their
     * prior execution status.
     * @see #getPriorStatusFirstQuestion
     */
    protected Question getPriorStatusSuccessorQuestion() {
        return getConcurrencyFirstQuestion();
    }

    @Override
    public int getConcurrency() {
        ConcurrencyParameters cParams = getConcurrencyParameters();
        return cParams == null ? 1 : cParams.getConcurrency();
    }

    /**
     * Get the first question concerning the number of tests that
     * may be run in parallel.
     *
     * @return the first question concerning the number of tests that
     * may be run in parallel.
     * @see #getConcurrencySuccessorQuestion
     */
    protected abstract Question getConcurrencyFirstQuestion();

    /**
     * Get the next question after those concerning the number
     * of tests that may be run in parallel.
     * The default is the result of getTimeoutFactorFirstQuestion
     *
     * @return the next question after those concerning the number
     * of tests that may be run in parallel.
     * @see #getConcurrencyFirstQuestion
     */
    protected Question getConcurrencySuccessorQuestion() {
        return getTimeoutFactorFirstQuestion();
    }

    @Override
    public float getTimeoutFactor() {
        TimeoutFactorParameters tParams = getTimeoutFactorParameters();
        return tParams == null ? 1 : tParams.getTimeoutFactor();
    }

    /**
     * Get the first question concerning the scale factor to
     * be applied to the standard timeout for each test.
     *
     * @return the first question concerning the scale factor to
     * be applied to the standard timeout for each test.
     * @see #getTimeoutFactorSuccessorQuestion
     */
    protected abstract Question getTimeoutFactorFirstQuestion();

    /**
     * Get the next question after those concerning the scale factor to
     * be applied to the standard timeout for each test.
     * The default is the result of getEpilogFirstQuestion
     *
     * @return the next question after those concerning the scale factor to
     * be applied to the standard timeout for each test.
     * @see #getTimeoutFactorFirstQuestion
     */
    protected Question getTimeoutFactorSuccessorQuestion() {
        return getEpilogFirstQuestion();
    }

    /**
     * Get the first question of the epilog, which should be asked after
     * all the other questions in the configuration interview have been asked.
     * The epilog should terminate in the standard way with a FinalQuestion.
     *
     * @return the first question of the epilog, which should be asked after
     * all the other questions in the configuration interview have been asked.
     */
    protected abstract Question getEpilogFirstQuestion();

    /**
     * Determine whether all the configuration values are valid, by
     * checking if the interview has been completed.
     * If so, the result will be true; if not, the result will be false,
     * and getErrorMessage will provide details about at least one of the
     * invalid values.
     *
     * @return true if and only if all the configuration values are valid
     * @see #getErrorMessage
     * @see #isFinishable
     */
    @Override
    public boolean isValid() {
        return isFinishable();
    }

    /**
     * If there is an error in any of the configuration values,
     * as indicated by isValid, this method will provide a detail
     * message about the first question for which there is a problem.
     *
     * @return a detail message about the first question with an invalid answer,
     * or null if none.
     * @see #isValid
     */
    @Override
    public String getErrorMessage() {
        Question[] path = getPath();
        Question lastQuestion = path[path.length - 1];
        if (lastQuestion instanceof FinalQuestion) {
            return null;
        } else if (lastQuestion instanceof ErrorQuestion) {
            return lastQuestion.getText();
        } else {
            String v = lastQuestion.getStringValue();
            return i18n.getString("ip.noAnswer",
                    lastQuestion.getSummary(), lastQuestion.getText(), lastQuestion.getTag(), Integer.valueOf(v == null ? 0 : 1), trim(v));
        }
    }

    private String trim(String text) {
        return text == null ? null
                : text.length() < 40 ? text
                : text.substring(0, 37) + "...";
    }

    /**
     * Get a filter which will filter tests according to the result
     * of getExcludeList(). If the result of getExcludeList is null
     * or an empty exclude list, the result of this method will also be null.
     *
     * @return a filter which will filter tests according to the result
     * of getExcludeList()
     * @see #getExcludeListFilter
     * @deprecated Use getExcludeListFilter().
     */
    @java.lang.Deprecated
    public TestFilter getExcludeTableFilter() {
        return getExcludeListFilter();
    }

    @Override
    public TestFilter getExcludeListFilter() {
        ExcludeList t = getExcludeList();
        if (t == null) {
            cachedExcludeListFilter = null;
        } else if (cachedExcludeListFilter == null
                || cachedExcludeListFilter.getExcludeList() != t) {
            cachedExcludeListFilter = new ExcludeListFilter(t);
        }
        return cachedExcludeListFilter;
    }

    @Override
    public TestFilter getKeywordsFilter() {
        Keywords k = getKeywords();
        if (k == null) {
            cachedKeywordsFilter = null;
        } else if (cachedKeywordsFilter == null
                || cachedKeywordsFilter.getKeywords() != k) {
            cachedKeywordsFilter = new KeywordsFilter(k);
        }
        return cachedKeywordsFilter;
    }

    @Override
    public TestFilter getPriorStatusFilter() {
        WorkDirectory wd = getWorkDirectory();
        TestResultTable r = wd == null ? null : wd.getTestResultTable();
        boolean[] s = getPriorStatusValues();
        if (r == null || s == null) {
            cachedStatusFilter = null;
        } else if (cachedStatusFilter == null
                || cachedStatusFilter.getTestResultTable() != r
                || !equal(cachedStatusFilter.getStatusValues(), s)) {
            cachedStatusFilter = new StatusFilter(s, r);
        }
        // else
        //   cachedStatusFilter is OK

        return cachedStatusFilter;
    }

    @Override
    public TestFilter getRelevantTestFilter() {
        TestSuite ts = getTestSuite();
        TestEnvironment env = getEnv();
        if (ts == null || env == null) {
            cachedRelevantTestFilter = null;
        } else if (cachedRelevantTestFilter == null ||
                ts != cachedRelevantTestFilterTestSuite ||
                env != cachedRelevantTestFilterEnv) {
            cachedRelevantTestFilter = ts.createTestFilter(env);
        }
        return cachedRelevantTestFilter;
    }

    @Override
    public synchronized TestFilter[] getFilters() {
        Vector<TestFilter> v = new Vector<>();

        TestFilter excludeFilter = getExcludeListFilter();
        if (excludeFilter != null) {
            v.add(excludeFilter);
        }

        TestFilter keywordFilter = getKeywordsFilter();
        if (keywordFilter != null) {
            v.add(keywordFilter);
        }

        TestFilter statusFilter = getPriorStatusFilter();
        if (statusFilter != null) {
            v.add(statusFilter);
        }

        TestFilter testSuiteFilter = null;
        try {
            testSuiteFilter = getRelevantTestFilter();
        } catch (Exception e) {
            testSuiteFilter = null;
        }
        if (testSuiteFilter != null) {
            v.add(testSuiteFilter);
        }

        if (v.isEmpty()) {
            return null;
        } else if (equal(v, cachedTestFilters)) {
            return cachedTestFilters;
        } else {
            return v.toArray(new TestFilter[v.size()]);
        }

    }

    /**
     * Determine if the current instance is a template or not.
     *
     * @return true if the current instance is a template,
     * and false otherwise
     */
    public boolean isTemplate() {
        return isTemplate;
    }

    /**
     * Set if the current instance is a template or not.
     * <b>For internal use only, architects should not use this.</b>
     */
    public void setTemplate(boolean tm) {
        isTemplate = tm;
        updatePath();
    }

    protected boolean isAutoUpdatableKey(String key) {
        return isAutoUpdatableKey(key, null);
    }

    protected boolean isAutoUpdatableKey(String key, String subkey) {
        return false;
    }

    protected boolean isUpdatableKey(String key) {
        return true;
    }

    /**
     * Return String path to the template file for the current instance.
     * If the current instance is a template (isTemplate() returns true),
     * the path to itself will be returned.
     *
     * @return String path to the template file, or null if the instance is
     * not template-based
     */
    public String getTemplatePath() {
        if (isTemplate()) {
            File f = getFile();
            if (f != null) {
                return f.getPath();
            }
        }
        return templatePath;
    }

    /**
     * Set the location of a configuration's master template.
     * Do not change this value if this instance is a template.
     */
    public void setTemplatePath(String tu) {
        templatePath = tu;
    }

    /**
     * Get the file associated with this interview.
     *
     * @return the file associated with this interview.
     * @see #setFile
     * @see #load
     * @see #save
     */
    public File getFile() {
        return currFile;
    }

    /**
     * Set the file associated with this interview. This file will be used
     * by subsequent load and save operations.
     *
     * @param f The file to be associated with this interview.
     * @see #getFile
     * @see #load
     * @see #save
     */
    public void setFile(File f) {
        currFile = f;
        currFileLoaded = false;
        if (f != null) {
            currFileLastModified = OptionalLong.of(f.lastModified());
        } else {
            // means: unknown; will likely a trigger a reload
            currFileLastModified = OptionalLong.empty();
        }
    }

    @Override
    public void clear() {
        WorkDirectory wd = getWorkDirectory();
        super.clear();

        if (wd != null && TemplateUtilities.getTemplatePath(wd) != null) {
            if (wd.getTestSuite() != null) {
                try {
                    wd.getTestSuite().loadInterviewFromTemplate(
                            TemplateUtilities.getTemplateFile(wd), this);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
        }
        setEdited(false);
        currFile = null;
    }

    /**
     * Load the interview with the contents of the file associated with
     * the interview. If the file does not exist, the interview will be
     * cleared.
     *
     * @return true if there was an update from template
     * @throws IOException     is there is a problem reading the file
     * @throws Interview.Fault if there is a problem loading the
     *                         interview
     */
    public boolean load() throws IOException, Fault {
        File f = getFile();
        if (f != null && f.exists()) {
            return load(f);
        } else {
            clear();
            setEdited(false);
            return false;
        }
    }

    /**
     * Load the interview with the contents of a specified file,
     * which will become the default file associated with the interview.
     *
     * @param file the file to be loaded
     * @return true if there was an update from template
     * @throws FileNotFoundException if the specified file does not exist.
     * @throws IOException           is there is a problem reading the file
     * @throws Interview.Fault       if there is a problem loading the
     *                               interview
     */
    public boolean load(File file) throws FileNotFoundException, IOException, Fault {
        try (InputStream in = new BufferedInputStream(new FileInputStream(file))) {
            Map<String, String> data = PropertyUtils.load(in);
            return load(data, file);
        }
    }

    /**
     * Load the interview with data that has already been read from a specified file,
     * which will become the default file associated with the interview.
     *
     * @param data the data to be loaded
     * @param file the file from which the data was read
     * @return true if there was an update from template
     * @throws Interview.Fault if there is a problem loading the interview
     */
    public boolean load(Map<String, String> data, File file) throws Fault {
        load(data);

        // restore template state
        String tm = data.get(IS_TEMPLATE);
        setTemplate(tm != null && tm.equalsIgnoreCase(TRUE));

        setEdited(false);
        currFile = file;
        currFileLastModified = OptionalLong.of(file.lastModified());
        currFileLoaded = true;
        return checkForUpdates();
    }

    /**
     * Returns true if there was update
     */
    public boolean checkForUpdates() {
        InterviewPropagator prop = new InterviewPropagator(this,
                ignorableProps, ignorablePrefs);
        return prop.checkForUpdate();
    }

    @Override
    public void load(Map<String, String> data, boolean checkChecksum) throws Fault {
        super.load(data, checkChecksum);

        String me = data.get(MARKERS_ENABLED);
        setMarkersEnabled(me != null && me.equalsIgnoreCase(TRUE));

        String mf = data.get(MARKERS_FILTER);
        setMarkersFilterEnabled(mf != null && mf.equalsIgnoreCase(TRUE));

        String tm = data.get(IS_TEMPLATE);
        setTemplate(tm != null && tm.equalsIgnoreCase(TRUE));

        String tu = null;
        //if (isTemplate()) {
        tu = data.get(TEMPLATE_PATH);
        //} else {
        //tu = (String) data.get(TEMPLATE_PREF + TEMPLATE_PATH);
        //}
        setTemplatePath(tu);
    }

    /**
     * Load the interview as best as possible with the data in another
     * Parameters object. If any of the various sub-objects as returned by
     * get<i>XXX</i>Parameters are not recognized, they will be ignored.
     *
     * @param other The Parameters object to be copied.
     */
    public void load(Parameters other) {
        loadTestsParameters(other.getTestsParameters());
        loadExcludeListParameters(other.getExcludeListParameters());
        loadKeywordsParameters(other.getKeywordsParameters());
        loadPriorStatusParameters(other.getPriorStatusParameters());
        loadEnvParameters(other.getEnvParameters());
        loadConcurrencyParameters(other.getConcurrencyParameters());
        loadTimeoutFactorParameters(other.getTimeoutFactorParameters());
    }

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

    private void loadTestsParameters(TestsParameters other) {
        TestsParameters tp = getTestsParameters();
        if (!(tp instanceof MutableTestsParameters)) {
            return;
        }

        MutableTestsParameters mtp = (MutableTestsParameters) tp;

        if (other instanceof MutableTestsParameters) {
            MutableTestsParameters mop = (MutableTestsParameters) other;
            mtp.setTestsMode(mop.getTestsMode());
            mtp.setSpecifiedTests(mop.getSpecifiedTests());
        } else {
            String[] tests = other.getTests();
            if (tests == null) {
                mtp.setTestsMode(MutableTestsParameters.ALL_TESTS);
                mtp.setSpecifiedTests((String[]) null);
            } else {
                mtp.setTestsMode(MutableTestsParameters.SPECIFIED_TESTS);
                mtp.setSpecifiedTests(tests);
            }
        }
    }

    private void loadExcludeListParameters(ExcludeListParameters other) {
        ExcludeListParameters tp = getExcludeListParameters();
        if (!(tp instanceof MutableExcludeListParameters)) {
            return;
        }

        MutableExcludeListParameters mtp = (MutableExcludeListParameters) tp;

        if (other instanceof MutableExcludeListParameters) {
            MutableExcludeListParameters mop = (MutableExcludeListParameters) other;
            mtp.setExcludeMode(mop.getExcludeMode());
            mtp.setCustomExcludeFiles(mop.getCustomExcludeFiles());
            mtp.setLatestExcludeAutoCheckEnabled(mop.isLatestExcludeAutoCheckEnabled());
            mtp.setLatestExcludeAutoCheckMode(mop.getLatestExcludeAutoCheckMode());
            mtp.setLatestExcludeAutoCheckInterval(mop.getLatestExcludeAutoCheckInterval());
        } else {
            mtp.setExcludeMode(MutableExcludeListParameters.CUSTOM_EXCLUDE_LIST);
            mtp.setCustomExcludeFiles((File[]) null);
            mtp.setLatestExcludeAutoCheckEnabled(false);
            mtp.setLatestExcludeAutoCheckMode(MutableExcludeListParameters.CHECK_EVERY_X_DAYS);
            mtp.setLatestExcludeAutoCheckInterval(0);
        }
    }

    private void loadKeywordsParameters(KeywordsParameters other) {
        KeywordsParameters tp = getKeywordsParameters();
        if (!(tp instanceof MutableKeywordsParameters)) {
            return;
        }

        MutableKeywordsParameters mtp = (MutableKeywordsParameters) tp;

        if (other instanceof MutableKeywordsParameters) {
            MutableKeywordsParameters mop = (MutableKeywordsParameters) other;
            mtp.setKeywordsMode(mop.getKeywordsMode());
            mtp.setMatchKeywords(mop.getMatchKeywordsMode(), mop.getMatchKeywordsValue());
        } else {
            Keywords k = other.getKeywords();
            if (k == null) {
                mtp.setKeywordsMode(MutableKeywordsParameters.NO_KEYWORDS);
                mtp.setMatchKeywords(MutableKeywordsParameters.EXPR, "");
            } else {
                mtp.setKeywordsMode(MutableKeywordsParameters.MATCH_KEYWORDS);
                mtp.setMatchKeywords(MutableKeywordsParameters.EXPR, k.toString());
            }

        }
    }

    private void loadPriorStatusParameters(PriorStatusParameters other) {
        PriorStatusParameters tp = getPriorStatusParameters();
        if (!(tp instanceof MutablePriorStatusParameters)) {
            return;
        }

        MutablePriorStatusParameters mtp = (MutablePriorStatusParameters) tp;

        if (other instanceof MutablePriorStatusParameters) {
            MutablePriorStatusParameters mop = (MutablePriorStatusParameters) other;
            mtp.setPriorStatusMode(mop.getPriorStatusMode());
            mtp.setMatchPriorStatusValues(mop.getMatchPriorStatusValues());
        } else {
            boolean[] b = other.getPriorStatusValues();
            if (b == null) {
                mtp.setPriorStatusMode(MutablePriorStatusParameters.NO_PRIOR_STATUS);
                mtp.setMatchPriorStatusValues(new boolean[Status.NUM_STATES]);
            } else {
                mtp.setPriorStatusMode(MutablePriorStatusParameters.MATCH_PRIOR_STATUS);
                mtp.setMatchPriorStatusValues(b);
            }
        }
    }

    private void loadEnvParameters(EnvParameters other) {
        EnvParameters tp = getEnvParameters();
        if (!(tp instanceof LegacyEnvParameters)) {
            return;
        }

        LegacyEnvParameters ltp = (LegacyEnvParameters) tp;

        if (other instanceof LegacyEnvParameters) {
            LegacyEnvParameters lop = (LegacyEnvParameters) other;
            ltp.setEnvFiles(lop.getEnvFiles());
            ltp.setEnvName(lop.getEnvName());
        }
    }

    private void loadConcurrencyParameters(ConcurrencyParameters other) {
        ConcurrencyParameters tp = getConcurrencyParameters();
        if (!(tp instanceof MutableConcurrencyParameters)) {
            return;
        }

        MutableConcurrencyParameters mtp = (MutableConcurrencyParameters) tp;
        mtp.setConcurrency(other.getConcurrency());
    }

    private void loadTimeoutFactorParameters(TimeoutFactorParameters other) {
        TimeoutFactorParameters tp = getTimeoutFactorParameters();
        if (!(tp instanceof MutableTimeoutFactorParameters)) {
            return;
        }

        MutableTimeoutFactorParameters mtp = (MutableTimeoutFactorParameters) tp;
        mtp.setTimeoutFactor(other.getTimeoutFactor());
    }

    /**
     * Save the current set of answers for the interview in the standard
     * file associated with the interview.
     *
     * @throws IOException     is there is a problem writing the file
     * @throws Interview.Fault if there is a problem preparing the
     *                         interview to be written
     * @see #getFile
     */
    public void save() throws IOException, Fault {
        File f = getFile();
        if (f == null) {
            throw new IllegalStateException();
        }
        save(f);
    }

    /**
     * Save the current state of the interview in a specified file,
     * and make that file the new file associated with the interview.
     *
     * @param file the file in which to save the state of the interview
     * @throws IOException     is there is a problem writing the file
     * @throws Interview.Fault if there is a problem preparing the
     *                         interview to be written
     * @see #getFile
     */
    public void save(File file) throws IOException, Fault {
        save(file, false);
    }

    /**
     * Save the current state of the interview in a specified file,
     * and make that file the new file associated with the interview.
     *
     * @param file       the file in which to save the state of the interview
     * @param isTemplate
     * @throws IOException     is there is a problem writing the file
     * @throws Interview.Fault if there is a problem preparing the
     *                         interview to be written
     * @see #getFile
     */
    public void save(File file, boolean isTemplate) throws IOException, Fault {
        saveAs(file, true, true, isTemplate);

        setEdited(false);
        currFile = file;
        currFileLastModified = OptionalLong.of(file.lastModified());
        currFileLoaded = true;
    }

    /**
     * Save the current state of the interview in a specified file,
     * including the paths for the test suite and work directory.
     *
     * @param file the file in which to save the state of the interview
     * @throws IOException     is there is a problem writing the file
     * @throws Interview.Fault if there is a problem preparing the
     *                         interview to be written
     */
    public void saveAs(File file)
            throws IOException, Fault {
        saveAs(file, true, true);
    }

    /**
     * Save the current state of the interview in a specified file.
     * If the test suite path is not saved, the file can only be used
     * as a configuration template.
     *
     * @param file          the file in which to save the state of the interview
     * @param saveTestSuite if true, the test suite path will be saved
     *                      in the file.
     * @param saveWorkDir   if true, the work directory path will be saved
     *                      in the file.
     * @param isTemplate    True, the interview will be saved as template.
     * @throws IOException     is there is a problem writing the file
     * @throws Interview.Fault if there is a problem preparing the
     *                         interview to be written
     */
    public void saveAs(File file, boolean saveTestSuite, boolean saveWorkDir, boolean isTemplate)
            throws IOException, Fault {
        SortedMap<String, String> data = new TreeMap<>();
        setTemplate(isTemplate);        // dubious, why do we need to do this?

        if (saveTestSuite) {
            TestSuite ts = getTestSuite();
            if (ts != null) {
                data.put(TESTSUITE, ts.getPath());
            }
        }

        if (saveWorkDir) {
            WorkDirectory wd = getWorkDirectory();
            if (wd != null) {
                data.put(WORKDIR, wd.getPath());
            }
        }

        save(data);

        if (this.isTemplate == true) {
            TemplateManager tm = this.templateManager;
            if (tm != null && !tm.canSaveTemplate(file)) {
                throw new Interview.Fault(i18n, "ip.badTmplPath");
            }
        }

        OutputStream out;
        if (backupPolicy == null) {
            out = new BufferedOutputStream(new FileOutputStream(file));
        } else {
            out = backupPolicy.backupAndOpenStream(file);
        }

        try {
            PropertyUtils.store(data, out, "JT Harness Configuration Interview");
        } finally {
            out.close();
        }
    }

    /**
     * Save the current state of the interview in a specified file.
     * If the test suite path is not saved, the file can only be used
     * as a configuration template.
     *
     * @param file          the file in which to save the state of the interview
     * @param saveTestSuite if true, the test suite path will be saved
     *                      in the file.
     * @param saveWorkDir   if true, the work directory path will be saved
     *                      in the file.
     * @throws IOException     is there is a problem writing the file
     * @throws Interview.Fault if there is a problem preparing the
     *                         interview to be written
     */
    public void saveAs(File file, boolean saveTestSuite, boolean saveWorkDir)
            throws IOException, Fault {
        saveAs(file, saveTestSuite, saveWorkDir, false);
    }

    @Override
    public void save(Map<String, String> data) {
        if (markersEnabled) {
            data.put(MARKERS_ENABLED, TRUE);
        }

        if (markersFilterEnabled) {
            data.put(MARKERS_FILTER, TRUE);
        }

        if (isTemplate()) {
            data.put(IS_TEMPLATE, TRUE);

            storeTemplateProperties(new HashMap<>());
        } else {
            WorkDirectory wd = getWorkDirectory();
            if (wd != null && TemplateUtilities.getTemplatePath(wd) != null) {
                data.put(TEMPLATE_PATH, TemplateUtilities.getTemplatePath(wd));
            }
        }

        String name = getName();
        if (name != null) {
            data.put(NAME, name);
        }

        String desc = getDescription();
        if (desc != null) {
            data.put(DESC, desc);
        }


        super.save(data);
    }

    /**
     * Get the backup policy to be used when saving configuration files.
     *
     * @return the backup policy object to be used when saving configuration files
     * @see #setBackupPolicy
     */
    public BackupPolicy getBackupPolicy() {
        return backupPolicy;
    }

    /**
     * Set the backup policy to be used when saving configuration files.
     *
     * @param backupPolicy the backup policy object to be used when saving configuration files
     * @see #getBackupPolicy
     */
    public void setBackupPolicy(BackupPolicy backupPolicy) {
        this.backupPolicy = backupPolicy;
    }

    /**
     * Check if the current file has been loaded into this interview,
     * or if the interview has been saved in it.
     *
     * @return true if the file associated with the interview was set as a
     * side effect of load or save, or false if the file was just set by
     * setFile.
     */
    public boolean isFileLoaded() {
        return currFileLoaded;
    }

    /**
     * Determine if the file associated with this interview has been modified
     * on disk after the last call of load or save.
     *
     * @return true if the file on disk has been modified after it was last
     * used by load or save.
     * @see #load()
     * @see #save()
     */
    public boolean isFileNewer() {
        File f = getFile();
        return f != null && f.exists() && (!currFileLastModified.isPresent()
                || (f.lastModified() > currFileLastModified.getAsLong()));
    }

    /**
     * Check whether or not markers should be enabled.
     *
     * @return whether or not markers should be enabled
     * @see #setMarkersEnabled
     */
    public boolean getMarkersEnabled() {
        return markersEnabled;
    }

    /**
     * Specify whether or not markers should be enabled.
     *
     * @param on whether or not markers should be enabled
     * @see #getMarkersEnabled
     */
    public void setMarkersEnabled(boolean on) {
        if (on != markersEnabled) {
            markersEnabled = on;
            setEdited(true);
        }
    }

    /**
     * Check whether or not the history list should be filtered to
     * just show questions which have been marked.
     *
     * @return whether or not the  history list should be filtered to
     * just show questions which have been marked
     * @see #setMarkersFilterEnabled
     */
    public boolean getMarkersFilterEnabled() {
        return markersFilterEnabled;
    }

    /**
     * Specify whether or not the history list should be filtered to
     * just show questions which have been marked.
     *
     * @param on whether or not the  history list should be filtered to
     *           just show questions which have been marked
     * @see #getMarkersFilterEnabled
     */
    public void setMarkersFilterEnabled(boolean on) {
        if (on != markersFilterEnabled) {
            markersFilterEnabled = on;
            setEdited(true);
        }
    }
    /**
     * The template manager is used to change behavior of
     * template saving, the default implementation is
     * the context manager of corresponding test suite.
     */
    public interface TemplateManager {
        /**
         * This method is invoked each time before saving template.
         * The template will be saved only if this method returns true.
         *
         * @param file template file
         * @return true if this operation is allowed, false otherwise
         */
        boolean canSaveTemplate(File file);
    }

    /**
     * Indicates problems when accessing the work directory.
     */
    public static class WorkDirFault extends Interview.Fault {
        /**
         * Create a fault with an internationalized message.
         *
         * @param i18n The bundle from which to get the string.
         * @param s    The key for getting the string to be displayed from the
         *             supplied bundle.
         */
        public WorkDirFault(ResourceBundle i18n, String s) {
            super(i18n, s);
        }

        /**
         * Create a fault with an internationalized message.
         *
         * @param i18n The bundle from which to get the string.
         * @param s    The key for getting the string to be displayed from the
         *             supplied bundle.
         * @param o    Parameter to use when resolving the string from the bundle.
         * @see java.text.MessageFormat
         */
        public WorkDirFault(ResourceBundle i18n, String s, Object o) {
            super(i18n, s, o);
        }

        /**
         * Create a fault with an internationalized message.
         *
         * @param i18n The bundle from which to get the string.
         * @param s    The key for getting the string to be displayed from the
         *             supplied bundle.
         * @param o    Parameters to use when resolving the string from the bundle.
         * @see java.text.MessageFormat
         */
        public WorkDirFault(ResourceBundle i18n, String s, Object... o) {
            super(i18n, s, o);
        }
    }

    /**
     * Indicates problems when accessing the test suite.
     */
    public static class TestSuiteFault extends Interview.Fault {
        /**
         * Create a fault with an internationalized message.
         *
         * @param i18n The bundle from which to get the string.
         * @param s    The key for getting the string to be displayed from the
         *             supplied bundle.
         */
        public TestSuiteFault(ResourceBundle i18n, String s) {
            super(i18n, s);
        }

        /**
         * Create a fault with an internationalized message.
         *
         * @param i18n The bundle from which to get the string.
         * @param s    The key for getting the string to be displayed from the
         *             supplied bundle.
         * @param o    Parameter to use when resolving the string from the bundle.
         * @see java.text.MessageFormat
         */
        public TestSuiteFault(ResourceBundle i18n, String s, Object o) {
            super(i18n, s, o);
        }

        /**
         * Create a fault with an internationalized message.
         *
         * @param i18n The bundle from which to get the string.
         * @param s    The key for getting the string to be displayed from the
         *             supplied bundle.
         * @param o    Parameters to use when resolving the string from the bundle.
         * @see java.text.MessageFormat
         */
        public TestSuiteFault(ResourceBundle i18n, String s, Object... o) {
            super(i18n, s, o);
        }
    }

    /**
     * Indicates problems when accessing the configuration file.
     */
    public static class JTIFault extends Interview.Fault {
        /**
         * Create a fault with an internationalized message.
         *
         * @param i18n The bundle from which to get the string.
         * @param s    The key for getting the string to be displayed from the
         *             supplied bundle.
         */
        public JTIFault(ResourceBundle i18n, String s) {
            super(i18n, s);
        }

        /**
         * Create a fault with an internationalized message.
         *
         * @param i18n The bundle from which to get the string.
         * @param s    The key for getting the string to be displayed from the
         *             supplied bundle.
         * @param o    Parameter to use when resolving the string from the bundle.
         * @see java.text.MessageFormat
         */
        public JTIFault(ResourceBundle i18n, String s, Object o) {
            super(i18n, s, o);
        }

        /**
         * Create a fault with an internationalized message.
         *
         * @param i18n The bundle from which to get the string.
         * @param s    The key for getting the string to be displayed from the
         *             supplied bundle.
         * @param o    Parameters to use when resolving the string from the bundle.
         * @see java.text.MessageFormat
         */
        public JTIFault(ResourceBundle i18n, String s, Object... o) {
            super(i18n, s, o);
        }
    }
}
