Package org.jsurveylib

Source Code of org.jsurveylib.Survey

/**
* This file is part of JSurveyLib.
*
* JSurveyLib is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* JSurveyLib 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with JSurveyLib.  If not, see <http://www.gnu.org/licenses/>.
**/

/*
*  Modulname:  SurveyPane
*  Autor:    Eyer Leander
*  Datum:    08.05.2006
*
*  (c) Copyright 2005
*/
package org.jsurveylib;

import org.jsurveylib.io.SurveyReader;
import org.jsurveylib.io.XMLResultWriter;
import org.jsurveylib.io.XMLSurveyReader;
import org.jsurveylib.model.Label;
import org.jsurveylib.model.Page;
import org.jsurveylib.model.SurveyElement;
import org.jsurveylib.model.Visitable;
import org.jsurveylib.model.Visitor;
import org.jsurveylib.model.Menu;
import org.jsurveylib.model.i18n.Strings;
import org.jsurveylib.model.listeners.LinkListener;
import org.jsurveylib.model.listeners.PageListener;
import org.jsurveylib.model.listeners.SurveyResetListener;
import org.jsurveylib.model.listeners.AnswerListener;
import org.jsurveylib.model.question.InsertQuestionListener;
import org.jsurveylib.model.question.Question;
import org.jsurveylib.model.question.Template;
import org.jsurveylib.model.script.interpreter.ScriptInterpreter;
import org.jsurveylib.utils.XMLUtil;
import org.xml.sax.SAXException;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
* The Survey object stores the data and the state of the survey.
* <pre><code>
* try {
*     ClientSurvey survey = new Survey("surveyGettingStarted.xml");
*     SurveyPanel surveyPanel = new SurveyPanel(survey);
*     survey.addSurveyListener(this);
*     frame.setLayout(new BorderLayout());
*     frame.add(surveyPanel, BorderLayout.CENTER);
*     frame.setJMenuBar(new SurveyMenu(survey, this));
* } catch (Exception e) {
*     e.printStackTrace();
* }
* </code></pre>
* <b>The Survey, ClientSurvey, SurveyPanel and the SurveyAdapter are the ONLY objects that the client should interact with directly.  Even then, there
* may be public methods in this class that are marked "for internal use only".  Methods marked "for
* internal use only" may be renamed, removed or replaced in future versions.  Because of this uncertain future,
* it is highly recommended you don't use these methods: Your application may not compile when you upgrade.
* Most other classes are marked "for internal use only" but even if they aren't, these are the only
* classes that you should be interacting with.</b>
*/
public final class Survey implements ClientSurvey, LinkListener, AnswerListener, Visitable {

    private List<SurveyListener> surveyListeners = new ArrayList<SurveyListener>();
    private List<PageListener> pageListeners = new ArrayList<PageListener>();
    private List<InsertQuestionListener> insertQuestionListeners = new ArrayList<InsertQuestionListener>();
    private List<SurveyResetListener> surveyResetListeners = new ArrayList<SurveyResetListener>();
    private ScriptInterpreter scriptInterpreter;

    private SurveyReader surveyReader;

    private boolean finished = false;
    private boolean saveToFileOnFinish;

    private int cPage;

    private String title;

    private Strings strings;

    private List<Page> pages;
    private Menu menu;
    private String workingFilePath;

    private Map<String, Question> idMap;
    private Map<String, Template> templateMap;

    /**
     * I'm thinking this should default to true because if you have a survey that's 100% optional, you'll still want to
     * save at the end.
     */
    private boolean dirty = true;


    public Survey(String xmlConfigFile) throws Exception {
        this(new XMLSurveyReader(new InputStreamReader(new FileInputStream(xmlConfigFile), "UTF-8")));
    }

    public Survey(Reader xmlConfigReader) throws Exception {
        this(new XMLSurveyReader(xmlConfigReader));
    }

