Skip to content

Commit d5b66f6

Browse files
galtmaj-stein-nist
authored andcommitted
Test modify phase, plus minor XSLT enhancements (#1321)
Most of the XSLT changes are to align with the latest specification. XSLT - Warn if set-parameter matches nothing - Handle more types of children of param - Make match="control" template handle both implicit and explicit binding - New oscal:patches-to-id-targeting-ancestor function for processing needed in multiple places - For explicit binding that adds a title, add it after original title to facilitate keeping the right one in finish phase - For removal by class, expect exact match with no tokenization or case insensitivity - In finish phase, keep only last title of a control XSpec - Add tests at the level of individual templates and functions
1 parent 988817c commit d5b66f6

File tree

2 files changed

+1770
-302
lines changed

2 files changed

+1770
-302
lines changed
Lines changed: 104 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
<?xml version="1.0" encoding="UTF-8"?>
2-
<xsl:stylesheet version="2.0"
2+
<xsl:stylesheet version="3.0"
33
exclude-result-prefixes="#all"
44
xmlns="http://csrc.nist.gov/ns/oscal/1.0"
5+
xmlns:mh="http://csrc.nist.gov/ns/message"
56
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
67
xmlns:xs="http://www.w3.org/2001/XMLSchema"
7-
xmlns:math="http://www.w3.org/2005/xpath-functions/math"
88
xmlns:oscal="http://csrc.nist.gov/ns/oscal/1.0"
99
xpath-default-namespace="http://csrc.nist.gov/ns/oscal/1.0" >
1010

11-
<!-- XSLT 2.0 so as to validate against XSLT 3.0 constructs -->
11+
<xsl:import href="message-handler.xsl"/>
1212

1313
<xsl:key name="alteration-for-control-id" match="alter" use="@control-id"/>
1414
<xsl:key name="addition-by-id-ref" match="add" use="@by-id"/>
1515
<xsl:key name="parameter-setting-for-id" match="set-parameter" use="@param-id"/>
1616

17-
<xsl:variable name="oscal-ns" select="'http://csrc.nist.gov/ns/oscal'"/>
17+
<xsl:variable name="oscal-versionless-ns" select="'http://csrc.nist.gov/ns/oscal'"/>
1818

1919
<xsl:template match="node() | @*">
2020
<xsl:copy>
@@ -26,115 +26,161 @@
2626
<xsl:copy>
2727
<xsl:apply-templates select="@*"/>
2828
<xsl:apply-templates>
29+
<!-- $modifications tunnel parameter is used by templates for
30+
param, control, and descendants of control. -->
2931
<xsl:with-param name="modifications" tunnel="yes" select="child::modify"/>
3032
</xsl:apply-templates>
3133
</xsl:copy>
3234
</xsl:template>
3335

34-
<xsl:template match="modify"/>
36+
<xsl:template match="modify" as="empty-sequence()">
37+
<!-- No output is required, but invoke template to warn if set-parameter
38+
does not match anything. -->
39+
<xsl:apply-templates select="set-parameter"/>
40+
</xsl:template>
41+
42+
<xsl:template match="set-parameter" as="empty-sequence()">
43+
<xsl:if test="not(@param-id = ancestor::catalog/descendant::param/@id)">
44+
<xsl:call-template name="mh:message-handler">
45+
<xsl:with-param name="text" expand-text="yes">set-parameter with param-id=&quot;{@param-id}&quot; does not match any param id.</xsl:with-param>
46+
<xsl:with-param name="message-type">Warning</xsl:with-param>
47+
</xsl:call-template>
48+
</xsl:if>
49+
</xsl:template>
3550

3651
<!-- priority to override template match="control//*" -->
37-
<xsl:template priority="2" match="param">
52+
<xsl:template match="param" priority="2">
3853
<xsl:param name="modifications" tunnel="yes" as="element(modify)?" required="yes"/>
3954
<xsl:variable name="id" select="@id"/>
4055
<!-- depending on 'merge' behavior there could be multiple settings. combine/use-first should keep only the first setting in the first profile with such a setting. combine/merge should remove all duplicates by value. combine/keep should keep all parameter settings contents even when results are invalid. These operations are assumed to be performed in the merge phase. -->
41-
<xsl:variable name="settings" select="$modifications/key('parameter-setting-for-id',$id,.)"/>
56+
<xsl:variable name="settings" as="element(set-parameter)*"
57+
select="$modifications/key('parameter-setting-for-id',$id,.)"/>
4258
<xsl:copy>
4359
<xsl:copy-of select="@*"/>
44-
<!--param contains: label, usage, constraint, guideline, value, link
45-
set-parameter contains: label, usage, constraint, value, link-->
46-
<xsl:sequence select="($settings/label,label)[1]"/>
47-
<xsl:sequence select="usage, $settings/usage"/>
60+
<!--param contains: prop, link, label, usage, constraint, guideline, value or select, remarks
61+
set-parameter contains: prop, link, label, usage, constraint, guideline, value or select-->
62+
63+
<!-- Any prop with a uuid value replaces an earlier one with that uuid.
64+
Gather them in order in a document, and then determine which ones
65+
to suppress from output. -->
66+
<xsl:variable name="all-props" as="document-node()">
67+
<xsl:document>
68+
<xsl:sequence select="prop, $settings/prop"/>
69+
</xsl:document>
70+
</xsl:variable>
71+
<xsl:sequence select="$all-props/prop[not(@uuid = following-sibling::prop/@uuid)]"/>
72+
<xsl:sequence select="link, $settings/link"/>
73+
<xsl:sequence select="(label, $settings/label)[last()]"/>
74+
<xsl:sequence select="(usage, $settings/usage)[last()]"/>
4875
<xsl:sequence select="constraint,$settings/constraint"/>
4976
<xsl:sequence select="guideline, $settings/guideline"/>
50-
<xsl:sequence select="($settings/(select,value), select, value)[1]"/>
51-
<xsl:sequence select="link, $settings/link"/>
77+
<xsl:sequence select="(value, select, $settings/(value,select))[last()]"/>
78+
<xsl:sequence select="remarks"/>
5279
</xsl:copy>
5380
</xsl:template>
5481

82+
<!-- When matching a control, insert content as indicated by <add> and copy everything else. -->
83+
5584
<xsl:template match="control" priority="2">
5685
<xsl:param name="modifications" tunnel="yes" as="element(modify)?" required="yes"/>
5786
<!--<xsl:variable name="modifications" select="/*/modify"/>-->
5887
<xsl:variable name="id" select="@id"/>
88+
<xsl:variable name="patches-to-id-targeting-ancestor" select="oscal:patches-to-id-targeting-ancestor(., $modifications)" as="element(add)*"/>
89+
<!-- condition not(@by-id != $id) includes any addition without an @by-id, or whose @by-id is the control id -->
90+
<xsl:variable name="implicit-patches-to-id" select="$modifications/key('alteration-for-control-id',$id,.)/add[not(@by-id != $id)]" as="element(add)*"/>
5991

60-
<xsl:copy>
61-
<xsl:copy-of select="@*"/>
62-
<xsl:apply-templates select="title" mode="#current"/>
63-
<!-- condition not(@by-id != $id) includes any addition without an @by-id, or whose @by-id is the control id -->
64-
<xsl:copy-of select="$modifications/key('alteration-for-control-id',$id,.)/add[not(@by-id != $id)][@position=('before','starting')]/*"/>
65-
66-
<xsl:apply-templates select="* except title" mode="#current"/>
67-
<!--<xsl:message expand-text="true">{ string-join((* except title)/(name() || '#' || @id), ', ') }</xsl:message>-->
68-
69-
<xsl:copy-of select="$modifications/key('alteration-for-control-id',$id,.)/add[not(@by-id != $id)][not(@position = ('before','starting'))]/*"/>
70-
</xsl:copy>
92+
<xsl:copy-of select="$patches-to-id-targeting-ancestor[@position = 'before']/*"/><xsl:message>got here! removable is <xsl:sequence select="oscal:removable(./*[1],$modifications)"/></xsl:message>
93+
<xsl:if test="not(ancestor::control and oscal:removable(.,$modifications))">
94+
<xsl:copy>
95+
<xsl:copy-of select="@*"/>
96+
<xsl:apply-templates select="title" mode="#current"/>
97+
<xsl:copy-of select="(
98+
$implicit-patches-to-id[@position=('before','starting')] |
99+
$patches-to-id-targeting-ancestor[@position='starting']
100+
)/*"/>
101+
102+
<xsl:apply-templates select="* except title" mode="#current"/>
103+
<!--<xsl:message expand-text="true">{ string-join((* except title)/(name() || '#' || @id), ', ') }</xsl:message>-->
104+
105+
<xsl:copy-of select="(
106+
$implicit-patches-to-id[not(@position = ('before','starting'))] |
107+
$patches-to-id-targeting-ancestor[empty(@position) or @position='ending' or not(@position=('before','after','starting','ending'))]
108+
)/*"/><!-- TODO: Revisit after approach to #1311 is clarified -->
109+
110+
</xsl:copy>
111+
</xsl:if>
112+
<xsl:copy-of select="$patches-to-id-targeting-ancestor[@position = 'after']/*"/>
71113
</xsl:template>
72114

115+
<!-- Find <add> elements that reference the $here element by ID,
116+
from the given <modify> element. -->
117+
<xsl:function name="oscal:patches-to-id-targeting-ancestor" as="element(add)*">
118+
<xsl:param name="here" as="element()"/>
119+
<xsl:param name="modifications" as="element(modify)?"/>
120+
<xsl:variable name="controls" select="$here/ancestor-or-self::control" as="element(control)*"/>
121+
<xsl:variable name="alterations" as="element(alter)*"
122+
select="for $control in $controls return $modifications/key('alteration-for-control-id',$control/@id,.)"/>
123+
<!-- Key retrievals scoped to alterations... -->
124+
<xsl:sequence select="$alterations/key('addition-by-id-ref',$here/@id,.)"/>
125+
</xsl:function>
126+
73127
<xsl:template match="control//*">
74128
<xsl:param name="modifications" tunnel="yes" as="element(modify)?" required="yes"/>
75129
<!--<xsl:variable name="modifications" select="/*/modify"/>-->
76-
<xsl:variable name="here" select="."/>
77-
<xsl:variable name="home" select="ancestor::control[1]"/>
78-
<xsl:variable name="alterations" select="$modifications/key('alteration-for-control-id',$home/@id,.)"/>
79-
<!-- Key retrievals scoped to alterations... -->
80-
<xsl:variable name="patches-to-id" select="$alterations/key('addition-by-id-ref',$here/@id,.)"/>
130+
<xsl:variable name="patches-to-id" select="oscal:patches-to-id-targeting-ancestor(., $modifications)" as="element(add)*"/>
81131

