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

StepStep descriptionRequirementRequirement constraints
1User chooses to make a new reservation
2System show the reservation screen where the user can enter Begin, Endate and Roomtype
3User enters begindate and enddate or the numbers of nightsREQ_RV_ 403 Reservation periodInvariant: 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

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.