Using Document Script fragments in Sparx EA
In this article we discover the possibilities of Document Script fragments when dealing with complex requirements for document geneneration templates in Sparx Enterprise Architect.
The request
The other day I got a difficult request for a document generation template. At this client they are documenting Use Case scenarios using the structured scenario editor in EA. Each scenario step can be linked to a requirement element.

Each Requirement could have one or more constraints

Based on these scenarios the customer wanted to generate a test scenario document that contained a table like this
Step | Step description | Requirement | Requirement constraints |
---|---|---|---|
1 | User chooses to make a new reservation | ||
2 | System show the reservation screen where the user can enter Begin, Endate and Roomtype | ||
3 | User enters begindate and enddate or the numbers of nights | REQ_RV_ 403 Reservation period | Invariant: StartDate < Enddate Post-condition: A reservation must have a reservation period in the future |
The first three columns are available in the standard point and click templates, but the problematic part is the Requirement constraints. These are not available in the standard templates.
SQL Fragments?
Now whenever I need data that is not available in the standard templates I first try to fill that in with an SQL Fragment. In this case that’s not really feasible because of the way the scenario’s are stored in the database. They are stored as an XML string in the column t_objectscenario.XMLContent
<path>
<step name="User chooses to make a new reservation" guid="{5E99836E-AD52-41b1-89E1-6F04C0027DCD}" level="1" uses="" useslist="" result="" state="" trigger="1" link=""/>
<step name="System shows the reservation screen where the user can enter BeginDate, EndDate and Roomtype" guid="{1B8AFDC3-F035-46fd-BBA6-D6AEDE403B2A}" level="2" uses="" useslist="" result="" state="" trigger="0" link=""/>
<step name="User enters BeginDate and EndDate or the number of nights" guid="{4FAAFB0F-F729-4d5c-8755-D574F9524021}" level="3" uses="REQ_RV_ 403 Reservation period" useslist="" result="" state="" trigger="1" link=""/>
<step name="System shows the available Roomtypes for the chosen period" guid="{C87C6C12-BDFE-497c-97CF-22CEEAAECABE}" level="4" uses="REQ_RV_ 401 Book on Roomtype" useslist="" result="" state="" trigger="0" link=""/>
<step name="User selects a Roomtype" guid="{A6CC2241-A687-4c42-AB2E-83B35430A38E}" level="5" uses="REQ_RV_ 403 Reservation period" useslist="" result="" state="" trigger="1" link="">
<extension level="5a" guid="{CFCA9B39-7CC1-4ced-9BE2-6D4E80E35507}" join="{304C0DF4-6707-4cfd-91A3-24D4FA0D5275}"/>
<extension level="5b" guid="{69265E19-90D0-46b2-8488-3308C7100D73}" join="{304C0DF4-6707-4cfd-91A3-24D4FA0D5275}"/>
</step>
<step name="User selects a Customer using Find Customer" guid="{304C0DF4-6707-4cfd-91A3-24D4FA0D5275}" level="6" uses=" REQ_RV_ 404 Customer on reservation" useslist="" result="" state="" trigger="1" link="{A5C62D71-0F26-47fa-AACC-31E91F119B49}"/>
<step name="If a Customer was not found then the user creates a new customer using Create New Customer" guid="{F98D2A25-B95B-42a5-8C88-EE73CCB9A496}" level="7" uses=" " useslist="" result="" state="" trigger="1" link="{DFC911EC-CDD6-403c-80C3-80A707B72C48}"/>
<step name="System shows an overview of the Reservation details" guid="{D6A2816D-BB8E-43c7-AAA9-BFEC16521DCA}" level="8" uses="" useslist="" result="" state="" trigger="0" link=""/>
<step name="User confirms the Reservation" guid="{82BE42BB-3605-44a3-B8AE-D5C1065F0644}" level="9" uses="" useslist="" result="" state="" trigger="1" link=""/>
<step name="System stores the Reservation" guid="{C573CB71-96B2-4512-A50D-7D38B20DFCC7}" level="10" uses="" useslist="" result="" state="" trigger="0" link=""/>
<context>
<item oldname="REQ_RV_ 403 Reservation period" guid="{187B149E-5A18-4bfa-9E45-0E1237A54608}"/>
<item oldname="Make Payment" guid="{678C2AAE-A4EB-4124-947E-BD367F7C6247}"/>
<item oldname="Reservations User" guid="{687B191E-4AA7-423c-9F7C-11AE757F2CE4}"/>
<item oldname="REQ_RV_ 401 Book on Roomtype" guid="{87DC7F02-BCD8-4ed9-A4A5-076E704A2C7D}"/>
<item oldname="Reservation Screen" guid="{8B38FAC7-1380-4287-84AC-81F42AB0028B}"/>
<item oldname="Find Customer" guid="{A5C62D71-0F26-47fa-AACC-31E91F119B49}"/>
<item oldname="REQ_RV_ 404 Customer on reservation" guid="{D5A6996B-2D6B-4525-8EB6-66B75F7F4A6A}"/>
<item oldname="Create New Customer" guid="{DFC911EC-CDD6-403c-80C3-80A707B72C48}"/>
<item oldname="REQ_RV_ 402 Book on RoomNumber" guid="{FF3E1EB7-343B-4893-B601-4BA1B9C351F3}"/>
<item oldname="Reservation" guid="{FFEC422A-B301-48d6-8E16-6B9D887F2F83}"/>
</context>
</path>
Script Fragments?
The next option is to use script fragments. With Script Fragments you use a script to return an XML string containing the contents to fill either a table, or a set of simple fields. Now the problem here is that we need data in two different levels.
One level is the scenario, with it’s name, type, and description, and the other level are the scenariosteps, where ne need to add the linked requirement, and the requirement contraints.
Another problem is in EA you can’t add a fragment in a scenario section; or more correctly, you can add it to the scenario section, but there is no way to pass the ID of the scenario to the fragment. You can only pass either the ObjectID, or the PackageID to a fragment. This means we need a single fragment to fill the complete scenarios section of the document, including the tables for the scenariosteps.
Solution: Use Document Script Fragments
The only way to get this done is to use a Document Script fragment. With this type of fragment the script has to return a string containing the RTF code for this part of the document. Now luckily you don’t have to write the RTF code yourself. EA’s DocumentGenerator class will help us returning the RTF code for a generated document.
The main template
Since this is a template for use cases the main template contains the use case diagram, and details for the use cases in the package