82-
<!-- $patches-before contains 'add' elements marked as patching before this element, either by its @id
83-
or if bound by its @class, iff it is the first of its class in the containing control
84-
-->
132+
<!-- $patches-before contains 'add' elements marked as patching before this element, by its @id -->
85133
<xsl:variable name="patches-before" select="$patches-to-id[@position='before']"/>
86134

87135
<xsl:copy-of select="$patches-before/*"/>
88136
<xsl:if test="not(oscal:removable(.,$modifications))">
89137
<xsl:copy>
90138
<xsl:apply-templates select="@*" mode="#current"/>
139+
<xsl:apply-templates select="title" mode="#current"/>
91140

92141
<xsl:variable name="patches-starting" select="$patches-to-id[@position='starting']"/>
93142
<xsl:copy-of select="$patches-starting/*"/>
94143

95-
<xsl:apply-templates select="node()" mode="#current"/>
144+
<xsl:apply-templates select="node() except title" mode="#current"/>
96145

97146
<xsl:variable name="patches-ending" select="$patches-to-id[empty(@position) or @position='ending' or not(@position=('before','after','starting','ending'))]"/>
98147
<xsl:copy-of select="$patches-ending/*"/>
99148
</xsl:copy>
100149
</xsl:if>
101150

102-
<!-- Reverse logic for 'after' patches. Note that elements inside descendant subcontrols or components are excluded from consideration. -->
151+
<!-- Reverse logic for 'after' patches. -->
103152
<xsl:variable name="patches-after" select="$patches-to-id[@position='after']"/>
104153
<xsl:copy-of select="$patches-after/*"/>
105154

