diff --git a/config/config.gradle b/config/config.gradle
index 0b5b43475a..fd9a9f0f4d 100644
--- a/config/config.gradle
+++ b/config/config.gradle
@@ -44,6 +44,10 @@ dependencies {
"cglib:cglib-nodep:2.2"
}
+test {
+ inputs.file file("$rootDir/docs/manual/src/docbook/appendix-namespace.xml")
+}
+
integrationTest {
systemProperties['apacheDSWorkDir'] = "${buildDir}/apacheDSWork"
}
diff --git a/config/src/test/groovy/org/springframework/security/config/doc/Attribute.groovy b/config/src/test/groovy/org/springframework/security/config/doc/Attribute.groovy
new file mode 100644
index 0000000000..398e543d69
--- /dev/null
+++ b/config/src/test/groovy/org/springframework/security/config/doc/Attribute.groovy
@@ -0,0 +1,49 @@
+/*
+ * Copyright 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 prefix = ""
+ def name
+ def desc
+ def elmt
+
+ def indent() {
+ prefix += " "
+ }
+
+ def toDocbook(prefix) {
+ def indent = " "+prefix
+ """
+${prefix}
+${indent}${name}
+${indent}${desc}
+${prefix}"""
+ }
+ def getId() {
+ return "${elmt.id}-${name}".toString()
+ }
+ public String toString() {
+ prefix + '@' + name + " - " + desc
+ }
+}
diff --git a/config/src/test/groovy/org/springframework/security/config/doc/Element.groovy b/config/src/test/groovy/org/springframework/security/config/doc/Element.groovy
new file mode 100644
index 0000000000..95120da58c
--- /dev/null
+++ b/config/src/test/groovy/org/springframework/security/config/doc/Element.groovy
@@ -0,0 +1,164 @@
+/*
+ * Copyright 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 prefix = ""
+ 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 indent() {
+ prefix += " "
+ attrs*.indent()
+ childElmts.values()*.indent()
+ }
+
+ def getId() {
+ return "nsa-${name}".toString()
+ }
+
+ /**
+ * Gets all the ids related to this Element including attributes, parent elements, and child elements.
+ *
+ *
+ * The expected ids to be found are documented below.
+ *
+ *
Elements - any xml element will have the nsa-<element>. For example the http element will have the id
+ * nsa-http
+ *
Parent Section - Any element with a parent other than beans will have a section named
+ * nsa-<element>-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.
+ *
Attributes Section - Any element with attributes will have a section with the id
+ * nsa-<element>-attributes. For example the http element would require a section with the id
+ * http-attributes.
+ *
Attribute - Each attribute of an element would have an id of nsa-<element>-<attributeName>. For
+ * example the attribute create-session for the http attribute would have the id http-create-session.
+ *
Child Section - Any element with a child element will have a section named nsa-<element>-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.
+ *
+ * @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 getFullName() {
+ parentElmt ? parentElmt.fullName+"-"+name : name
+ }
+
+ def docbookParentElmts(prefix) {
+ if(parentElmts.empty) {
+ return ''
+ }
+ def indent = prefix+' '
+ def parents = """
+${prefix}
+${indent}Parent Elements of <${name}>
+${indent}"""
+ parentElmts.sort {l,r -> l.name.compareTo(r.name)}.each {
+ parents += """\n${indent} ${it.name}"""
+ }
+ parents += "\n${indent}\n${prefix}"
+ parents
+ }
+
+ def docbookChildElmts(prefix) {
+ if(!childElmts) {
+ return ''
+ }
+ def indent = prefix+' '
+ def children = """
+${prefix}
+${indent}Child Elements of <${name}>
+${indent}"""
+ childElmts.values().sort {l,r -> l.name.compareTo(r.name)}.each {
+ children += """\n${indent} ${it.name}"""
+ }
+ children += "\n${indent}\n${prefix}"
+ children
+ }
+ def toDocbook(prefix) {
+ def indent = prefix+' '
+ def parentElmt = docbookParentElmts(indent)
+ def attributes = ""
+ attrs.sort {l,r -> l.name.compareTo(r.name)}.each {
+ attributes += it.toDocbook(indent+' ')
+ }
+ if(attributes) {
+ attributes = """
+${indent}
+${indent} <${name}> Attributes${attributes}
+${indent}"""
+ }
+ def childElmts = docbookChildElmts(indent)
+ def elements = ""
+ childElmts.values().sort {l,r -> l.name.compareTo(r.name)}.each {
+ elements += it.toDocbook(prefix)
+ }
+ """
+${prefix}
+${indent}<${name}>
+${indent}${desc}${parentElmt}${attributes}${childElmts}
+${prefix}${elements}"""
+ }
+
+ public String toString() {
+ def result = prefix + name + " - " + desc+"\n"
+ attrs.sort {l,r -> l.name.compareTo(r.name)}.each { result+= it.toString()}
+ childElmts.values().sort {l,r -> l.name.compareTo(r.name)}.each { result+= it.toString() }
+ result
+ }
+
+ 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
+ }
+}
diff --git a/config/src/test/groovy/org/springframework/security/config/doc/SpringSecurityXsdParser.groovy b/config/src/test/groovy/org/springframework/security/config/doc/SpringSecurityXsdParser.groovy
new file mode 100644
index 0000000000..6c5a081a44
--- /dev/null
+++ b/config/src/test/groovy/org/springframework/security/config/doc/SpringSecurityXsdParser.groovy
@@ -0,0 +1,179 @@
+/*
+ * Copyright 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 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 {
+ private def n = c.attributes().get('ref').split(':')[1]
+ private 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.childElmts.values()*.indent()
+ e.attrs*.indent()
+ 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
+ }
+}
diff --git a/config/src/test/groovy/org/springframework/security/config/doc/XsdDocumentedSpec.groovy b/config/src/test/groovy/org/springframework/security/config/doc/XsdDocumentedSpec.groovy
new file mode 100644
index 0000000000..6f5f9673eb
--- /dev/null
+++ b/config/src/test/groovy/org/springframework/security/config/doc/XsdDocumentedSpec.groovy
@@ -0,0 +1,143 @@
+/*
+ * Copyright 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.util.slurpersupport.NodeChild;
+import spock.lang.*
+
+/**
+ * Tests to ensure that the xsd is properly documented.
+ *
+ * @author Rob Winch
+ */
+class XsdDocumentedSpec extends Specification {
+
+ def ignoredIds = ['nsa-any-user-service','nsa-any-user-service-parents','nsa-authentication','nsa-ldap','nsa-method-security','nsa-web']
+ @Shared def appendix = new File('../docs/manual/src/docbook/appendix-namespace.xml')
+ @Shared def appendixRoot = new XmlSlurper().parse(appendix)
+
+ @Shared File schemaDocument = new File('src/main/resources/org/springframework/security/config/spring-security-3.1.xsd')
+ @Shared Map elementNameToElement
+
+ def setupSpec() {
+ def rootElement = new XmlSlurper().parse(schemaDocument)
+ elementNameToElement = new SpringSecurityXsdParser(rootElement: rootElement).parse()
+ appendixRoot.getMetaClass().sections = {
+ delegate.breadthFirst().inject([]) {result, c->
+ if(c.name() == 'section' && c.@id) {
+ result.add(c)
+ }
+ result
+ }
+ }
+ NodeChild.metaClass.hrefs = { result ->
+ def id = delegate.@id.text().replace('-parents', '').replace('-children', '')
+ result.put(id,[])
+ delegate.children().breadthFirst().each { sectionChild ->
+ def href = sectionChild.@href.text()
+ if(href) {
+ result.get(id).add(href[1..-1])
+ }
+ }
+ }
+ }
+
+ /**
+ * 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 7, if not then schemaDocument needs updated'
+ schemas.size() == 7
+ }
+
+ /**
+ * 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 = appendixRoot.sections().collect { it.@id.text() }
+ 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
+ then: 'all the elements and attributes are documented'
+ documentedIds.sort() == expectedIds.sort()
+ }
+
+ /**
+ * 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 = [:]
+ appendixRoot.sections().each { c->
+ def id = c.@id.text()
+ if(id.endsWith('-parents')) {
+ c.hrefs(docAttrNameToParents)
+ }
+ if(id.endsWith('-children')) {
+ c.hrefs(docAttrNameToChildren)
+ }
+ }
+ 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
+ }
+}