/**
* 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;
}
}