106155
</xsl:template>
107156

108-
<xsl:function name="oscal:classes" as="xs:string*">
109-
<xsl:param name="who" as="element()"/>
110-
<!-- HTML is not case sensitive so neither are we -->
111-
<xsl:sequence select="tokenize($who/@class/lower-case(.), '\s+')"/>
112-
</xsl:function>
113-
157+
<!-- Return true if the $who element is meant to be removed, according to $mods -->
114158
<xsl:function name="oscal:removable" as="xs:boolean">
115-
<xsl:param name="who" as="node()"/>
159+
<xsl:param name="who" as="element()"/><!-- Descendant of control -->
116160
<xsl:param name="mods" as="element(modify)?"/>
117-
<xsl:variable name="home" select="($who/ancestor::control | $who/ancestor::component)[last()]"/>
118-
<xsl:variable name="alterations" select="$mods/key('alteration-for-control-id',$home/@id,.)"/>
119-
<xsl:variable name="removals" select="$alterations/remove"/>
120-
161+
<xsl:variable name="home" as="element()+" select="$who/ancestor::control"/>
162+
<xsl:variable name="alterations" as="element(alter)*" select="$mods/key('alteration-for-control-id',$home/@id,.)"/>
163+
<xsl:variable name="removals" as="element(remove)*" select="$alterations/remove"/>
121164
<xsl:sequence select="some $r in $removals satisfies oscal:remove-match($who,$r)"/>
122165
</xsl:function>
123166