In the element section we then call the template fragment UC_Scenarios
The Document Script fragment
The fragment template itself doesn’t contain much content. Only the title, the package and element sections, and the custom section.

In the properties of the fragment, we set the type of fragment, select the script and set the call.

The ScenarioFragment Script
In the script we define a function witht the name documentUseCaseScenarios(ObjectID) that will be called from the fragment.
function documentUseCaseScenarios(objectID)
dim docgen as EA.DocumentGenerator
set docgen = Repository.CreateDocumentGenerator()
docgen.NewDocument "UC_ScenarioSteps"
dim scenarios
set scenarios = getScenariosForUseCase(objectID)
dim scenario
for each scenario in scenarios
dim scenarioXmlString
scenarioXmlString = getScenarioXML(scenario)
docgen.DocumentCustomData scenarioXmlString, 1, "UC_Scenario"
dim scenarioStepsXmlString
scenarioStepsXmlString = getScenarioStepsXMLString(objectID, scenario.Name)
docgen.DocumentCustomData scenarioStepsXmlString, 1, "UC_ScenarioSteps"
next
documentUseCaseScenarios = docgen.GetDocumentAsRTF()
end function
The first thing we do is get the EA.DocumentGenerator object and create a new Document
Then we call the method getScenariosForUseCase(objectID)
function getScenariosForUseCase(useCaseObjectID)
dim scenarios
set scenarios = CreateObject("System.Collections.ArrayList")
dim sqlGetData
sqlGetData = "select oc.Scenario, oc.ScenarioType, oc.XMLContent, oc.Notes, oc.ea_guid " & vbNewLine & _
" from t_objectscenarios oc " & vbNewLine & _
" inner join t_object o on o.Object_ID = oc.Object_ID " & vbNewLine & _
" where o.Object_ID = " & useCaseObjectID & " " & vbNewLine & _
" order by case when oc.ScenarioType = 'Basic Path' then 1 else 2 end, oc.EValue "
dim results
set results = getArrayListFromQuery(sqlGetData)
dim scenarioRow
dim mainScenarioXMLDom
set mainScenarioXMLDom = nothing
for each scenarioRow in results
dim scenario
set scenario = new UsecaseScenario
scenario.initialize scenarioRow(0), scenarioRow(1), scenarioRow(2), scenarioRow(3), scenarioRow(4)
scenarios.Add scenario
'the main scenario xml contains the entry and join information
if mainScenarioXMLDom is nothing then
set mainScenarioXMLDom = CreateObject("MSXML2.DOMDocument")
mainScenarioXMLDom.LoadXML scenarioRow(2)
else
scenario.ResolveEntryAndJoin mainScenarioXMLDom
end if
next
'return
set getScenariosForUseCase = scenarios
end function
In this function we query the database directly, and get the data we need in a two dimensional ArrayList. For each row we create a new UsecaseScenario object, which is a class definition we created to keep the data of a scenario together.
UseCaseScenarioClass
const ucBasicPath = "Basic Path"
const ucAlternate = "Alternate"
const ucException = "Exception"
Class UsecaseScenario
Private m_Name
Private m_Notes
Private m_ScenarioType
Private m_GUID
Private m_Entry
Private m_Join
Private m_XMLContent
Private Sub Class_Initialize
m_Name = ""
m_Notes = ""
m_ScenarioType = ""
m_GUID = ""
m_Entry = ""
m_Join = ""
m_XMLContent = ""
End Sub
' Name property.
Public Property Get Name
Name = m_Name
End Property
Public Property Let Name(value)
m_Name = value
End Property
' GUID property.
Public Property Get GUID
GUID = m_GUID
End Property
Public Property Let GUID(value)
m_GUID = value
End Property
' Notes property.
Public Property Get Notes
Notes = m_Notes
End Property
Public Property Let Notes(value)
m_Notes = value
End Property
' ScenarioType property.
Public Property Get ScenarioType
ScenarioType = m_ScenarioType
End Property
Public Property Let ScenarioType(value)
m_ScenarioType = value
End Property
' Entry property.
Public Property Get Entry
Entry = m_Entry
End Property
Public Property Let Entry(value)
m_Entry = value
End Property
' Join property.
Public Property Get Join
Join = m_Join
End Property
Public Property Let Join(value)
m_Join = value
End Property
' XMLContent property.
Public Property Get XMLContent
XMLContent = m_XMLContent
End Property
Public Property Let XMLContent(value)
m_XMLContent = value
End Property
public function initialize(name, scenarioType, xmlContent, notes, guid)
me.Name = name
me.ScenarioType = scenarioType
me.XMLContent = xmlContent
me.Notes = notes
me.GUID = guid
end function
public function ResolveEntryAndJoin(mainScenarioXMLDom)
dim extensionNode
set extensionNode = mainScenarioXMLDom.SelectSingleNode("//extension[@guid = '" & me.GUID & "']")
if extensionNode is nothing then
'no entry or join found in main scenario
exit function
end if
me.Entry = extensionNode.GetAttribute("level")
dim joinStepGUID
joinStepGUID = extensionNode.GetAttribute("join")
'fin the step with the given joinStepGUID
if len(joinStepGUID) > 0 then
dim joinStepNode
set joinStepNode = mainScenarioXMLDom.SelectSingleNode("//step[@guid = '" & joinStepGUID & "']")
if not joinStepNode is nothing then
me.Join = joinStepNode.GetAttribute("level")
end if
end if
end function
end Class
Next to serving as a datastructure, this class also contains the details on how to get the Entry and Join points from the main scenario XML.
The Scenario Template
For each scenario, use the scenario template to get the details of the scenario.

