On the branching page, we have described how a tool can produce tool output definitions, etc and how authoring and branching interacts with the tools to use the definitions for tool based branching. But if all you want to know is how to create Tool Outputs that can be used in branching, then this page is your step by step "how-to" guide.
The easiest way to start is to copy some existing code. To see how to do a Boolean output or a Range output, have a look at McOutputFactory in lams_tool_lamc, or to see a more complex use of default conditions based on ranges see VoteOutputFactory in lams_tool_vote. There is also a ForumOutputFactory in lams_tool_forum, and that uses a slightly different messageService bean.
What is this "condition" vs "tool output" business? What am I creating?
From a tools perspective you are creating tool outputs. These happen to be used to create conditions for branching in authoring. But they could just be passed onto another tool (sometime in the future when we have implemented tool to tool output transfers). It just so happens that at the moment the outputs are only used for branching, so all the outputs look like conditions and in some cases we set up the conditions for authoring to use, but please keep in mind that the tool never decides which branch to take - it merely reports back to the progress engine on some data in the tool's tables. The tool does not "make a decision".
There are two sorts of outputs. Some of them are simple and can be "hardcoded" into your tool. For example, what mark did the user get in the multiple choice quiz? Did they user get all the questions right in the multiple choice quiz? How many postings has a user done in the forum?
How are the outputs used for Branching? When the teacher is in authoring, the Authoring client will call your tool to get
its ToolOutputDefinitions. These definitions contain a name, a description to be shown to the teacher and a type - Boolean, Range of Integers, etc. If it is a range, then you also supply a start and stop value.
Then in authoring, the teacher sets up conditions. For example, say your ToolOutputDefinition is a Integer range and is the mark that a user gets in the multiple choice quiz. The ToolOutputDefinition will specify the minimum mark and the maximum mark the learner can get (as the start and stop values). Then authoring will allow the teacher to set up Conditions (mark is 0 to 3, mark is 4 to 10, etc) and then these conditions are mapped to a branch. The conditions and the mapping to a branch are stored as part of the learning design, they are not stored in the tool's tables.
Then when the lesson is running and the learner reaches the branching point, then the progress engine calls the tool to get the ToolOutput values. The tool calculates the output and returns it back to the progress engine. In our example, the tool would find the learner's details for the quiz and get their overall mark, and return that back to the progress engine. The progress engine then compares the output to the conditions set up in authoring and determines the branch.
Setting up Simple Range and Boolean Values
There are also simple String values but we haven't used this yet, so when we do, it will be based on Range logic and the authoring client will need to be modified.
Set Message Bean and Output Factory
To make life easier, an abstract class org.lamsfoundation.lams.tool.OutputFactory has been written to handle many of the details.
In your tool's service package, create a BlahOutputFactory class based on org.lamsfoundation.lams.tool.mc.service.OutputFactory, replacing Blah with your tool name. This class should be a singleton defined in your applicationContext.xml file. The BlahOutputFactory will need a message source to access the translations in the I18N files.
Tools that already have their own MessageService bean defined in the Spring applicationContext.xml file (e.g. Forum has forumMessageService) can use that bean as a toolMessageService property. If a tool does not have their own MessageService bean then it can use the loadedMessageSourceService property and use the loadedMessageSourceService bean from the LAMS core. To use the loadedMessageSourceService you do NOT need to define the bean itself - just reference it - and supply the base name of the tool's language files as the languageFilename parameter.
One of the two options (but not both) is required so that the OutputFactory method can get the internationalised description of the output definition from the tool's internationalisation files. If neither the messageService or the I18N name is not supplied then the name if the output definition will appear in the description field, modified to replace dots with spaces.
Add an entry in your tool's service bean to access the factory singleton (ie add a variable BlahOutputFactory outputFactory and add a getter and a setter).
Create Simple Definitions for Authoring
There are three things you need to do to create simple definitions for authoring.
Step 1 In your service bean, implement the method "public SortedMap<String, ToolOutputDefinition> getToolOutputDefinitions(Long toolContentId) throws ToolException;". Normally this method just calls your BlahOutputFactory to do the work. This method gets the definitions for possible output for an activity, based on the toolContentId. Make sure this method is covered in the transactionAttributes section of your tool's service bean - normally tools include "get*" in the transaction and this covers all the Condition related calls.
Step 2 In your BlahOutputFactory, create the ToolOutputDefinitions, using the methods in OutputFactory. Each type of ToolOutputDefinitions (String, Boolean, etc) has its own method and these methods take care of getting the description of the condition from the language files, setting up the correct type, etc.
The name field in your definition is extremely important. Firstly, this name is the name that the progress engine will use to ask for the ToolOutput when the lesson is run. So your code must know what the name means.
Secondly, the name is used to find the descriptions in the language files. When the definition is created, the OutputFactory code will go to the language file and look for an entry with the key "output.desc.<name>" e.g. "output.desc.learner.mark". If for some reason it can't find the description, the OutputFactory will take the name, convert the "." to spaces and use that as the description. So the user will see something in the Authoring, but it won't be an I18N'd description.
What are default conditions? Some ToolOutputDefinitions define their own conditions, and these conditions are shown to the teacher in authoring. When these conditions exist, the teacher cannot set up their own conditions. There are normally two conditions (true/false) for Boolean values, and these are set up for you automatically by the OutputFactory when you use buildBooleanOutputDefinition(). This is a convenience for the user, as they are nearly always going to want true/false conditions so why not do it for them?
It is also a way to generate some slightly more complex tool output / condition combinations, such as the "Which nomination did the user choose?" output for Voting. But normally if you are doing simple tool outputs then the only defaultConditions are the ones for Booleans.
Step 3 Add the description text to your language files, remember to prepend "output.desc." to each definition's name. If it is a boolean condition, you need to add "output.desc.(definition name).true" and "output.desc.(definition name).false" entries that define the condition descriptions for the True and False conditions (which are set up automatically by the OutputFactory).
If you need to get to an arbitrary string in the I18N file, say to generate a condition name then your OutputFactory class can call getI18NText(String key, boolean addPrefix) and set addPrefix to false. This will call the code that gets the description from the I18N file, without prepending "output.desc.".
Create Tool Outputs for Learning
There are two steps to be done to create the tool outputs.
Step 1 In your service bean, implement the ToolOutput related methods, calling your BlahOutputFactory to do the work.
- public SortedMap<String, ToolOutput> getToolOutput(List<String> names, Long toolSessionId, Long learnerId);
Get all the outputs that match the list of names. If names is null, then all possible output will be returned. If names is an array of length 0, then no output will be returned. If the learnerId is null, then return the outputs based on all learners in that toolSession. If the output is nonsense for all learners, then return an "empty" but valid answer. For example, for a range value you might return 0.
- public ToolOutput getToolOutput(String name, Long toolSessionId, Long learnerId);
Get the outputs for a particular tool output name. If the learnerId is null, then return the outputs based on all learners in that toolSession. If the output is nonsense for all learners, then return an "empty" but valid answer. For example, for a range you might return 0.
Step 2 In your BlahOutputFactory, create the ToolOutput objects, based on your definitions. The "name" parameter is the name from the ToolOutputDefinition object, and will have been stored as part of the learning design.
If the Output is a Boolean, then you have to calculate whether or not it is true or false, and return the boolean value.
If the Output is a Range then all you have to return is the calculated value e.g. learner's mark. Remember you do not the actual comparison. You are producing an output, the progress engine then does the comparisons to determine which branch to select.
Setting up Outputs and Conditions based on User/Tool Information
Once we get past the simple "what is the mark?" type outputs, the ToolOutputDefinitions start to get harder to define. The authoring client can cope with generating exclusive conditions based on ranges, but it can't with anything that overlaps or needs to record an ID value for some tool content in the conditions.
For example, the task tool needs to set up a series of conditions such as "Task 1 and 2 completed", "Task 1 and 3 completed", "Task 4 completed". To test this, we need to keep a record of which tasks are being checked. So can't we just pass the ID's of the task to Flash, have that build a series of "and" cases and then have the newly created conditions saved in the learning design? Yes and no. We could get Flash to build "and/or" type cases. But the ID values are meaningless to the learning design, and when you copy the tool content, the ID values stored in the learning design are out of date. So then the core would have to ask the tool to give it the new IDs for the conditions. But if the tool kept track of the IDs, then it can ensure they are always the correct values when the content is copied. What if the test is "Does answer to the second question (in Q&A) contain any of the words Sydney, Brisbane or Melbourne"? What exactly does it mean to do a string test on the second value in learner's data. We could get around all the problems with whats the "2" item, or the "3rd item in the 2 item", if we tried hard enough, but the ID problem is extremely difficult to avoid.
So, the compromise design is to add another tab to the tool authoring screens. If the tool wants this feature, then its adds a tab "Conditions". In this tab, the user will set up conditions that are tailored to the tool. The tool then saves these conditions in their own tables as part of the tool content.
Now when the Flash authoring client requests the ToolOutputDefinitions, the tool creates any simple, hardcoded definitions as outlined previously on this page.
Then if the tool content has any special conditions, then it creates an OUTPUT_SET_BOOLEAN ToolOutputDefinition. In this definition the tool creates a OUTPUT_BOOLEAN BranchCondition for each special condition. The name for each BranchCondition must be unique, and must be created using the "String buildConditionName(String definitionName, String uniquePart)" method. For the Flash authoring client to work properly, it needs the condition name to be in a particular format, so it can link it up with the ToolOutputDefinition later, and calling buildConditionName() ensures that the condition name is in the correct format. Each condition will also have its exactMatchValue set to Boolean.TRUE.toString().
Then Flash will display all the default conditions, and the teacher will map them to branches and the mapping and branch conditions are stored in the learning design as normal.
When the lesson is run, the progress engine will pass the condition name to the tool and the tool will calculate true/false based on the condition it has stored in its own tables. If the tool needs to split up the condition name back into its parts, then it can call "String splitConditionName(String conditionName)".
Finally, the branching logic in the progress engine will test each condition looking for one that is true, and when it finds one that is true, it will take that branch. If none of them are true, it will take the default branch.
The orderId field in the BranchConditions is important. This is the order that the conditions will appear in the Flash dialogue box and, more importantly, this is the order in which the progress engine will check the conditions. So say that the tool's data is such that conditions orderId #2 and orderId#4 are both true. Then it is condition orderId #2 that will get selected.
Checking for Text
One of the other sorts of tests we will do like this will be checking for text in forum or q&a entries. For this we need to build a screen that allows the users to enter the desired/undesired words in an entry form similar to the Google Advanced Search (so that users don't have to remember syntax like +, -, and, or, etc) and from that generate a regex that is stored in the condition details in the tool (would be nice to store a compiled regex for performance but that would have issues when server restarts/change JVMs/etc). Then when the learner reaches the branching point, the regex would be run against the learner's responses.
As we develop this for the first time, we need to make as much of it as possible generic code that can be used across many tools. The regex parts can go in a utility class in lams_common, and we need to use a tag or some other jsp fragment that reuses the same search entry fields.
Setting up Complex Outputs
In the future, we may have output that are "structured", such as a series of postings for forum. We won't need this until we handle tool to tool outputs, so apart from fields in the ToolOutputDefinition and ToolOutput objects referring to complex bits, we haven't done any of the implementation for this. So you can ignore any reference to Complex outputs for now.