124-
<!-- <remove by-item-name="" by-id="" by-class="" by-name="" by-ns=""/>-->
125-
126-
<xsl:function name="oscal:remove-match">
127-
<xsl:param name="who" as="node()"/>
167+
<!-- For a given element $who and removal element $removal, return true if $removal indicates
168+
that $who should be removed. In practice, $who is a descendant of control. -->
169+
<!-- $removal has this form, where attributes that appear must have nonempty values:
170+
<remove by-item-name="..." by-id="..." by-class="..." by-name="..." by-ns=".."/>-->
171+
<xsl:function name="oscal:remove-match" as="xs:boolean">
172+
<xsl:param name="who" as="element()"/>
128173
<xsl:param name="removal" as="element(remove)"/>
129-
<xsl:variable name="item-okay" select="empty($removal/@by-item-name) or ($removal/@by-item-name = local-name($who))"/>
130-
<xsl:variable name="id-okay" select="empty($removal/@by-id) or ($removal/@by-id = $who/@id)"/>
131-
<xsl:variable name="name-okay" select="empty($removal/@by-name) or ($removal/@by-name/normalize-space(.) = $who/@name/normalize-space(.))"/>
132-
<xsl:variable name="ns-okay" select="empty($removal/@by-ns[not(normalize-space(.) = $oscal-ns)])
133-
or ($removal/@by-ns/normalize-space(.) = $who/@ns/normalize-space(.))"/>
134-
<xsl:variable name="oscal-ns-okay" select="empty($removal/@by-ns[normalize-space(.) = $oscal-ns])
135-
or (($who/@ns/normalize-space(.) = $oscal-ns) or empty($who/@ns))"/>
136-
<xsl:variable name="class-okay" select="empty($removal/@by-class) or ($removal/@by-class = oscal:classes($who))"/>
137-
<xsl:sequence select="exists($removal/(@by-item-name|@by-id|@by-name|@by-ns|@by-class)) and ($item-okay and $id-okay and $name-okay and $ns-okay and $oscal-ns-okay and $class-okay)"/>
174+
<xsl:variable name="item-okay" as="xs:boolean" select="empty($removal/@by-item-name) or ($removal/@by-item-name = local-name($who))"/>
175+
<xsl:variable name="id-okay" as="xs:boolean" select="empty($removal/@by-id) or ($removal/@by-id = $who/@id)"/>
176+
<xsl:variable name="name-okay" as="xs:boolean" select="empty($removal/@by-name) or ($removal/@by-name = $who/@name)"/>
177+
<xsl:variable name="ns-okay" as="xs:boolean" select="empty($removal/@by-ns[not(. = $oscal-versionless-ns)])
178+
or ($removal/@by-ns = $who/@ns/normalize-space(.))"/>
179+
<xsl:variable name="oscal-ns-okay" as="xs:boolean" select="empty($removal/@by-ns[. = $oscal-versionless-ns])
180+
or (($who/@ns/normalize-space(.) = $oscal-versionless-ns) or empty($who/@ns))"/>
181+
<xsl:variable name="class-okay" as="xs:boolean" select="empty($removal/@by-class) or ($removal/@by-class = $who/@class)"/>
182+
<xsl:variable name="removal-includes-specifier" as="xs:boolean" select="exists($removal/(@by-item-name|@by-id|@by-name|@by-ns|@by-class))"/>
183+
<xsl:sequence select="$removal-includes-specifier and ($item-okay and $id-okay and $name-okay and $ns-okay and $oscal-ns-okay and $class-okay)"/>
138184
</xsl:function>
139185

140186
</xsl:stylesheet>

0 commit comments

Comments
 (0)