This template is called from the main script:
docgen.DocumentCustomData scenarioXmlString, 1, "UC_Scenario"
The contents for this scenario are coming from an XML string. The function that assembles this XML
function getScenarioXML(scenario)
dim xmlDOM
set xmlDOM = CreateObject( "Microsoft.XMLDOM" )
xmlDOM.validateOnParse = false
xmlDOM.async = false
dim node
set node = xmlDOM.createProcessingInstruction( "xml", "version='1.0'")
xmlDOM.appendChild node
'
dim xmlRoot
set xmlRoot = xmlDOM.createElement( "EADATA" )
xmlDOM.appendChild xmlRoot
dim xmlDataSet
set xmlDataSet = xmlDOM.createElement( "Dataset_0" )
xmlRoot.appendChild xmlDataSet
dim xmlData
set xmlData = xmlDOM.createElement( "Data" )
xmlDataSet.appendChild xmlData
dim xmlRow
set xmlRow = xmlDOM.createElement( "Row" )
xmlData.appendChild xmlRow
'ScenarioType
dim xmlScenarioType
set xmlScenarioType = xmlDOM.createElement("ScenarioType")
xmlScenarioType.text = scenario.ScenarioType
xmlRow.appendChild xmlScenarioType
'Name
dim xmlName
set xmlName = xmlDOM.createElement("name")
xmlName.text = scenario.Name
xmlRow.appendChild xmlName
'Entry
dim xmlEntry
set xmlEntry = xmlDOM.createElement("Entry")
xmlEntry.text = scenario.Entry
xmlRow.appendChild xmlEntry
'Join
dim xmlJoin
set xmlJoin = xmlDOM.createElement("Join")
xmlJoin.text = scenario.Join
xmlRow.appendChild xmlJoin
'Notes
dim formattedAttr
set formattedAttr = xmlDOM.createAttribute("formatted")
formattedAttr.nodeValue="1"
dim xmlNotes
set xmlNotes = xmlDOM.createElement("Notes")
xmlNotes.text = scenario.Notes
xmlNotes.setAttributeNode(formattedAttr)
xmlRow.appendChild xmlNotes
'return
getScenarioXML = xmlDOM.xml
end function
This function gets the data from the Scenario object into an xml string as expected by EA. Important to remember to set the formatted=1 attribute for anything containing notes that could contain formatted text.
The result of this method could be something like this
<?xml version="1.0" standalone="no" ?>
<EADATA>
<Dataset_0>
<Data>
<Row>
<ScenarioType>Alternate</ScenarioType>
<name>Overbooking on Roomtype</name>
<Entry>5b</Entry>
<Join>6</Join>
<Notes formatted="1">In some cases, and especially for larger hotels, the user wants to overbook a certain RoomType and book more rooms then there actually are.
Chances are that, because of cancellations, there will be a room available anyway.</Notes>
</Row>
</Data>
</Dataset_0>
</EADATA>
The ScenarioSteps template
After the Scenario template, we call the ScenarioSteps template

