XsdDocumentedTests groovy->java

Groovy has more extensive support for Xml parsing via XmlSlurper.
To replace it, this conversion also introduces a SAX wrapper,
NicerXmlParser, and a companion Node wrapper, NicerNode, that
allowed for less modification of the converted tests.

Issue: gh-4939
This commit is contained in:
Josh Cummings 2018-03-26 10:31:03 -06:00 committed by Rob Winch
parent fb7394c1de
commit 0c0abea3ad
12 changed files with 951 additions and 522 deletions

View File

@ -1,33 +0,0 @@
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.springframework.security.config.doc
/**
* Represents a Spring Security XSD Attribute. It is created when parsing the current xsd to compare to the documented appendix.
*
* @author Rob Winch
* @see SpringSecurityXsdParser
* @see XsdDocumentedSpec
*/
class Attribute {
def name
def desc
def elmt
def getId() {
return "${elmt.id}-${name}".toString()
}
}

View File

@ -1,90 +0,0 @@
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.springframework.security.config.doc
/**
* Represents a Spring Security XSD Element. It is created when parsing the current xsd to compare to the documented appendix.
*
* @author Rob Winch
* @see SpringSecurityXsdParser
* @see XsdDocumentedSpec
*/
class Element {
def name
def desc
def attrs
/**
* Contains the elements that extend this element (i.e. any-user-service contains ldap-user-service)
*/
def subGrps = []
def childElmts = [:]
def parentElmts = [:]
def getId() {
return "nsa-${name}".toString()
}
/**
* Gets all the ids related to this Element including attributes, parent elements, and child elements.
*
* <p>
* The expected ids to be found are documented below.
* <ul>
* <li>Elements - any xml element will have the nsa-&lt;element&gt;. For example the http element will have the id
* nsa-http</li>
* <li>Parent Section - Any element with a parent other than beans will have a section named
* nsa-&lt;element&gt;-parents. For example, authentication-provider would have a section id of
* nsa-authentication-provider-parents. The section would then contain a list of links pointing to the
* documentation for each parent element.</li>
* <li>Attributes Section - Any element with attributes will have a section with the id
* nsa-&lt;element&gt;-attributes. For example the http element would require a section with the id
* http-attributes.</li>
* <li>Attribute - Each attribute of an element would have an id of nsa-&lt;element&gt;-&lt;attributeName&gt;. For
* example the attribute create-session for the http attribute would have the id http-create-session.</li>
* <li>Child Section - Any element with a child element will have a section named nsa-&lt;element&gt;-children.
* For example, authentication-provider would have a section id of nsa-authentication-provider-children. The
* section would then contain a list of links pointing to the documentation for each child element.</li>
* </ul>
* @return
*/
def getIds() {
def ids = [id]
childElmts.values()*.ids.each { ids.addAll it }
attrs*.id.each { ids.add it }
if(childElmts) {
ids.add id+'-children'
}
if(attrs) {
ids.add id+'-attributes'
}
if(parentElmts) {
ids.add id+'-parents'
}
ids
}
def getAllChildElmts() {
def result = [:]
childElmts.values()*.subGrps*.each { elmt -> result.put(elmt.name,elmt) }
result + childElmts
}
def getAllParentElmts() {
def result = [:]
parentElmts.values()*.subGrps*.each { elmt -> result.put(elmt.name,elmt) }
result + parentElmts
}
}

View File

