JSurveyLib: Scripting Guide
Written for version 8.01.29
This guide will teach you all about the JSurveyLib scripting language. You should read the Getting Started Guide and the Configuration Guide first, if you haven't already.
There are two tags in the JSurveyLib configuration file where you can place scripts: Between the initScript tag and between the onAnswerChanged tag. Each is optional. The Init Script is defined at the top of the config file below the surveyConfig tag. The onAnswerChanged tag appears below the initScript tag. If you have defined the templates tag, it should appear below both of these. Both script tags are optional. These scripts will be described in detail below but first the scripting language itself will be explained.
All scripts use the beanshell language. Beanshell is a powerful and complex language and this documentation can not cover it all. An in-depth guide to the beanshell language can be found here.
Beanshell is basically Java 1.4 with a looser syntax. Here are some key differences that are useful to know:
int x = 5;but you can also say
x = 5;
x = 5 y = 6 z = 7
int five() { return 5; } x = five()x will equal 5 after this script is run.
The very last line of your script will become the script's return value:
x = 5; xwill return a value of 5.
"foo".contains("bar")returns false.
You need to be very careful with this rule. If a script requires a return type, you may accidentally be returning a null value even though it doesn't look like it. Can you tell what value this script returns:
x = 5 if (x == 5) { true; } else { false; }This script has no return value (null). Here is the proper way to define this script:
x = 5 if (x == 5) { return true; } else { return false; }You should always use the return keyword and semicolons when your last statement is surrounded by braces.
x = 5; print(x); //5
The initScript will be evaluated as soon as the survey starts and will never be reevaluated. When the onAnswerChanged tag is used below the initScript tag, it gets evaluated once after the initScript is evaluated and then every time any question's answer string changes thereafter. When the onAnswerChanged tag is used inside the question tag, it gets evaluated only when the answer to that question changes. Regardless of where the onAnswerChanged tag resides, it will not be evaluated by the question's default attribute and it will be evaluted by changes caused by Survey#loadXMLAnswers
NOTE: If your scripts become difficult to maintain, it is suggested that you define all methods and global variables inside the initScript and only use the onAnswerChanged scripts inside the question tag.
Every JSurveyLib onAnswerChanged and initScript has access to the current answer of every question on the survey. The answer of a question is stored in a variable with the same name as that question's id. For example, if your survey has a question on it with an id of "foo", every script has access to a variable named "foo" which resolves to the current answer string of this question.
One variable/id you should not use in your scripts is the word "ENVIRONMENT". This word is reserved for internal use.
These three methods can be used to change the state of a question. They are best explained by example:
<?xml version="1.0" encoding="UTF-8"?> <surveyConfig title="Validation Usage" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://jsurveylib.sourceforge.net/survey-8.01.29.xsd"> <onAnswerChanged> <![CDATA[setVisible("showme", makevisible.equals("yes"))]]> </onAnswerChanged> <page> <question id="makevisible"> <label>Should we make a question appear:</label> <yesNo/> </question> <question id="showme" mandatory="true"> <label>Show me:</label> <yesNo/> </question> </page> </surveyConfig>
This is a survey that has two questions and the second one starts invisible. You can toggle the second question's visibility by selecting between the "Yes" and "No" on the first question. setVisible takes two parameters. The first is a String of a question's id. The second is a boolean. If true, the question with this id will become visible. If false, it will become invisible. Try setting the underlined text into the word, "setEnabled". Your configuration should look like this:
<?xml version="1.0" encoding="UTF-8"?> <surveyConfig title="Validation Usage" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://jsurveylib.sourceforge.net/survey-8.01.29.xsd"> <onAnswerChanged> <![CDATA[setEnabled("showme", makevisible.equals("yes"))]]> </onAnswerChanged> <page> <question id="makevisible"> <label>Should we make a question appear:</label> <yesNo/> </question> <question id="showme" mandatory="true"> <label>Show me:</label> <yesNo/> </question> </page> </surveyConfig>
The second question will now toggle between enabled and disabled.
You may have tried changing this method to setValid but you will soon find out that your configuration will not work correctly. setValid takes a question id, a boolean and a String. The String will be displayed on the GUI to inform the user how they can correct the problem. Here is an example that uses setValid
<?xml version="1.0" encoding="UTF-8"?> <surveyConfig title="Validation Usage" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://jsurveylib.sourceforge.net/survey-8.01.29.xsd"> <onAnswerChanged> <![CDATA[setValid("fiveOrLess", fiveOrLess.length() <= 5, "You must use 5 characters or less!")]]> </onAnswerChanged> <page> <question id="fiveOrLess"> <label>Use five or less characters:</label> <textField/> </question> </page> </surveyConfig>
You may also notice that this String value is passed in whether or not the question is valid. For convenience, the String is ignored if the boolean is "true".
If you've been trying all these examples, you may have noticed how the "Finish" button is effected by the questions with scripts. In the first two examples, the questions with scripts are marked as mandatory but you can still click the "Finish" button if the questions are invisible or disabled, respectively. In the third survey, even though the question is optional, you cannot progress if it is invalid. When a question is in multiple states (ie: invisible and invalid) the progression rules are even more complex. This example illustrates these rules clearly:
<?xml version="1.0" encoding="UTF-8"?> <surveyConfig title="Validation Usage" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://jsurveylib.sourceforge.net/survey-8.01.29.xsd"> <onAnswerChanged><![CDATA[ setVisible("optional", visible.equals("checked")); setEnabled("optional", enable.equals("checked")); setValid("optional", valid.equals("checked"), "I'm invalid!"); ]]></onAnswerChanged> <page> <question id="visible"> <label>Make the question invisible:</label> <checkbox/> </question> <question id="enable"> <label>Make the question enabled:</label> <checkbox/> </question> <question id="valid"> <label>Make the question valid:</label> <checkbox/> </question> <question id="optional" mandatory="false"> <label>Change my status:</label> <textField/> </question> </page> </surveyConfig>
If you check the boxes "yes", "no", "yes" or "no", "yes", "yes" or "yes", "yes", "yes", you'll see that you can click the "Finish" button even though the question is invalid. To summarize, you cannot progress if a mandatory, answerable question is unanswered OR an answerable question is invalid. An answerable question is a question that is enabled and visible.
The answerOf method allows you to get the answer of a question by passing in its String id. It is usually unnecessary because you can already get the answer of a question by using the question's id as a variable. Here is an example:
<?xml version="1.0" encoding="UTF-8"?> <surveyConfig title="Answer Of Usage" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://jsurveylib.sourceforge.net/survey-8.01.29.xsd"> <initScript><![CDATA[ int intOfField(int field) { String value = answerOf("field" + field); try { return Integer.valueOf(value); } catch (Exception e) { return -999; } } void setValidationTo(boolean state) { for (int i = 0; i < 3; i++) { setValid("field" + i, state, "We don't add up to 20"); } } ]]></initScript> <onAnswerChanged><![CDATA[ int total = 0; for (int i = 0; i < 3; i++) { total += intOfField(i); } setValidationTo(total == 20); ]]></onAnswerChanged> <page> <label>Make the fields add up to 20:</label> <question id="field0"> <label>First:</label> <textField/> </question> <question id="field1"> <label>Second:</label> <textField/> </question> <question id="field2"> <label>Third:</label> <textField/> </question> </page> </surveyConfig>
This example uses answerOf mostly for convenience, but there are situations where it is required to accomplish a task (usually in combination with the populateTemplateAfter/populateTemplateBefore methods. These are described below).
This is also the first example that uses the initScript tag. This example uses it to define methods that are called later.
The setOnAnswerChanged method allows you to change the onAnswerChanged event listener of a question while the survey is running. It has two parameters: A string of the id to set and a string of the new onAnswerChanged to give the question. Here is an example:
<?xml version="1.0" encoding="UTF-8"?> <surveyConfig title="setOnAnswerChanged Usage" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://jsurveylib.sourceforge.net/survey-8.01.29.xsd"> <page> <question id="switcher"> <label>Switch:</label> <radioButtons> <choice id="invisible" label="invisible"/> <choice id="disabled" label="disabled"/> </radioButtons> <onAnswerChanged><![CDATA[ if (switcher.equals("invisible")) { setEnabled("below", true); setOnAnswerChanged("invisibleDisabled", "setVisible(\"below\", invisibleDisabled.equals(\"no\"))"); } else { setVisible("below", true); setOnAnswerChanged("invisibleDisabled", "setEnabled(\"below\", invisibleDisabled.equals(\"yes\"))"); } ]]></onAnswerChanged> </question> <question id="invisibleDisabled"> <label>I change the question IF you set the switch:</label> <yesNo/> </question> <question id="below"> <textField/> </question> </page> </surveyConfig>
This survey completely changes the onAnswerChanged of the "invisibleDisabled" based off of what you pick in the "switcher" question. You may notice that the setOnAnswerChanged method does not get evaluated until you change the answer of the question it was applied to.
The populateDropdown method allows you to redefine the choices that the dropdown question type has while the survey is running. It takes two parameters: A string of a dropdown id and a string array. The string array contains the new ids and labels of the choices. It is ordered like "id1, label1, id2, label2, id3, label3...". You must pass in at least two elements. Here is an example:
<?xml version="1.0" encoding="UTF-8"?> <surveyConfig title="populateDropdown Usage" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://jsurveylib.sourceforge.net/survey-8.01.29.xsd"> <initScript><![CDATA[ void configureDropdown() { populateDropdown("mine", new String[] {"first", c1, "second", c2, "third", c3}); } ]]></initScript> <page label="Make your own dropdown!"> <question id="c1"> <label>First Choice:</label> <textField/> <onAnswerChanged>configureDropdown()</onAnswerChanged> </question> <question id="c2"> <label>Second Choice:</label> <textField/> <onAnswerChanged>configureDropdown()</onAnswerChanged> </question> <question id="c3"> <label>Third Choice:</label> <textField/> <onAnswerChanged>configureDropdown()</onAnswerChanged> </question> <question id="mine" mandatory="true"> <label><![CDATA[I will be populated by the<br/>contents of the text fields:]]></label> <dropdown> <choice id="" label=""/> </dropdown> </question> </page> </surveyConfig>
As you type into the textFields, the dropdown will become populated with the labels as choices. If you have selected a choice in the dropdown, it will be reset to the first choice when the populateDropdown method is called. It's important to notice that the dropdown question does not call configureDropdown() itself. If it did, every time you select a choice it would become repopulated and reset to the first choice. The effect to the user is a dropdown where they can see choices but the only one that can be selected is the first.
The goToPage method will jump to a non-skipped page. This method takes the index of the page to go to as a parameter. The index is zero based meaning that the first page you've defined has an index of 0, the second page has an index of 1 and so on. Here is an example:
<?xml version="1.0" encoding="UTF-8"?> <surveyConfig title="Go To Page Usage" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://jsurveylib.sourceforge.net/survey-8.01.29.xsd"> <page label="0"> <question id="pageJump"> <label>Which page do you want to jump to:</label> <radioButtons> <choice id="0" label="0"/> <choice id="1" label="1"/> <choice id="2" label="2"/> <choice id="3" label="3"/> </radioButtons> <onAnswerChanged><![CDATA[ goToPage(Integer.valueOf(pageJump)); ]]></onAnswerChanged> </question> </page> <page label="1"/> <page label="2"/> <page label="3"/> </surveyConfig>
The skipPage method allows you to set a page as skipped so that it will not be visited while taking the survey. This method takes two parameters: The index of the page to effect and a boolean that specifies whether the question is skipped. It is illegal to set the current page as skipped. Here is an example:
<?xml version="1.0" encoding="UTF-8"?> <surveyConfig title="Skip Page Usage" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://jsurveylib.sourceforge.net/survey-8.01.29.xsd"> <page label="page 0"> <question id="page0"> <label>Skip Page Zero:</label> <yesNo/> <!-- This will throw an exception if set to "yes" because it's the current page! --> <onAnswerChanged>skipPage(0, page0.equals("yes"));</onAnswerChanged> </question> <question id="page1"> <label>Skip Page One:</label> <yesNo/> <onAnswerChanged>skipPage(1, page1.equals("yes"));</onAnswerChanged> </question> <question id="page2"> <label>Skip Page Two:</label> <yesNo/> <onAnswerChanged>skipPage(2, page2.equals("yes"));</onAnswerChanged> </question> </page> <page label="page 1"/> <page label="page 2"/> </surveyConfig>
NOTE: The populateTemplateBefore/populateTemplateAfter methods are experimental. They may not exist or behave the same in the future.
The populateTemplateBefore/populateTemplateAfter methods can be used to dynamically place questions to the survey while it is being taken. The populateTemplateBefore/populateTemplateAfter methods have five parameters:
Suppose you want to write a survey that asks the user to list the names of their friends. You want to allow the user to enter an unlimited amount of names. The populateTemplateBefore/populateTemplateAfter methods make this possible:
<?xml version="1.0" encoding="UTF-8"?> <surveyConfig title="Populate Template Usage" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://jsurveylib.sourceforge.net/survey-8.01.29.xsd"> <initScript><![CDATA[ nameCount = 0; boolean allAnswered() { for (int i = 0; i <= nameCount; i++) { name = answerOf("name" + i); if ("".equals(name)) { return false; } } return true; } void addNewName() { nameCount++; String id = "name" + nameCount; String label = "Name " + (nameCount + 1) + ":"; String afterId = "name" + (nameCount - 1); populateTemplateAfter("text", id, label, false, afterId); } ]]></initScript> <onAnswerChanged><![CDATA[ if (allAnswered()) { addNewName(); } ]]></onAnswerChanged> <templates> <template name="text"> <textField/> </template> </templates> <page> <label>Enter as many names as you want:</label> <question id="name0"> <label>Name 1:</label> <template name="text"/> </question> </page> </surveyConfig>
Every time a question is answered the onAnswerChanged is evaluated. The onAnswerChanged checks if all questions are answered and if they are, adds a new "namex" question. Also note that this is an example of answerOf actually being necessary. We need to have a way to get the answers of questions that don't exist at startup and answerOf provides a way to do this.
NOTE: One of the reasons these methods are experimental is because they make it possible to create surveys that break Survey#loadXMLAnswers. For example, it's possible to make a survey that populates a template only if you take it after 5pm. If you save the results and then load them tomorrow at 3pm, the survey cannot reproduce the conditions to create this question it has an answer to. As a result, the Survey#loadXMLAnswers will throw an IllegalArgumentException because it has an answer for a question that doesn't exist.
Survey#loadXMLAnswers will try as best as it can to answer these questions. It goes in a loop answering all questions that exist until the result file only contains questions that the survey doesn't have. At that point, it will throw the IllegalArgumentException. If you catch this exception and continue, the Survey object will have loaded what it could. The behavior of Survey#loadXMLAnswers as just described is not guaranteed to use this algorithm in the future. For example, it may simply print a warning instead of throwing an exception in the future. The implementation is being described so that users understand that in many situations you can use populateTemplateBefore/populateTemplateAfter and Survey#loadXMLAnswers together.
This concludes the JSurveyLib Scripting Guide. For support and bug reports, please post a message in the JSurveyLib forum or tracker.