    public Survey(SurveyReader reader) {
        this.surveyReader = reader;
        reset(); //this will initialize the survey
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> Reset the survey so it looks like it did when it was first
     * started.  The only reason this is public is so that tests can use it.  Otherwise, I'd make it private.
     */
    public void reset() {
        //if this is called to reset (as opposed to initialize) the Survey, there's a chance I've created a memory leak here
        //This MAY result in detached questions that are still listening to this survey.  TODO: look into that later
        title = surveyReader.getTitle();

        strings = surveyReader.getStrings();

        //should we remove ourself from every previous question as an AnswerListener?
        pages = surveyReader.getPages();
        for (Page p : pages) {
            for (Question q : p.getQuestions()) {
                q.addAnswerListener(this);
            }
        }

        templateMap = surveyReader.getTemplateMap();
        idMap = buildIDMap();
        listenToLinks();

        goToFirstPage();

        if (scriptInterpreter != null) {
            //if scriptInterpreter is not null, that means this method is being called to reset the survey
            //if that's the case, the old scriptInterpreter should stop listening to these events
            removeInsertQuestionListener(scriptInterpreter);
        }
        scriptInterpreter = new ScriptInterpreter(this, surveyReader.getInitScript(), surveyReader.getOnAnswerChanged());
        menu = surveyReader.getMenu();
        saveToFileOnFinish = surveyReader.saveToFileOnFinish();
        notifySurveyResetListeners();
    }

    private void notifySurveyResetListeners() {
        for (SurveyResetListener surveyResetListener : surveyResetListeners) {
            surveyResetListener.surveyReset();
        }
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> This will go to the first page, considering
     * that some pages may be skipped.
     * This is internal because the method may be replaced in the future and I see no reason
     * for the client to listen to page events at this time.
     */
    public void goToFirstPage() {
        int cPage = 0;
        try {
            while (pages.get(cPage).isSkipped()) {
                cPage++;
            }
            goToPage(cPage);
        } catch (IndexOutOfBoundsException e) {
            throw new IllegalStateException("All pages are skipped", e);
        }
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> This will go to the last page or the page
     * that has blank required/invalid questions, considering
     * that some pages may be skipped.
     * This is internal because the method may be replaced in the future and I see no reason
     * for the client to listen to page events at this time.
     */
    public void goToLastPage() {
        int currentPage = cPage;
        cPage = 0;
        try {
            while (isNextPageAvailable()) {
                cPage++;
            }
            //If we're not on the last page
            if (currentPage != cPage) {
                //goToPage will only fire a pageChanged event if cPage != lastPage.  This will guarantee this to be true
                int lastPage = cPage;
                cPage++;
                goToPage(lastPage);
            }
        } catch (IndexOutOfBoundsException e) {
            throw new IllegalStateException("No pages available", e);
        }
    }

    private void listenToLinks() {
        for (Page page : pages) {
            for (SurveyElement se : page.getSurveyElements()) {
                if (se instanceof Label) {
                    Label label = (Label) se;
                    label.addLinkListener(this);
                } else if (se instanceof Question) {
                    Question q = (Question) se;
                    q.getLabel().addLinkListener(this);
                }
            }
        }
    }

    private Map<String, Question> buildIDMap() {
        Map<String, Question> idMap = new HashMap<String, Question>();
        for (Page page : pages) {
            for (SurveyElement se : page.getSurveyElements()) {
                if (se instanceof Question) {
                    Question q = (Question) se;
                    idMap.put(q.getId(), q);
                }
            }
        }
        return idMap;
    }

    /**
     * Add a listener which will be informed when the survey is finished
     *
     * @param listener The object that will be informed of the survey finishing.
     */
    public void addSurveyListener(SurveyListener listener) {
        surveyListeners.add(listener);
    }

    /**
     * Remove a survey listener
     *
     * @param listener The survey listener to be removed.
     */
    public void removeSurveyListener(SurveyListener listener) {
        surveyListeners.remove(listener);
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> Add a listener that gets informed when a page changes.
     * This is for internal use because the PageListener interface may change in the future.  This method may be moved
     * to a different class too.
     *
     * @param listener A listener that will be notified when the current page changes to another page
     */
    public void addPageListener(PageListener listener) {
        pageListeners.add(listener);
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> Remove a page listener.
     * This is for internal use because the PageListener interface may change in the future.  This method may be moved
     * to a different class too.
     *
     * @param listener A listener that will be notified when the current page changes to another page
     */
    public void removePageListener(PageListener listener) {
        pageListeners.remove(listener);
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> Add a listener that gets informed when a new question
     * is inserted into the survey.
     * This is for internal use because the api of the Question object may change in the future.
     *
     * @param listener A listener that will be notified when the current page changes to another page
     */
    public void addInsertQuestionListener(InsertQuestionListener listener) {
        insertQuestionListeners.add(listener);
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> Remove a insert question listener.
     * This is for internal use because the api of the Question object may change in the future.
     *
     * @param listener A listener that will be notified when a question is added to a page
     */
    public void removeInsertQuestionListener(InsertQuestionListener listener) {
        insertQuestionListeners.remove(listener);
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> Add a listener that gets informed when the survey
     * gets reset.  This is for internal use because we may handle this situation in a different way later.
     *
     * @param listener A listener that will be notified when the survey resets
     */
    public void addSurveyResetListener(SurveyResetListener listener) {
        surveyResetListeners.add(listener);
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> Remove a insert question listener.
     * This is for internal use because we may handle this situation in a different way later.
     *
     * @param listener A listener that will be notified when the survey resets
     */
    public void removeSurveyResetListener(SurveyResetListener listener) {
        surveyResetListeners.remove(listener);
    }

    /**
     * This will set the survey's questions to all the answers in answerReader.  answerReader is expected to be an XML reader that has content in it
     * that can validate against the result.xsd of this version.
     * <p/>
     * The result of saveXMLAnswers can always be passed directly into this method.
     * The labels are ignored.  Calling this method will overwrite the default values of questions.
     *
     * @param answerXMLReader A Reader that reads from an XML file that is valid against this XSD:  http://jsurveylib.sourceforge.net/result-7.10.4.4.xsd
     * @throws IOException  If there is a problem reading from the file.
     * @throws SAXException If there is an error parsing the file.
     * @see Survey#saveXMLAnswers(String)
     * @see Survey#saveXMLAnswers(java.io.Writer)
     */
    public void loadXMLAnswers(Reader answerXMLReader) throws IOException, SAXException {
        reset();
        try {
            Map<String, String> answerMap = XMLUtil.getAnswerMapFromElement(XMLUtil.getXMLRoot(answerXMLReader));

            //populateTemplate can cause questions to appear that weren't originally there.  That is why we must
            //only set answers to questions that exist and keep on trying until the only answers left are for
            //questions that don't exist
            while (hasAnswersForExistingQuestions(answerMap)) {
                answerExistingQuestions(answerMap);
            }

            //populateTemplate can cause questions to appear that weren't originally there.  Therefore it may be valid for an answer
            //to exist for a question that doesn't initially exist.  That is why we check for invalid id AFTER
            for (String id : answerMap.keySet()) {
                if (getQuestionByID(id) == null) {
                    throw new IllegalArgumentException("There is an answer for an id that does not exist. id='" + id + "' answer='" + answerMap.get(id) + "'");
                }
            }
        } catch (SAXException e) {
            throw new IllegalArgumentException("The file is not a valid Result file", e);
        }
    }

    /**
     * This will set answers of the answerMap to questions that exist on the survey.  If there are answers for
     * questions that don't exist, they are ignored.  This method has the side effect that it will remove answers
     * from the answerMap after they have been set.
     *
     * @param answerMap A Map that has question IDs as keys and question answers as values.
     */
    private void answerExistingQuestions(Map<String, String> answerMap) {
        for (Iterator<String> idIterator = answerMap.keySet().iterator(); idIterator.hasNext();) {
            String id = idIterator.next();
            if (idMap.get(id) != null) {
                if (isAnswerDifferent(answerMap, id)) {
                    setAnswer(id, answerMap.get(id));
                }
                idIterator.remove();
            }
        }
    }

    private boolean isAnswerDifferent(Map<String, String> answerMap, String id) {
        return !"".equals(answerMap.get(id)) && !getAnswer(id).equals(answerMap.get(id));
    }

    private boolean hasAnswersForExistingQuestions(Map<String, String> answerMap) {
        for (String id : answerMap.keySet()) {
            if (idMap.get(id) != null) {
                return true;
            }
        }
        return false;
    }

    /**
     * This is the same as loadXMLAnswers(Reader answerXMLReader) except it takes a String instead of a reader for convenience.
     * This method will update the value of the getWorkingFilePath() to the absolute path of <code>answerXMLFile</code>.  isDirty() will
     * return false after this method is called.
     *
     * @param answerXMLFile A path to an XML file that is valid against this XSD:  http://jsurveylib.sourceforge.net/result-7.10.4.4.xsd
     * @throws IOException  If there is a problem reading from the file.
     * @throws SAXException If there is an error parsing the file.
     * @see Survey#loadXMLAnswers(java.io.Reader)
     */
    public void loadXMLAnswers(String answerXMLFile) throws IOException, SAXException {
        loadXMLAnswers(new InputStreamReader(new FileInputStream(answerXMLFile), "UTF-8"));
        workingFilePath = new File(answerXMLFile).getAbsolutePath();
        dirty = false;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> Sets the answer of a question.  To unset a question, pass in null or "".
     * The client should not be setting
     * answers programmatically.  At this point it is a complicated issue that often requires looking
     * at source code.
     * <p/>
     * For example, on some questions passing in an invalid question sets it to some default, in other
     * situations it throws a RuntimeException.  I'd like to make things more consistent before I let others use this.
     *
     * @param id     The id of the question you are answering
     * @param answer The text/id/path/etc. that answers the question.
     * @throws RuntimeException A RuntimeException may be thrown if an invalid answer is set for the question.
     */
    public void setAnswer(String id, String answer) {
        Question question = getQuestionByID(id);
        if (question != null) {
            question.setAnswer(answer);
        } else {
            System.out.println("[setAnswer] No Question With Id <" + id + ">");
        }
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> Returns true if the next page is available.
     * The next page is available when all requirements are met on the current page and there is a visitable (non-skipped) next page to go to.  This
     * is for internal use only because the definition of what makes the next page available may change in the future.
     *
     * @return true if the next page is available, otherwise false.
     */
    public boolean isNextPageAvailable() {
        return pages.get(cPage).areRequirementsMet() && isVisitableNext();
    }

    private boolean isVisitableNext() {
        for (int i = cPage + 1; i < pages.size(); ++i) {
            if (!pages.get(i).isSkipped()) {
                return true;
            }
        }
        return false;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> Returns true if the previous page is available.
     * The previous page is available when you are not at the first page and there is a visitable (non-skipped) previous page to go to.
     * This is for internal use only because the definition of what makes the previous page available may change in the future.
     *
     * @return true if the previous page is available.
     */
    public boolean isPreviousPageAvailable() {
        return cPage > 0 && isVisitablePrevious();
    }

    private boolean isVisitablePrevious() {
        for (int i = cPage - 1; i >= 0; --i) {
            if (!pages.get(i).isSkipped()) {
                return true;
            }
        }
        return false;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> This will return true if the current page
     * is the last page and all other non-skipped pages have their requirements met.  The current page is considered the last
     * page if there are no visitable (non-skipped) next pages.  This is for internal use only because the name
     * and logic of this method may change in the future.
     *
     * @return true if all pages have requirements met and you are on the last page, otherwise false.
     */
    public boolean isLastPageAndComplete() {
        //if last visitable page
        if (!isVisitableNext()) {
            for (Page page : pages) {
                if (!page.areRequirementsMet()) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> This will increment the current page then fire
     * a page changed event.  If the next page is skipped, it will visit the first page that is not skipped.
     * This is internal because the method may be replaced in the future and I see no reason
     * for the client to listen to page events at this time.
     */
    public void goToNextPage() {
        int nextPage = cPage + 1;
        while (nextPage < pages.size() && pages.get(nextPage).isSkipped()) {
            nextPage++;
        }
        goToPage(nextPage);
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> This will decrement the current page and fire
     * a page changed event. If the previous page is skipped, it will visit the first page that is not skipped.
     * This is internal because the method may be replaced in the future and I see no reason
     * for the client to listen to page events at this time.
     */
    public void goToPreviousPage() {
        int previousPage = cPage - 1;
        while (previousPage > 0 && pages.get(previousPage).isSkipped()) {
            previousPage--;
        }
        goToPage(previousPage);
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> This method changes the page to the pageNum passed
     * in.  If the pageNum is different from the current page, a currentPageChanged event is fired.  This is for internal
     * use only because the client should not change the page manually.
     *
     * @param pageNum The to make the new current page.
     * @throws IllegalStateException If the pageNum is an index to a skipped page
     */
    public void goToPage(int pageNum) {
        if (pageNum < 0 || pageNum >= pages.size()) {
            throw new IndexOutOfBoundsException(pageNum + " is not a valid page.");
        }

        if (pages.get(pageNum).isSkipped()) {
            throw new IllegalArgumentException("Tried to go to a skipped page: " + pageNum);
        }

        if (pageNum != cPage) {
            cPage = pageNum;
            firePageChanged();
        }
    }

    private void firePageChanged() {
        for (PageListener listener : pageListeners) {
            listener.currentPageChanged();
        }
    }

    /**
     * Save the answers of the survey to an XML file.  The file path is specified by the
     * parameter.  The answers will be written to outputXMLFile and will validate against the lastest
     * result.xsd.  The getWorkingFilePath() method will return the absolute path of <code>outputXMLFile</code>
     * after you call this method. isDirty() will return false after this method is called.
     *
     * @param outputXMLFile The path to the file that will have the answers written to it.
     * @throws IOException If an error occurs writing the file.
     */
    public void saveXMLAnswers(String outputXMLFile) throws IOException {
        Writer writer = null;
        try {
            writer = new OutputStreamWriter(new FileOutputStream(outputXMLFile), "UTF-8");
            saveXMLAnswers(writer);
            workingFilePath = new File(outputXMLFile).getAbsolutePath();
            dirty = false;
        } finally {
            if (writer != null) {
                writer.close();
            }
        }
    }

    /**
     * This method does the same thing as saveXMLAnswers(String) but it takes a Writer instead
     * of a path to a file.  The writer will be left open after you call this method.
     *
     * @param writer A writer that the xml will be written to.
     * @throws IOException If an error occurs writing to the writer.
     * @see Survey#saveXMLAnswers(String)
     */
    public void saveXMLAnswers(Writer writer) throws IOException {
        XMLResultWriter resultWriter = new XMLResultWriter(writer);
        resultWriter.write(this);
    }

    /**
     * Returns an answer map of the survey.  This map uses a question's ID as the key and its answer as the value.
     *
     * @return A map that uses each question's ID as a key and its answer as the value.
     */
    public Map<String, String> getAnswerMap() {
        Writer writer = new StringWriter();
        try {
            saveXMLAnswers(writer);
            return XMLUtil.getAnswerMapFromElement(XMLUtil.getXMLRoot(new StringReader(writer.toString())));
        } catch (SAXException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Get the current answer of a question by its question id.
     * This will return null if the question does not exist and "" if the question exists, but has not been answered.
     *
     * @param id The id of the question you want to get the answer of.
     * @return The answer string or null if the question does not exist.
     */
    public String getAnswer(String id) {
        if (getQuestionByID(id) == null) {
            return null;
        }
        return getQuestionByID(id).getAnswer();
    }

    /**
     * Gets the title of this survey.
     *
     * @return The title of the survey.
     */
    public String getTitle() {
        return title;
    }

    /**
     * This will return the "working file" path of the survey.  The "working file" is the file that got saved or opened
     * last.  If you save or open to readers or never saved or opened, this will return null.
     * @return The working file path
     */
    public String getWorkingFilePath() {
        return workingFilePath;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u>  This will return the current page number the survey is on.
     * This is not publicly usable because its name may change in the future.
     *
     * @return The current page number.  The first page is page number 0.
     * @see org.jsurveylib.Survey#getCurrentPageNumberExcludingSkipped()
     */
    public int getCurrentPageNumber() {
        return cPage;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u>  This will return the current page number the survey is on
     * but it will not count skipped pages.  This is useful for display purposes.  If you're on page 1 and page 0 is a skipped page,
     * this method will return 0 while getCurrentPageNumber() will still return 1.
     * This is not publicly usable because its name may change in the future.
     *
     * @return The current page number excluding skipped pages.  The first page is page number 0.
     * @see org.jsurveylib.Survey#getCurrentPageNumber()
     */
    public int getCurrentPageNumberExcludingSkipped() {
        int currentPage = cPage;
        for (int i = 0; i < cPage; ++i) {
            if (pages.get(i).isSkipped()) {
                currentPage--;
            }
        }
        return currentPage;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u>  This will return the total page number the survey has
     * but it will not count skipped pages.  This is useful for display purposes.  If you have 2 pages and one is skipped,
     * this will return 1.
     * <p/>
     * If the current page is a skipped page, the methods result will be incremented by one.
     * <p/>
     * This is not publicly usable because its name may change in the future.
     *
     * @return The total pages excluding skipped pages.  If the current page is a skipped page, the value will be incremented by one.
     */
    public int getTotalPagesExcludingSkipped() {
        int totalPages = 0;
        for (Page p : pages) {
            if (!p.isSkipped()) {
                totalPages++;
            }
        }
        if (getCurrentPage().isSkipped()) {
            totalPages++;
        }
        return totalPages;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> This will return the current Page that the survey is on.
     * This is for internal use because the interface to the Page object can not be guaranteed in the future.
     *
     * @return The current Page the survey is on.
     */
    public Page getCurrentPage() {
        return pages.get(cPage);
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> This informs every SurveyListener that the survey has
     * been finished and makes isFinished() return true and informs all survey listeners.
     * <p/>
     * This is internal because I'm not sure that a client should be telling the survey when it's finished.  This method may
     * be changed/renamed/have a different meaning in the future.
     */
    public void finish() {
        finished = true;
        for (SurveyListener listener : surveyListeners) {
            listener.surveyFinished(this);
        }
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> Returns whether the finish() method has been called yet.
     * I'm not totally sure what defines a survey to be finished yet and that's why I'm not making this public.
     * Is a survey finished only when the finish button has been pressed?  Is it finished if you quit early (the X button on a frame)?
     * If it's finished, can you still set answers?
     *
     * @return true if the finish() method has been called, otherwise false.
     */
    boolean isFinished() {
        return finished;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> Returns an unmodifiable List of the pages in this Survey.
     * It is for internal use because the Page object is not guaranteed to have the same interface in the future.
     *
     * @return The list of pages on this Survey object.
     */
    public List<Page> getPages() {
        return Collections.unmodifiableList(pages);
    }

    /**
     * Sets the title of the survey.
     *
     * @param title The title of the survey.
     */
    void setTitle(String title) {
        this.title = title;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> Get a Question object using it's id.
     * This is for internal use because the interface for a Question object can not be guaranteed in the future.
     * Use getAnswer instead if possible.  If you really need to programmatically set an answer, calling
     * set answer is safer than doing it through this, but as you'll read, that method is for internal use only, too.
     *
     * @param id The ID of the question to get.
     * @return The Question object which corresponds to this ID or null if no question exists with that ID.
     * @see Survey#getAnswer(String)
     * @see Survey#setAnswer(String,String)
     */
    public Question getQuestionByID(String id) {
        return idMap.get(id);
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> This returns
     * an unmodifiable map that has question id's as keys and questions as
     * values.  This is for internal use only because the api of the Question class
     * can not be guaranteed in the future.
     *
     * @return An unmodifiable map that has question id's as keys and questions as
     *         values.
     */
    public Map<String, Question> getIdMap() {
        return Collections.unmodifiableMap(idMap);
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> This is called
     * to insert a question on a survey that is already running.  It will fire a
     * questionInserted event.  This is for internal use because the api of
     * the Question object may change in the future.
     *
     * @param question The question to insert
     * @param pageNum  The page number to insert the question on.  The first page is page 0
     * @param row      The row to insert the question on.  All SurveyElements on or after this
     *                 row will be incremented by one row.
     * @throws IllegalArgumentException If a question with this id exists or the page number is
     *                                  out of bounds.
     */
    public void insertQuestion(Question question, int pageNum, int row) {
        if (idMap.get(question.getId()) != null) {
            throw new IllegalArgumentException("Attempted to insert a question with an id that already exists: " + question.getId());
        }
        try {
            Page p = pages.get(pageNum);
            p.insertQuestion(question, row);
            idMap.put(question.getId(), question);
            fireQuestionInserted(question, pageNum, row);
        } catch (IndexOutOfBoundsException e) {
            IllegalArgumentException i = new IllegalArgumentException("Attempted to insert a question to an invalid page number.");
            i.initCause(e);
            throw i;
        }
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> This is called
     * to insert a question on a survey that is already running.  It will fire a
     * questionInserted event.  This is for internal use because the api of
     * the Question object may change in the future.
     *
     * @param question The question to insert
     * @param page     The page to insert the question on
     * @param row      The row to insert the question on.  All SurveyElements on or after this
     *                 row will be incremented by one row.
     * @throws IllegalArgumentException If a question with this id or the page number is
     *                                  out of bounds.
     */
    public void insertQuestion(Question question, Page page, int row) {
        insertQuestion(question, pages.indexOf(page), row);
    }

    private void fireQuestionInserted(Question question, int page, int row) {
        for (InsertQuestionListener listener : insertQuestionListeners) {
            listener.questionInserted(question, page, row);
        }
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> This returns the
     * survey's script interpreter.  The script interpreter evaluates all question's
     * scripts.  This is for internal use only because the api of the ScriptInterpeter
     * may change in the future.  The Survey may have multiple script interpreters in the
     * future.
     *
     * @return The script interpreter of this survey.
     */
    public ScriptInterpreter getInterpreter() {
        return scriptInterpreter;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u>  This returns a map
     * where the keys are template names and the values are template questions.  To populate
     * the template you should use Question.populateTemplate(String newId, String newLabel, Boolean mandatory).
     * <p/>
     * This is for internal use because the api of the Question class may change in the future.
     *
     * @return A Template.  A Template is a question with a meaningless id, label and mandatory setting that does
     *         not show up on the survey.  You can populate a survey by calling Question.populateTemplate(String newId, String newLabel, Boolean mandatory)
     *         on it.
     * @see Question#populateTemplate(String,String,boolean,String)
     */
    public Map<String, Template> getTemplateMap() {
        return Collections.unmodifiableMap(templateMap);
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u> This will return the
     * page that a question resides in or null if the question does not exist on any page.
     *
     * @param questionId The question ID of a question that exists on a page
     * @return The page that contains this question ID
     */
    public Page pageOf(String questionId) {
        for (Page page : pages) {
            //if rowOf returns a number that is < 0, that means the question does not exist on that page
            if (page.rowOf(questionId) >= 0) {
                return page;
            }
        }
        return null;
    }


    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u>
     */
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u>
     */
    public void onClick(String url) {
        if (url.startsWith("page://")) {
            int pageToGoTo = Integer.valueOf(url.substring(7));
            goToPage(pageToGoTo);
        }
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u>
     * @return A container of static strings
     */
    public Strings getStrings() {
        return strings;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u>
     * @return The menu configuration for this survey
     */
    public Menu getMenu() {
        return menu;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u>
     */
    public void answerChanged(Question question, boolean evaluateScript) {
        dirty = true;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u>
     * @return true if an answer has changed since the last opening/saving, otherwise, false.  The result of this
     * method is only reliable when you load/save to a file.  The methods that load/save a Reader do not update this
     * variable.
     */
    public boolean isDirty() {
        return dirty;
    }

    /**
     * <u><b><font color="red">FOR INTERNAL USE ONLY.</font></b></u>
     * @return true if this survey requires the user to save to a file on finish.
     */
    public boolean saveToFileOnFinish() {
        return saveToFileOnFinish;
    }
}
TOP

Related Classes of org.jsurveylib.Survey

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.