This template is also called in the main script
docgen.DocumentCustomData scenarioStepsXmlString, 1, "UC_ScenarioSteps"
And the method to get the scenarioStepsXmlString
function getScenarioStepsXMLString(objectID, scenarioName)
dim xmlDOM
set xmlDOM = CreateObject( "Microsoft.XMLDOM" )
xmlDOM.validateOnParse = false
xmlDOM.async = false
dim node
set node = xmlDOM.createProcessingInstruction( "xml", "version='1.0'")
xmlDOM.appendChild node
'
dim xmlRoot
set xmlRoot = xmlDOM.createElement( "EADATA" )
xmlDOM.appendChild xmlRoot
dim xmlDataSet
set xmlDataSet = xmlDOM.createElement( "Dataset_0" )
xmlRoot.appendChild xmlDataSet
dim xmlData
set xmlData = xmlDOM.createElement( "Data" )
xmlDataSet.appendChild xmlData
'add the contents for this scenario
addScenarioContents xmlDom, xmlData, ObjectID, scenarioName
getScenarioStepsXMLString = xmlDOM.xml
end function
Again we create the basic XML as needed by EA, and then we call the addScenarioContents function
function addScenarioContents(xmlDom, xmlData, scenario)
'get the scenario xml
dim scenarioXML
set scenarioXML = CreateObject("MSXML2.DOMDocument")
if not scenarioXML.LoadXML(scenario.XMLContent) then
'exit if not a valid XML
exit function
end if
'loop steps
dim stepNodes
set stepNodes = scenarioXML.SelectNodes("//step")
dim stepNode
for each stepNode in stepNodes
'add details for each step
addRow xmlDOM, xmlData, stepNode, scenarioXML
next
end function
This part parses the XML content of the scenario, loops the steps, and add a row to the XML for each step
function addRow(xmlDOM, xmlData, stepNode, scenarioXML)
dim xmlRow
set xmlRow = xmlDOM.createElement( "Row" )
xmlData.appendChild xmlRow
'Step number
dim xmlStep
set xmlStep = xmlDOM.createElement( "step" )
xmlStep.text = stepNode.GetAttribute("level")
xmlRow.appendChild xmlStep
'Step Name
dim xmlName
set xmlName = xmlDOM.createElement( "name" )
xmlName.text = stepNode.GetAttribute("name")
xmlRow.appendChild xmlName
'Uses Name
dim xmlUses
set xmlUses = xmlDOM.createElement( "uses" )
xmlUses.text = stepNode.GetAttribute("uses")
xmlRow.appendChild xmlUses
'Requirement constraints
dim xmlConstraints
set xmlConstraints = xmlDOM.createElement("constraints")
xmlConstraints.text = getConstraintsText(xmlUses.text, scenarioXML)
xmlRow.appendChild xmlConstraints
'Expected Results
dim xmlResult
set xmlResult = xmlDOM.createElement( "result" )
xmlResult.text = stepNode.GetAttribute("result")
xmlRow.appendChild xmlResult
'State
dim xmlState
set xmlState = xmlDOM.createElement( "state" )
xmlState.text = stepNode.GetAttribute("state")
xmlRow.appendChild xmlState
end function
Now most of this function is pretty straightforward, except for the Requirement Constraints. We use the method getConstraintsText to get to get the constraints of the linked requirement object
function getConstraintsText(requirementName, scenarioXML)
dim text
text = ""
getConstraintsText = text 'initialize
'get the requirement GUID
dim reqNode
set reqNode = scenarioXML.SelectSingleNode("//item[@oldname = '" & sanitizeXMLString(requirementName) & "']")
if reqNode is nothing then
exit function
end if
'get the requirement object
dim requirementGUID
requirementGUID = reqNode.GetAttribute("guid")
'get the constraint details with a query
dim sqlGetData
sqlGetData = "select oc.ConstraintType + ': ' + oc.[Constraint] as ConstraintText " & vbNewLine & _
" from t_objectconstraint oc " & vbNewLine & _
" inner join t_object o on o.Object_ID = oc.Object_ID " & vbNewLine & _
" where o.ea_guid = '" & requirementGUID & "' " & vbNewLine & _
" order by oc.Weight "
dim constraintStrings
set constraintStrings = getVerticalArrayListFromQuery(sqlGetData)
dim constraintString
if constraintStrings.Count = 0 then
exit function
end if
text = Join(constraintStrings(0).ToArray(), vbNewLine)
'return
getConstraintsText = text
end function
In this function selects the requirements GUID from the XML, and then uses an SQL Query to get the constraint details directly. This could have been done using the API as well, but this method is faster, and also lets the database do the sorting for us based on the Weight attribute.
The result
The result of all of this is a pretty neat document, containing all scenarios in a clean tabular format, ready to be used as test scenarios.



More about scripting or document generation
Leave a Reply
Want to join the discussion?Feel free to contribute!