@ -1,177 +0,0 @@
/*
* Copyright 2002-2011 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.springframework.security.config.doc
import groovy.xml.Namespace
/**
* Parses the Spring Security Xsd Document
*
* @author Rob Winch
*/
class SpringSecurityXsdParser {
private def rootElement
private def xs = new Namespace("http://www.w3.org/2001/XMLSchema", 'xs')
private def attrElmts = [] as Set
private def elementNameToElement = [:] as Map
/**
* Returns a map of the element name to the {@link Element}.
* @return
*/
Map<String,Element> parse() {
elements(rootElement)
elementNameToElement
}
/**
* Creates a Map of the name to an Element object of all the children of element.
*
* @param element
* @return
*/
private def elements(element) {
def elementNameToElement = [:] as Map
element.children().each { c->
if(c.name() == 'element') {
def e = elmt(c)
elementNameToElement.put(e.name,e)
} else {
elementNameToElement.putAll(elements(c))
}
}
elementNameToElement
}
/**
* Any children that are attribute will be returned as an Attribute object.
* @param element
* @return a collection of Attribute objects that are children of element.
*/
private def attrs(element) {
def r = []
element.children().each { c->
if(c.name() == 'attribute') {
r.add(attr(c))
}else if(c.name() == 'element') {
}else {
r.addAll(attrs(c))
}
}
r
}
/**
* Any children will be searched for an attributeGroup, each of it's children will be returned as an Attribute
* @param element
* @return
*/
private def attrgrps(element) {
def r = []
element.children().each { c->
if(c.name() == 'element') {
}else if (c.name() == 'attributeGroup') {
if(c.attributes().get('name')) {
r.addAll(attrgrp(c))
} else {
def n = c.attributes().get('ref').split(':')[1]
def attrGrp = findNode(element,n)
r.addAll(attrgrp(attrGrp))
}
} else {
r.addAll(attrgrps(c))
}
}
r
}
private def findNode(c,name) {
def root = c
while(root.name() != 'schema') {
root = root.parent()
}
def result = root.breadthFirst().find { child-> name == child.@name?.text() }
assert result?.@name?.text() == name
result
}
/**
* Processes an individual attributeGroup by obtaining all the attributes and then looking for more attributeGroup elements and prcessing them.
* @param e
* @return all the attributes for a specific attributeGroup and any child attributeGroups
*/
private def attrgrp(e) {
def attrs = attrs(e)
attrs.addAll(attrgrps(e))
attrs
}
/**
* Obtains the description for a specific element
* @param element
* @return
*/
private def desc(element) {
return element['annotation']['documentation']
}
/**
* Given an element creates an attribute from it.
* @param n
* @return
*/
private def attr(n) {
new Attribute(desc: desc(n), name: n.@name.text())
}
/**
* Given an element creates an Element out of it by collecting all its attributes and child elements.
*
* @param n
* @return
*/
private def elmt(n) {
def name = n.@ref.text()
if(name) {
name = name.split(':')[1]
n = findNode(n,name)
} else {
name = n.@name.text()
}
if(elementNameToElement.containsKey(name)) {
return elementNameToElement.get(name)
}
attrElmts.add(name)
def e = new Element()
e.name = n.@name.text()
e.desc = desc(n)
e.childElmts = elements(n)
e.attrs = attrs(n)
e.attrs.addAll(attrgrps(n))
e.attrs*.elmt = e
e.childElmts.values()*.each { it.parentElmts.put(e.name,e) }
def subGrpName = n.@substitutionGroup.text()
if(subGrpName) {
def subGrp = elmt(findNode(n,subGrpName.split(":")[1]))
subGrp.subGrps.add(e)
}
elementNameToElement.put(name,e)
e
}
}

View File

@ -1,222 +0,0 @@
/*
* Copyright 2011-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.springframework.security.config.doc
import groovy.util.slurpersupport.GPathResult
import spock.lang.*
import org.springframework.security.config.http.SecurityFilters
/**
* Tests to ensure that the xsd is properly documented.
*
* @author Rob Winch
*/
class XsdDocumentedTests extends Specification {
def ignoredIds = [
'nsa-any-user-service',
'nsa-any-user-service-parents',
'nsa-authentication',
'nsa-websocket-security',
'nsa-ldap',
'nsa-method-security',
'nsa-web'
]
@Shared def reference = new File('../docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc')
@Shared File schema31xDocument = new File('src/main/resources/org/springframework/security/config/spring-security-3.1.xsd')
@Shared File schemaDocument = new File('src/main/resources/org/springframework/security/config/spring-security-5.0.xsd')
@Shared Map<String,Element> elementNameToElement
@Shared GPathResult schemaRootElement
def setupSpec() {
schemaRootElement = new XmlSlurper().parse(schemaDocument)
elementNameToElement = new SpringSecurityXsdParser(rootElement: schemaRootElement).parse()
}
def cleanupSpec() {
reference = null
schema31xDocument = null
schemaDocument = null
elementNameToElement = null
schemaRootElement = null
}
def 'SEC-2139: named-security-filter are all defined and ordered properly'() {
setup:
def expectedFilters = (EnumSet.allOf(SecurityFilters) as List).sort { it.order }
when:
def nsf = schemaRootElement.simpleType.find { it.@name == 'named-security-filter' }
def nsfValues = nsf.children().children().collect { c ->
Enum.valueOf(SecurityFilters, c.@value.toString())
}
then:
expectedFilters == nsfValues
}
def 'SEC-2139: 3.1.x named-security-filter are all defined and ordered properly'() {
setup:
def expectedFilters = [
"FIRST",
"CHANNEL_FILTER",
"SECURITY_CONTEXT_FILTER",
"CONCURRENT_SESSION_FILTER",
"LOGOUT_FILTER",
"X509_FILTER",
"PRE_AUTH_FILTER",
"CAS_FILTER",
"FORM_LOGIN_FILTER",
"OPENID_FILTER",
"LOGIN_PAGE_FILTER",
"DIGEST_AUTH_FILTER",
"BASIC_AUTH_FILTER",
"REQUEST_CACHE_FILTER",
"SERVLET_API_SUPPORT_FILTER",
"JAAS_API_SUPPORT_FILTER",
"REMEMBER_ME_FILTER",
"ANONYMOUS_FILTER",
"SESSION_MANAGEMENT_FILTER",
"EXCEPTION_TRANSLATION_FILTER",
"FILTER_SECURITY_INTERCEPTOR",
"SWITCH_USER_FILTER",
"LAST"
].collect {
Enum.valueOf(SecurityFilters, it)
}
def schema31xRootElement = new XmlSlurper().parse(schema31xDocument)
when:
def nsf = schema31xRootElement.simpleType.find { it.@name == 'named-security-filter' }
def nsfValues = nsf.children().children().collect { c ->
Enum.valueOf(SecurityFilters, c.@value.toString())
}
then:
expectedFilters == nsfValues
}
/**
* This will check to ensure that the expected number of xsd documents are found to ensure that we are validating
* against the current xsd document. If this test fails, all that is needed is to update the schemaDocument
* and the expected size for this test.
* @return
*/
def 'the latest schema is being validated'() {
when: 'all the schemas are found'
def schemas = schemaDocument.getParentFile().list().findAll { it.endsWith('.xsd') }
then: 'the count is equal to 12, if not then schemaDocument needs updated'
schemas.size() == 12
}
/**
* This uses a naming convention for the ids of the appendix to ensure that the entire appendix is documented.
* The naming convention for the ids is documented in {@link Element#getIds()}.
* @return
*/
def 'the entire schema is included in the appendix documentation'() {
setup: 'get all the documented ids and the expected ids'
def documentedIds = []
reference.eachLine { line ->
if(line.matches("\\[\\[(nsa-.*)\\]\\]")) {
documentedIds.add(line.substring(2,line.length() - 2))
}
}
when: 'the schema is compared to the appendix documentation'
def expectedIds = [] as Set
elementNameToElement*.value*.ids*.each { expectedIds.addAll it }
documentedIds.removeAll ignoredIds
expectedIds.removeAll ignoredIds
def undocumentedIds = (expectedIds - documentedIds)
def shouldNotBeDocumented = (documentedIds - expectedIds)
then: 'all the elements and attributes are documented'
shouldNotBeDocumented.empty
undocumentedIds.empty
}
/**
* This test ensures that any element that has children or parents contains a section that has links pointing to that
* documentation.
* @return
*/
def 'validate parents and children are linked in the appendix documentation'() {
when: "get all the links for each element's children and parents"
def docAttrNameToChildren = [:]
def docAttrNameToParents = [:]
def currentDocAttrNameToElmt
def docAttrName
reference.eachLine { line ->
if(line.matches('^\\[\\[.*\\]\\]$')) {
def id = line.substring(2,line.length() - 2)
if(id.endsWith("-children")) {
docAttrName = id.substring(0,id.length() - 9)
currentDocAttrNameToElmt = docAttrNameToChildren
} else if(id.endsWith("-parents")) {
docAttrName = id.substring(0,id.length() - 8)
currentDocAttrNameToElmt = docAttrNameToParents
} else if(docAttrName && !id.startsWith(docAttrName)) {
currentDocAttrNameToElmt = null
docAttrName = null
}
}
if(docAttrName) {
def expression = '^\\* <<(nsa-.*),.*>>$'
if(line.matches(expression)) {
String elmtId = line.replaceAll(expression, '$1')
currentDocAttrNameToElmt.get(docAttrName, []).add(elmtId)
}
}
}
def schemaAttrNameToParents = [:]
def schemaAttrNameToChildren = [:]
elementNameToElement.each { entry ->
def key = 'nsa-'+entry.key
if(ignoredIds.contains(key)) {
return
}
def parentIds = entry.value.allParentElmts.values()*.id.findAll { !ignoredIds.contains(it) }.sort()
if(parentIds) {
schemaAttrNameToParents.put(key,parentIds)
}
def childIds = entry.value.allChildElmts.values()*.id.findAll { !ignoredIds.contains(it) }.sort()
if(childIds) {
schemaAttrNameToChildren.put(key,childIds)
}
}
then: "the expected parents and children are all documented"
schemaAttrNameToChildren.sort() == docAttrNameToChildren.sort()
schemaAttrNameToParents.sort() == docAttrNameToParents.sort()
}
/**
* This test checks each xsd element and ensures there is documentation for it.
* @return
*/
def 'entire xsd is documented'() {
when: "validate that the entire xsd contains documentation"
def notDocElmtIds = elementNameToElement.values().findAll {
!it.desc.text() && !ignoredIds.contains(it.id)
}*.id.sort().join("\n")
def notDocAttrIds = elementNameToElement.values()*.attrs.flatten().findAll {
!it.desc.text() && !ignoredIds.contains(it.id)
}*.id.sort().join("\n")
then: "all the elements and attributes have some documentation"
!notDocElmtIds
!notDocAttrIds
}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.doc;
/**
* Represents a Spring Security XSD Attribute. It is created when parsing the current xsd to compare to the documented appendix.
*
* @author Rob Winch
* @author Josh Cummings
*
* @see SpringSecurityXsdParser
* @see XsdDocumentedTests
*/
public class Attribute {
private String name;
private String desc;
private Element elmt;
public Attribute(String desc, String name) {
this.desc = desc;
this.name = name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return this.desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public Element getElmt() {
return this.elmt;
}
public void setElmt(Element elmt) {
this.elmt = elmt;
}
public String getId() {
return String.format("%s-%s", this.elmt.getId(), this.name);
}
}

View File

@ -0,0 +1,169 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.doc;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* Represents a Spring Security XSD Element. It is created when parsing
* the current xsd to compare to the documented appendix.
*
* @author Rob Winch
* @author Josh Cummings
*
* @see SpringSecurityXsdParser
* @see XsdDocumentedTests
*/
public class Element {
private String name;
private String desc;
private Collection<Attribute> attrs = new ArrayList<>();
/**
* Contains the elements that extend this element (i.e. any-user-service contains ldap-user-service)
*/
private Collection<Element> subGrps = new ArrayList<>();
private Map<String, Element> childElmts = new HashMap<>();
private Map<String, Element> parentElmts = new HashMap<>();
public String getId() {
return String.format("nsa-%s", this.name);
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getDesc() {
return this.desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
public Collection<Attribute> getAttrs() {
return this.attrs;
}
public void setAttrs(Collection<Attribute> attrs) {
this.attrs = attrs;
}
public Collection<Element> getSubGrps() {
return this.subGrps;
}
public void setSubGrps(Collection<Element> subGrps) {
this.subGrps = subGrps;
}
public Map<String, Element> getChildElmts() {
return this.childElmts;
}
public void setChildElmts(Map<String, Element> childElmts) {
this.childElmts = childElmts;
}
public Map<String, Element> getParentElmts() {
return this.parentElmts;
}
public void setParentElmts(Map<String, Element> parentElmts) {
this.parentElmts = parentElmts;
}
/**
* Gets all the ids related to this Element including attributes, parent elements, and child elements.
*
* <p>
* The expected ids to be found are documented below.
* <ul>
* <li>Elements - any xml element will have the nsa-&lt;element&gt;. For example the http element will have the id
* nsa-http</li>
* <li>Parent Section - Any element with a parent other than beans will have a section named
* nsa-&lt;element&gt;-parents. For example, authentication-provider would have a section id of
* nsa-authentication-provider-parents. The section would then contain a list of links pointing to the
* documentation for each parent element.</li>
* <li>Attributes Section - Any element with attributes will have a section with the id
* nsa-&lt;element&gt;-attributes. For example the http element would require a section with the id
* http-attributes.</li>
* <li>Attribute - Each attribute of an element would have an id of nsa-&lt;element&gt;-&lt;attributeName&gt;. For
* example the attribute create-session for the http attribute would have the id http-create-session.</li>
* <li>Child Section - Any element with a child element will have a section named nsa-&lt;element&gt;-children.
* For example, authentication-provider would have a section id of nsa-authentication-provider-children. The
* section would then contain a list of links pointing to the documentation for each child element.</li>
* </ul>
* @return
*/
public Collection<String> getIds() {
Collection<String> ids = new ArrayList<>();
ids.add(getId());
this.childElmts.values()
.forEach(elmt -> ids.add(elmt.getId()));
this.attrs.forEach(attr -> ids.add(attr.getId()));
if ( !this.childElmts.isEmpty() ) {
ids.add(getId() + "-children");
}
if ( !this.attrs.isEmpty() ) {
ids.add(getId() + "-attributes");
}
if ( !this.parentElmts.isEmpty() ) {
ids.add(getId() + "-parents");
}
return ids;
}
public Map<String, Element> getAllChildElmts() {
Map<String, Element> result = new HashMap<>();
this.childElmts.values()
.forEach(elmt ->
elmt.subGrps.forEach(
subElmt -> result.put(subElmt.name, subElmt)));
result.putAll(this.childElmts);
return result;
}
public Map<String, Element> getAllParentElmts() {
Map<String, Element> result = new HashMap<>();
this.parentElmts.values()
.forEach(elmt ->
elmt.subGrps.forEach(
subElmt -> result.put(subElmt.name, subElmt)));
result.putAll(this.parentElmts);
return result;
}
}

View File

@ -0,0 +1,73 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.doc;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import java.util.Optional;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
* @author Josh Cummings
*/
public class NicerNode {
private final Node node;
public NicerNode(Node node) {
this.node = node;
}
public String simpleName() {
String[] parts = this.node.getNodeName().split(":");
return parts[parts.length-1];
}
public String text() {
return this.node.getTextContent();
}
public Stream<NicerNode> children() {
NodeList children = this.node.getChildNodes();
return IntStream.range(0, children.getLength())
.mapToObj(children::item)
.map(NicerNode::new);
}
public Optional<NicerNode> child(String name) {
return this.children()
.filter(child -> name.equals(child.simpleName()))
.findFirst();
}
public Optional<NicerNode> parent() {
return Optional.ofNullable(this.node.getParentNode())
.map(parent -> new NicerNode(parent));
}
public String attribute(String name) {
return Optional.ofNullable(this.node.getAttributes())
.map(attrs -> attrs.getNamedItem(name))
.map(attr -> attr.getTextContent())
.orElse(null);
}
public Node node() {
return this.node;
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.doc;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.io.InputStream;
/**
* @author Josh Cummings
*/
public class NicerXmlParser implements AutoCloseable {
private InputStream xml;
public NicerXmlParser(InputStream xml) {
this.xml = xml;
}
public NicerNode parse() {
try {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
return new NicerNode(dBuilder.parse(this.xml));
} catch ( IOException | ParserConfigurationException | SAXException e ) {
throw new IllegalStateException(e);
}
}
@Override
public void close() throws IOException {
this.xml.close();
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.doc;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.util.Map;
/**
* Support for ensuring preparing the givens in {@link XsdDocumentedTests}
*
* @author Josh Cummings
*/
public class NicerXmlSupport {
private NicerXmlParser parser;
public NicerNode parse(String location) throws IOException {
ClassPathResource resource = new ClassPathResource(location);
this.parser = new NicerXmlParser(resource.getInputStream());
return this.parser.parse();
}
public Map<String, Element> elementsByElementName(String location) throws IOException {
NicerNode node = parse(location);
return new SpringSecurityXsdParser(node).parse();
}
public void close() throws IOException {
if ( this.parser != null ) {
this.parser.close();
}
}
}

View File

@ -0,0 +1,211 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.doc;
import org.springframework.util.StringUtils;
import java.util.*;
import java.util.stream.Stream;
/**
* Parses the Spring Security Xsd Document
*
* @author Rob Winch
* @author Josh Cummings
*/
public class SpringSecurityXsdParser {
private NicerNode rootElement;
private Set<String> attrElmts = new LinkedHashSet<>();
private Map<String, Element> elementNameToElement = new HashMap<>();
public SpringSecurityXsdParser(NicerNode rootElement) {
this.rootElement = rootElement;
}
/**
* Returns a map of the element name to the {@link Element}.
*
* @return
*/
public Map<String, Element> parse() {
elements(this.rootElement);
return this.elementNameToElement;
}
/**
* Creates a Map of the name to an Element object of all the children of element.
*
* @param node
* @return
*/
private Map<String, Element> elements(NicerNode node) {
Map<String, Element> elementNameToElement = new HashMap<>();
node.children().forEach(child -> {
if ("element".equals(child.simpleName())) {
Element e = elmt(child);
elementNameToElement.put(e.getName(), e);
} else {
elementNameToElement.putAll(elements(child));
}
});
return elementNameToElement;
}
/**
* Any children that are attribute will be returned as an Attribute object.
*
* @param element
* @return a collection of Attribute objects that are children of element.
*/
private Collection<Attribute> attrs(NicerNode element) {
Collection<Attribute> attrs = new ArrayList<>();
element.children().forEach(c -> {
String name = c.simpleName();
if ("attribute".equals(name)) {
attrs.add(attr(c));
} else if ("element".equals(name)) {
} else {
attrs.addAll(attrs(c));
}
});
return attrs;
}
/**
* Any children will be searched for an attributeGroup, each of its children will be returned as an Attribute
*
* @param element
* @return
*/
private Collection<Attribute> attrgrps(NicerNode element) {
Collection<Attribute> attrgrp = new ArrayList<>();
element.children().forEach(c -> {
if ("element".equals(c.simpleName())) {
} else if ("attributeGroup".equals(c.simpleName())) {
if (c.attribute("name") != null) {
attrgrp.addAll(attrgrp(c));
} else {
String name = c.attribute("ref").split(":")[1];
NicerNode attrGrp = findNode(element, name);
attrgrp.addAll(attrgrp(attrGrp));
}
} else {
attrgrp.addAll(attrgrps(c));
}
});
return attrgrp;
}
private NicerNode findNode(NicerNode c, String name) {
NicerNode root = c;
while (!"schema".equals(root.simpleName())) {
root = root.parent().get();
}
return expand(root)
.filter(node -> name.equals(node.attribute("name")))
.findFirst().orElseThrow(IllegalArgumentException::new);
}
private Stream<NicerNode> expand(NicerNode root) {
return Stream.concat(
Stream.of(root),
root.children().flatMap(this::expand));
}
/**
* Processes an individual attributeGroup by obtaining all the attributes and then looking for more attributeGroup elements and prcessing them.
*
* @param e
* @return all the attributes for a specific attributeGroup and any child attributeGroups
*/
private Collection<Attribute> attrgrp(NicerNode e) {
Collection<Attribute> attrs = attrs(e);
attrs.addAll(attrgrps(e));
return attrs;
}
/**
* Obtains the description for a specific element
*
* @param element
* @return
*/
private String desc(NicerNode element) {
return element.child("annotation")
.flatMap(annotation -> annotation.child("documentation"))
.map(documentation -> documentation.text())
.orElse(null);
}
/**
* Given an element creates an attribute from it.
*
* @param n
* @return
*/
private Attribute attr(NicerNode n) {
return new Attribute(desc(n), n.attribute("name"));
}
/**
* Given an element creates an Element out of it by collecting all its attributes and child elements.
*
* @param n
* @return
*/
private Element elmt(NicerNode n) {
String name = n.attribute("ref");
if (StringUtils.isEmpty(name)) {
name = n.attribute("name");
} else {
name = name.split(":")[1];
n = findNode(n, name);
}
if (this.elementNameToElement.containsKey(name)) {
return this.elementNameToElement.get(name);
}
this.attrElmts.add(name);
Element e = new Element();
e.setName(n.attribute("name"));
e.setDesc(desc(n));
e.setChildElmts(elements(n));
e.setAttrs(attrs(n));
e.getAttrs().addAll(attrgrps(n));
e.getAttrs().forEach(attr -> attr.setElmt(e));
e.getChildElmts().values().forEach(element ->
element.getParentElmts().put(e.getName(), e));
String subGrpName = n.attribute("substitutionGroup");
if (!StringUtils.isEmpty(subGrpName)) {
Element subGrp = elmt(findNode(n, subGrpName.split(":")[1]));
subGrp.getSubGrps().add(e);
}
this.elementNameToElement.put(name, e);
return e;
}
}

View File

@ -0,0 +1,293 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.doc;
import org.apache.commons.lang.StringUtils;
import org.junit.After;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.config.http.SecurityFiltersAssertions;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests to ensure that the xsd is properly documented.
*
* @author Rob Winch
* @author Josh Cummings
*/
public class XsdDocumentedTests {
Collection<String> ignoredIds = Arrays.asList(
"nsa-any-user-service",
"nsa-any-user-service-parents",
"nsa-authentication",
"nsa-websocket-security",
"nsa-ldap",
"nsa-method-security",
"nsa-web");
String referenceLocation = "../docs/manual/src/docs/asciidoc/_includes/appendix/namespace.adoc";
String schema31xDocumentLocation = "org/springframework/security/config/spring-security-3.1.xsd";
String schemaDocumentLocation = "org/springframework/security/config/spring-security-5.0.xsd";
NicerXmlSupport xml = new NicerXmlSupport();
@After
public void close() throws IOException {
this.xml.close();
}
@Test
public void parseWhenLatestXsdThenAllNamedSecurityFiltersAreDefinedAndOrderedProperly()
throws IOException {
NicerNode root = this.xml.parse(this.schemaDocumentLocation);
List<String> nodes =
root.child("schema")
.map(NicerNode::children)
.orElse(Stream.empty())
.filter(node ->
"simpleType".equals(node.simpleName()) &&
"named-security-filter".equals(node.attribute("name")))
.flatMap(NicerNode::children)
.flatMap(NicerNode::children)
.map(node -> node.attribute("value"))
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
SecurityFiltersAssertions.assertEquals(nodes);
}
@Test
public void parseWhen31XsdThenAllNamedSecurityFiltersAreDefinedAndOrderedProperly()
throws IOException {
List<String> expected = Arrays.asList(
"FIRST",
"CHANNEL_FILTER",
"SECURITY_CONTEXT_FILTER",
"CONCURRENT_SESSION_FILTER",
"LOGOUT_FILTER",
"X509_FILTER",
"PRE_AUTH_FILTER",
"CAS_FILTER",
"FORM_LOGIN_FILTER",
"OPENID_FILTER",
"LOGIN_PAGE_FILTER",
"DIGEST_AUTH_FILTER",
"BASIC_AUTH_FILTER",
"REQUEST_CACHE_FILTER",
"SERVLET_API_SUPPORT_FILTER",
"JAAS_API_SUPPORT_FILTER",
"REMEMBER_ME_FILTER",
"ANONYMOUS_FILTER",
"SESSION_MANAGEMENT_FILTER",
"EXCEPTION_TRANSLATION_FILTER",
"FILTER_SECURITY_INTERCEPTOR",
"SWITCH_USER_FILTER",
"LAST"
);
NicerNode root = this.xml.parse(this.schema31xDocumentLocation);
List<String> nodes =
root.child("schema")
.map(NicerNode::children)
.orElse(Stream.empty())
.filter(node ->
"simpleType".equals(node.simpleName()) &&
"named-security-filter".equals(node.attribute("name")))
.flatMap(NicerNode::children)
.flatMap(NicerNode::children)
.map(node -> node.attribute("value"))
.filter(StringUtils::isNotEmpty)
.collect(Collectors.toList());
assertThat(nodes).isEqualTo(expected);
}
/**
* This will check to ensure that the expected number of xsd documents are found to ensure that we are validating
* against the current xsd document. If this test fails, all that is needed is to update the schemaDocument
* and the expected size for this test.
* @return
*/
@Test
public void sizeWhenReadingFilesystemThenIsCorrectNumberOfSchemaFiles()
throws IOException {
ClassPathResource resource = new ClassPathResource(this.schemaDocumentLocation);
String[] schemas = resource.getFile().getParentFile().list((dir, name) -> name.endsWith(".xsd"));
assertThat(schemas.length).isEqualTo(12)
.withFailMessage("the count is equal to 12, if not then schemaDocument needs updating");
}
/**
* This uses a naming convention for the ids of the appendix to ensure that the entire appendix is documented.
* The naming convention for the ids is documented in {@link Element#getIds()}.
* @return
*/
@Test
public void countReferencesWhenReviewingDocumentationThenEntireSchemaIsIncluded()
throws IOException {
Map<String, Element> elementsByElementName =
this.xml.elementsByElementName(this.schemaDocumentLocation);
List<String> documentIds =
Files.lines(Paths.get(this.referenceLocation))
.filter(line -> line.matches("\\[\\[(nsa-.*)\\]\\]"))
.map(line -> line.substring(2, line.length() - 2))
.collect(Collectors.toList());
Set<String> expectedIds =
elementsByElementName.values().stream()
.flatMap(element -> element.getIds().stream())
.collect(Collectors.toSet());
documentIds.removeAll(this.ignoredIds);
expectedIds.removeAll(this.ignoredIds);
assertThat(documentIds).containsAll(expectedIds);
assertThat(expectedIds).containsAll(documentIds);
}
/**
* This test ensures that any element that has children or parents contains a section that has links pointing to that
* documentation.
* @return
*/
@Test
public void countLinksWhenReviewingDocumentationThenParentsAndChildrenAreCorrectlyLinked()
throws IOException {
Map<String, List<String>> docAttrNameToChildren = new HashMap<>();
Map<String, List<String>> docAttrNameToParents = new HashMap<>();
String docAttrName = null;
Map<String, List<String>> currentDocAttrNameToElmt = null;
List<String> lines = Files.readAllLines(Paths.get(this.referenceLocation));
for ( String line : lines ) {
if(line.matches("^\\[\\[.*\\]\\]$")) {
String id = line.substring(2, line.length() - 2);
if(id.endsWith("-children")) {
docAttrName = id.substring(0, id.length() - 9);
currentDocAttrNameToElmt = docAttrNameToChildren;
} else if(id.endsWith("-parents")) {
docAttrName = id.substring(0, id.length() - 8);
currentDocAttrNameToElmt = docAttrNameToParents;
} else if(docAttrName != null && !id.startsWith(docAttrName)) {
currentDocAttrNameToElmt = null;
docAttrName = null;
}
}
if(docAttrName != null && currentDocAttrNameToElmt != null) {
String expression = "^\\* <<(nsa-.*),.*>>$";
if(line.matches(expression)) {
String elmtId = line.replaceAll(expression, "$1");
currentDocAttrNameToElmt
.computeIfAbsent(docAttrName, key -> new ArrayList<>())
.add(elmtId);
}
}
}
Map<String, Element> elementNameToElement = this.xml.elementsByElementName(this.schemaDocumentLocation);
Map<String, List<String>> schemaAttrNameToChildren = new HashMap<>();
Map<String, List<String>> schemaAttrNameToParents = new HashMap<>();
elementNameToElement.entrySet().stream()
.forEach(entry -> {
String key = "nsa-" + entry.getKey();
if (this.ignoredIds.contains(key) ) {
return;
}
List<String> parentIds =
entry.getValue().getAllParentElmts().values().stream()
.filter(element -> !this.ignoredIds.contains(element.getId()))
.map(element -> element.getId())
.sorted()
.collect(Collectors.toList());
if ( !parentIds.isEmpty() ) {
schemaAttrNameToParents.put(key, parentIds);
}
List<String> childIds =
entry.getValue().getAllChildElmts().values().stream()
.filter(element -> !this.ignoredIds.contains(element.getId()))
.map(element -> element.getId())
.sorted()
.collect(Collectors.toList());
if ( !childIds.isEmpty() ) {
schemaAttrNameToChildren.put(key, childIds);
}
});
assertThat(docAttrNameToChildren).isEqualTo(schemaAttrNameToChildren);
assertThat(docAttrNameToParents).isEqualTo(schemaAttrNameToParents);
}
/**
* This test checks each xsd element and ensures there is documentation for it.
* @return
*/
@Test
public void countWhenReviewingDocumentationThenAllElementsDocumented()
throws IOException {
Map<String, Element> elementNameToElement =
this.xml.elementsByElementName(this.schemaDocumentLocation);
String notDocElmtIds =
elementNameToElement.values().stream()
.filter(element ->
StringUtils.isEmpty(element.getDesc()) &&
!this.ignoredIds.contains(element.getId()))
.map(element -> element.getId())
.sorted()
.collect(Collectors.joining("\n"));
String notDocAttrIds =
elementNameToElement.values().stream()
.flatMap(element -> element.getAttrs().stream())
.filter(element ->
StringUtils.isEmpty(element.getDesc()) &&
!this.ignoredIds.contains(element.getId()))
.map(element -> element.getId())
.sorted()
.collect(Collectors.joining("\n"));
assertThat(notDocElmtIds).isEmpty();
assertThat(notDocAttrIds).isEmpty();
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.http;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Assertions for tests that rely on confirming behavior of the package-private SecurityFilters enum
*
* @author Josh Cummings
*/
public class SecurityFiltersAssertions {
private static Collection<SecurityFilters> ordered = Arrays.asList(SecurityFilters.values());
public static void assertEquals(List<String> filters) {
List<String> expected = ordered.stream()
.map(SecurityFilters::name)
.collect(Collectors.toList());
assertThat(filters).isEqualTo(expected);
}
}