Explorar o código

Add ACL Document Management System XML sample

Closes gh-34
Marcus Da Coregio %!s(int64=4) %!d(string=hai) anos
pai
achega
0bf72c4580
Modificáronse 22 ficheiros con 1665 adicións e 0 borrados
  1. 41 0
      servlet/xml/java/dms/build.gradle
  2. 1 0
      servlet/xml/java/dms/gradle.properties
  3. BIN=BIN
      servlet/xml/java/dms/gradle/wrapper/gradle-wrapper.jar
  4. 5 0
      servlet/xml/java/dms/gradle/wrapper/gradle-wrapper.properties
  5. 185 0
      servlet/xml/java/dms/gradlew
  6. 104 0
      servlet/xml/java/dms/gradlew.bat
  7. 103 0
      servlet/xml/java/dms/src/main/java/sample/dms/AbstractElement.java
  8. 186 0
      servlet/xml/java/dms/src/main/java/sample/dms/DataSourcePopulator.java
  9. 42 0
      servlet/xml/java/dms/src/main/java/sample/dms/Directory.java
  10. 54 0
      servlet/xml/java/dms/src/main/java/sample/dms/DocumentDao.java
  11. 128 0
      servlet/xml/java/dms/src/main/java/sample/dms/DocumentDaoImpl.java
  12. 50 0
      servlet/xml/java/dms/src/main/java/sample/dms/File.java
  13. 112 0
      servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDataSourcePopulator.java
  14. 35 0
      servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDocumentDao.java
  15. 73 0
      servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDocumentDaoImpl.java
  16. 40 0
      servlet/xml/java/dms/src/main/resources/applicationContext-dms-insecure.xml
  17. 245 0
      servlet/xml/java/dms/src/main/resources/applicationContext-dms-secure.xml
  18. 18 0
      servlet/xml/java/dms/src/main/resources/applicationContext-dms-shared.xml
  19. 156 0
      servlet/xml/java/dms/src/test/java/sample/DmsIntegrationTests.java
  20. 71 0
      servlet/xml/java/dms/src/test/java/sample/SecureDmsIntegrationTests.java
  21. 15 0
      servlet/xml/java/dms/src/test/resources/logback-test.xml
  22. 1 0
      settings.gradle

+ 41 - 0
servlet/xml/java/dms/build.gradle

@@ -0,0 +1,41 @@
+plugins {
+	id "java"
+	id "war"
+}
+
+repositories {
+	jcenter()
+	maven { url "https://repo.spring.io/snapshot" }
+}
+
+dependencies {
+	implementation platform("org.springframework.security:spring-security-bom:5.6.0-SNAPSHOT")
+	implementation platform("org.springframework:spring-framework-bom:5.3.9")
+	implementation platform("org.junit:junit-bom:5.7.0")
+
+	implementation 'org.springframework:spring-beans'
+	implementation 'org.springframework:spring-jdbc'
+	implementation 'org.springframework:spring-tx'
+	implementation "org.springframework.security:spring-security-acl"
+	implementation "org.springframework.security:spring-security-core"
+	implementation "org.springframework.security:spring-security-config"
+	implementation "org.thymeleaf:thymeleaf-spring5:3.0.11.RELEASE"
+	implementation 'javax.servlet:jstl:1.2'
+	implementation 'org.slf4j:slf4j-api:1.7.30'
+	implementation 'org.slf4j:slf4j-simple:1.7.30'
+
+	runtime 'net.sf.ehcache:ehcache:2.10.5'
+	runtime 'org.hsqldb:hsqldb:2.5.0'
+	runtime 'org.springframework:spring-context-support'
+
+	testImplementation "org.springframework:spring-test"
+	testImplementation "org.springframework.security:spring-security-test"
+	testImplementation("org.junit.jupiter:junit-jupiter-api")
+	testImplementation "org.assertj:assertj-core:3.18.0"
+
+	testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
+}
+
+tasks.withType(Test).configureEach {
+	useJUnitPlatform()
+}

+ 1 - 0
servlet/xml/java/dms/gradle.properties

@@ -0,0 +1 @@
+version=5.6.0-SNAPSHOT

BIN=BIN
servlet/xml/java/dms/gradle/wrapper/gradle-wrapper.jar


+ 5 - 0
servlet/xml/java/dms/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 185 - 0
servlet/xml/java/dms/gradlew

@@ -0,0 +1,185 @@
+#!/usr/bin/env sh
+
+#
+# Copyright 2015 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
+#
+#      https://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.
+#
+
+##############################################################################
+##
+##  Gradle start up script for UN*X
+##
+##############################################################################
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn () {
+    echo "$*"
+}
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "`uname`" in
+  CYGWIN* )
+    cygwin=true
+    ;;
+  Darwin* )
+    darwin=true
+    ;;
+  MINGW* )
+    msys=true
+    ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+        JAVACMD="$JAVA_HOME/bin/java"
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD="java"
+    which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
+    MAX_FD_LIMIT=`ulimit -H -n`
+    if [ $? -eq 0 ] ; then
+        if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+            MAX_FD="$MAX_FD_LIMIT"
+        fi
+        ulimit -n $MAX_FD
+        if [ $? -ne 0 ] ; then
+            warn "Could not set maximum file descriptor limit: $MAX_FD"
+        fi
+    else
+        warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+    fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+    GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
+    APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+    CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+    
+    JAVACMD=`cygpath --unix "$JAVACMD"`
+
+    # We build the pattern for arguments to be converted via cygpath
+    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+    SEP=""
+    for dir in $ROOTDIRSRAW ; do
+        ROOTDIRS="$ROOTDIRS$SEP$dir"
+        SEP="|"
+    done
+    OURCYGPATTERN="(^($ROOTDIRS))"
+    # Add a user-defined pattern to the cygpath arguments
+    if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+        OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+    fi
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    i=0
+    for arg in "$@" ; do
+        CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+        CHECK2=`echo "$arg"|egrep -c "^-"`                                 ### Determine if an option
+
+        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition
+            eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+        else
+            eval `echo args$i`="\"$arg\""
+        fi
+        i=`expr $i + 1`
+    done
+    case $i in
+        0) set -- ;;
+        1) set -- "$args0" ;;
+        2) set -- "$args0" "$args1" ;;
+        3) set -- "$args0" "$args1" "$args2" ;;
+        4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+        5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+        6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+        7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+        8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+        9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+    esac
+fi
+
+# Escape application args
+save () {
+    for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
+    echo " "
+}
+APP_ARGS=`save "$@"`
+
+# Collect all arguments for the java command, following the shell quoting and substitution rules
+eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
+
+exec "$JAVACMD" "$@"

+ 104 - 0
servlet/xml/java/dms/gradlew.bat

@@ -0,0 +1,104 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windows variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if  not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 103 - 0
servlet/xml/java/dms/src/main/java/sample/dms/AbstractElement.java

@@ -0,0 +1,103 @@
+/*
+ * Copyright 2002-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
+ *
+ *      https://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 sample.dms;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.util.Assert;
+
+/**
+ * Base implementation for a element.
+ *
+ * @author Ben Alex
+ */
+public abstract class AbstractElement {
+
+	/** The name of this token (a filename or directory segment name). */
+	private final String name;
+
+	/** The parent of this token (a directory, or null if referring to root). */
+	private final AbstractElement parent;
+
+	/** The database identifier for this object (null if not persisted). */
+	private Long id;
+
+	/**
+	 * Constructor to use to represent a root element. A root element has an id of -1.
+	 */
+	protected AbstractElement() {
+		this.name = "/";
+		this.parent = null;
+		this.id = -1L;
+	}
+
+	/**
+	 * Constructor to use to represent a non-root element.
+	 * @param name name for this element (required, cannot be "/")
+	 * @param parent for this element (required, cannot be null)
+	 */
+	protected AbstractElement(String name, AbstractElement parent) {
+		Assert.hasText(name, "Name required");
+		Assert.notNull(parent, "Parent required");
+		Assert.notNull(parent.getId(), "The parent must have been saved in order to create a child");
+		this.name = name;
+		this.parent = parent;
+	}
+
+	public Long getId() {
+		return this.id;
+	}
+
+	/**
+	 * Gets the name of this element.
+	 * @return the name of this token (never null, although will be "/" if root, otherwise
+	 * it won't include separators)
+	 */
+	public String getName() {
+		return this.name;
+	}
+
+	public AbstractElement getParent() {
+		return this.parent;
+	}
+
+	/**
+	 * Gets the fully-qualified name of this element, including any parents.
+	 * @return the fully-qualified name of this element, including any parents
+	 */
+	public String getFullName() {
+		List<String> strings = new ArrayList<>();
+		AbstractElement currentElement = this;
+		while (currentElement != null) {
+			strings.add(0, currentElement.getName());
+			currentElement = currentElement.getParent();
+		}
+
+		StringBuilder sb = new StringBuilder();
+		String lastCharacter = null;
+		for (String token : strings) {
+			if (!"/".equals(lastCharacter) && lastCharacter != null) {
+				sb.append("/");
+			}
+			sb.append(token);
+			lastCharacter = token.substring(token.length() - 1);
+		}
+		return sb.toString();
+	}
+
+}

+ 186 - 0
servlet/xml/java/dms/src/main/java/sample/dms/DataSourcePopulator.java

@@ -0,0 +1,186 @@
+/*
+ * Copyright 2002-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
+ *
+ *      https://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 sample.dms;
+
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.AuthorityUtils;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.Assert;
+
+/**
+ * Populates the DMS in-memory database with document and ACL information.
+ *
+ * @author Ben Alex
+ */
+public class DataSourcePopulator implements InitializingBean {
+
+	protected static final int LEVEL_NEGATE_READ = 0;
+
+	protected static final int LEVEL_GRANT_READ = 1;
+
+	protected static final int LEVEL_GRANT_WRITE = 2;
+
+	protected static final int LEVEL_GRANT_ADMIN = 3;
+
+	protected JdbcTemplate template;
+
+	protected DocumentDao documentDao;
+
+	public DataSourcePopulator(DataSource dataSource, DocumentDao documentDao) {
+		Assert.notNull(dataSource, "DataSource required");
+		Assert.notNull(documentDao, "DocumentDao required");
+		this.template = new JdbcTemplate(dataSource);
+		this.documentDao = documentDao;
+	}
+
+	public void afterPropertiesSet() {
+		// ACL tables
+		this.template.execute(
+				"CREATE TABLE ACL_SID(ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY,PRINCIPAL BOOLEAN NOT NULL,SID VARCHAR_IGNORECASE(100) NOT NULL,CONSTRAINT UNIQUE_UK_1 UNIQUE(SID,PRINCIPAL));");
+		this.template.execute(
+				"CREATE TABLE ACL_CLASS(ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY,CLASS VARCHAR_IGNORECASE(100) NOT NULL,CLASS_ID_TYPE VARCHAR_IGNORECASE(100),CONSTRAINT UNIQUE_UK_2 UNIQUE(CLASS));");
+		this.template.execute(
+				"CREATE TABLE ACL_OBJECT_IDENTITY(ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY,OBJECT_ID_CLASS BIGINT NOT NULL,OBJECT_ID_IDENTITY VARCHAR_IGNORECASE(36) NOT NULL,PARENT_OBJECT BIGINT,OWNER_SID BIGINT,ENTRIES_INHERITING BOOLEAN NOT NULL,CONSTRAINT UNIQUE_UK_3 UNIQUE(OBJECT_ID_CLASS,OBJECT_ID_IDENTITY),CONSTRAINT FOREIGN_FK_1 FOREIGN KEY(PARENT_OBJECT)REFERENCES ACL_OBJECT_IDENTITY(ID),CONSTRAINT FOREIGN_FK_2 FOREIGN KEY(OBJECT_ID_CLASS)REFERENCES ACL_CLASS(ID),CONSTRAINT FOREIGN_FK_3 FOREIGN KEY(OWNER_SID)REFERENCES ACL_SID(ID));");
+		this.template.execute(
+				"CREATE TABLE ACL_ENTRY(ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY,ACL_OBJECT_IDENTITY BIGINT NOT NULL,ACE_ORDER INT NOT NULL,SID BIGINT NOT NULL,MASK INTEGER NOT NULL,GRANTING BOOLEAN NOT NULL,AUDIT_SUCCESS BOOLEAN NOT NULL,AUDIT_FAILURE BOOLEAN NOT NULL,CONSTRAINT UNIQUE_UK_4 UNIQUE(ACL_OBJECT_IDENTITY,ACE_ORDER),CONSTRAINT FOREIGN_FK_4 FOREIGN KEY(ACL_OBJECT_IDENTITY) REFERENCES ACL_OBJECT_IDENTITY(ID),CONSTRAINT FOREIGN_FK_5 FOREIGN KEY(SID) REFERENCES ACL_SID(ID));");
+
+		// Normal authentication tables
+		this.template.execute(
+				"CREATE TABLE USERS(USERNAME VARCHAR_IGNORECASE(50) NOT NULL PRIMARY KEY,PASSWORD VARCHAR_IGNORECASE(500) NOT NULL,ENABLED BOOLEAN NOT NULL);");
+		this.template.execute(
+				"CREATE TABLE AUTHORITIES(USERNAME VARCHAR_IGNORECASE(50) NOT NULL,AUTHORITY VARCHAR_IGNORECASE(50) NOT NULL,CONSTRAINT FK_AUTHORITIES_USERS FOREIGN KEY(USERNAME) REFERENCES USERS(USERNAME));");
+		this.template.execute("CREATE UNIQUE INDEX IX_AUTH_USERNAME ON AUTHORITIES(USERNAME,AUTHORITY);");
+
+		// Document management system business tables
+		this.template.execute(
+				"CREATE TABLE DIRECTORY(ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY, DIRECTORY_NAME VARCHAR_IGNORECASE(50) NOT NULL, PARENT_DIRECTORY_ID BIGINT)");
+		this.template.execute(
+				"CREATE TABLE FILE(ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 100) NOT NULL PRIMARY KEY, FILE_NAME VARCHAR_IGNORECASE(50) NOT NULL, CONTENT VARCHAR_IGNORECASE(1024), PARENT_DIRECTORY_ID BIGINT)");
+
+		// Populate the authentication and role tables
+		this.template.execute(
+				"INSERT INTO USERS VALUES('rod','$2a$10$75pBjapg4Nl8Pzd.3JRnUe7PDJmk9qBGwNEJDAlA3V.dEJxcDKn5O',TRUE);");
+		this.template.execute(
+				"INSERT INTO USERS VALUES('dianne','$2a$04$bCMEyxrdF/7sgfUiUJ6Ose2vh9DAMaVBldS1Bw2fhi1jgutZrr9zm',TRUE);");
+		this.template.execute(
+				"INSERT INTO USERS VALUES('scott','$2a$06$eChwvzAu3TSexnC3ynw4LOSw1qiEbtNItNeYv5uI40w1i3paoSfLu',TRUE);");
+		this.template.execute(
+				"INSERT INTO USERS VALUES('peter','$2a$04$8.H8bCMROLF4CIgd7IpeQ.tcBXLP5w8iplO0n.kCIkISwrIgX28Ii',FALSE);");
+		this.template.execute(
+				"INSERT INTO USERS VALUES('bill','$2a$04$8.H8bCMROLF4CIgd7IpeQ.3khQlPVNWbp8kzSQqidQHGFurim7P8O',TRUE);");
+		this.template.execute(
+				"INSERT INTO USERS VALUES('bob','$2a$06$zMgxlMf01SfYNcdx7n4NpeFlAGU8apCETz/i2C7VlYWu6IcNyn4Ay',TRUE);");
+		this.template.execute(
+				"INSERT INTO USERS VALUES('jane','$2a$05$ZrdS7yMhCZ1J.AAidXZhCOxdjD8LO/dhlv4FJzkXA6xh9gdEbBT/u',TRUE);");
+		this.template.execute("INSERT INTO AUTHORITIES VALUES('rod','ROLE_USER');");
+		this.template.execute("INSERT INTO AUTHORITIES VALUES('rod','ROLE_SUPERVISOR');");
+		this.template.execute("INSERT INTO AUTHORITIES VALUES('dianne','ROLE_USER');");
+		this.template.execute("INSERT INTO AUTHORITIES VALUES('scott','ROLE_USER');");
+		this.template.execute("INSERT INTO AUTHORITIES VALUES('peter','ROLE_USER');");
+		this.template.execute("INSERT INTO AUTHORITIES VALUES('bill','ROLE_USER');");
+		this.template.execute("INSERT INTO AUTHORITIES VALUES('bob','ROLE_USER');");
+		this.template.execute("INSERT INTO AUTHORITIES VALUES('jane','ROLE_USER');");
+
+		// Now create an ACL entry for the root directory
+		SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("rod", "ignored",
+				AuthorityUtils.createAuthorityList(("ROLE_IGNORED"))));
+
+		addPermission(this.documentDao, Directory.ROOT_DIRECTORY, "ROLE_USER", LEVEL_GRANT_WRITE);
+
+		// Now go off and create some directories and files for our users
+		createSampleData("rod", "koala");
+		createSampleData("dianne", "emu");
+		createSampleData("scott", "wombat");
+
+	}
+
+	/**
+	 * Creates a directory for the user, and a series of sub-directories. The root
+	 * directory is the parent for the user directory. The sub-directories are
+	 * "confidential" and "shared". The ROLE_USER will be given read and write access to
+	 * "shared".
+	 * @param username the user's username
+	 * @param password the user's password
+	 */
+	private void createSampleData(String username, String password) {
+		Assert.notNull(this.documentDao, "DocumentDao required");
+		Assert.hasText(username, "Username required");
+
+		Authentication auth = new UsernamePasswordAuthenticationToken(username, password);
+
+		try {
+			// Set the SecurityContextHolder ThreadLocal so any subclasses
+			// automatically know which user is operating
+			SecurityContextHolder.getContext().setAuthentication(auth);
+
+			// Create the home directory first
+			Directory home = new Directory(username, Directory.ROOT_DIRECTORY);
+			this.documentDao.create(home);
+			addPermission(this.documentDao, home, username, LEVEL_GRANT_ADMIN);
+			addPermission(this.documentDao, home, "ROLE_USER", LEVEL_GRANT_READ);
+			createFiles(this.documentDao, home);
+
+			// Now create the confidential directory
+			Directory confid = new Directory("confidential", home);
+			this.documentDao.create(confid);
+			addPermission(this.documentDao, confid, "ROLE_USER", LEVEL_NEGATE_READ);
+			createFiles(this.documentDao, confid);
+
+			// Now create the shared directory
+			Directory shared = new Directory("shared", home);
+			this.documentDao.create(shared);
+			addPermission(this.documentDao, shared, "ROLE_USER", LEVEL_GRANT_READ);
+			addPermission(this.documentDao, shared, "ROLE_USER", LEVEL_GRANT_WRITE);
+			createFiles(this.documentDao, shared);
+		}
+		finally {
+			// Clear the SecurityContextHolder ThreadLocal so future calls are
+			// guaranteed to be clean
+			SecurityContextHolder.clearContext();
+		}
+	}
+
+	private void createFiles(DocumentDao documentDao, Directory parent) {
+		Assert.notNull(documentDao, "DocumentDao required");
+		Assert.notNull(parent, "Parent required");
+		int countBeforeInsert = documentDao.findElements(parent).length;
+		for (int i = 0; i < 10; i++) {
+			File file = new File("file_" + i + ".txt", parent);
+			documentDao.create(file);
+		}
+		Assert.isTrue(countBeforeInsert + 10 == documentDao.findElements(parent).length,
+				"Failed to increase count by 10");
+	}
+
+	/**
+	 * Allows subclass to add permissions.
+	 * @param documentDao that will presumably offer methods to enable the operation to be
+	 * completed
+	 * @param element to the subject of the new permissions
+	 * @param recipient to receive permission (if it starts with ROLE_ it is assumed to be
+	 * a GrantedAuthority, else it is a username)
+	 * @param level based on the static final integer fields on this class
+	 */
+	protected void addPermission(DocumentDao documentDao, AbstractElement element, String recipient, int level) {
+	}
+
+}

+ 42 - 0
servlet/xml/java/dms/src/main/java/sample/dms/Directory.java

@@ -0,0 +1,42 @@
+/*
+ * Copyright 2002-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
+ *
+ *      https://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 sample.dms;
+
+/**
+ * Represents a directory.
+ *
+ * @author Ben Alex
+ */
+public class Directory extends AbstractElement {
+
+	/** The root directory. */
+	public static final Directory ROOT_DIRECTORY = new Directory();
+
+	private Directory() {
+	}
+
+	public Directory(String name, Directory parent) {
+		super(name, parent);
+	}
+
+	@Override
+	public String toString() {
+		return "Directory[fullName='" + getFullName() + "'; name='" + getName() + "'; id='" + getId() + "'; parent='"
+				+ getParent() + "']";
+	}
+
+}

+ 54 - 0
servlet/xml/java/dms/src/main/java/sample/dms/DocumentDao.java

@@ -0,0 +1,54 @@
+/*
+ * Copyright 2002-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
+ *
+ *      https://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 sample.dms;
+
+/**
+ * Interface to define Document data access operations.
+ *
+ * @author Ben Alex
+ */
+public interface DocumentDao {
+
+	/**
+	 * Creates an entry in the database for the element.
+	 * @param element an unsaved element (the "id" will be updated after method is
+	 * invoked)
+	 */
+	void create(AbstractElement element);
+
+	/**
+	 * Removes a file from the database for the specified element.
+	 * @param file the file to remove (cannot be null)
+	 */
+	void delete(File file);
+
+	/**
+	 * Modifies a file in the database.
+	 * @param file the file to update (cannot be null)
+	 */
+	void update(File file);
+
+	/**
+	 * Locates elements in the database which appear under the presented directory.
+	 * @param directory the directory (cannot be null - use
+	 * {@link Directory#ROOT_DIRECTORY} for root)
+	 * @return zero or more elements in the directory (an empty array may be returned -
+	 * never null)
+	 */
+	AbstractElement[] findElements(Directory directory);
+
+}

+ 128 - 0
servlet/xml/java/dms/src/main/java/sample/dms/DocumentDaoImpl.java

@@ -0,0 +1,128 @@
+/*
+ * Copyright 2002-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
+ *
+ *      https://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 sample.dms;
+
+import java.util.List;
+
+import org.springframework.jdbc.core.support.JdbcDaoSupport;
+import org.springframework.security.util.FieldUtils;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+import org.springframework.util.Assert;
+
+/**
+ * Basic JDBC implementation of {@link DocumentDao}.
+ *
+ * @author Ben Alex
+ */
+public class DocumentDaoImpl extends JdbcDaoSupport implements DocumentDao {
+
+	private static final String INSERT_INTO_DIRECTORY = "insert into directory(directory_name, parent_directory_id) values (?,?)";
+
+	private static final String INSERT_INTO_FILE = "insert into file(file_name, content, parent_directory_id) values (?,?,?)";
+
+	private static final String SELECT_FROM_DIRECTORY = "select id from directory where parent_directory_id = ?";
+
+	private static final String SELECT_FROM_DIRECTORY_NULL = "select id from directory where parent_directory_id is null";
+
+	private static final String SELECT_FROM_FILE = "select id, file_name, content, parent_directory_id from file where parent_directory_id = ?";
+
+	private static final String SELECT_FROM_DIRECTORY_SINGLE = "select id, directory_name, parent_directory_id from directory where id = ?";
+
+	private static final String DELETE_FROM_FILE = "delete from file where id = ?";
+
+	private static final String UPDATE_FILE = "update file set content = ? where id = ?";
+
+	private static final String SELECT_IDENTITY = "call identity()";
+
+	private Long obtainPrimaryKey() {
+		Assert.isTrue(TransactionSynchronizationManager.isSynchronizationActive(), "Transaction must be running");
+		return getJdbcTemplate().queryForObject(SELECT_IDENTITY, Long.class);
+	}
+
+	public void create(AbstractElement element) {
+		Assert.notNull(element, "Element required");
+		Assert.isNull(element.getId(), "Element has previously been saved");
+		if (element instanceof Directory) {
+			Directory directory = (Directory) element;
+			Long parentId = (directory.getParent() == null) ? null : directory.getParent().getId();
+			getJdbcTemplate().update(INSERT_INTO_DIRECTORY, new Object[] { directory.getName(), parentId });
+			FieldUtils.setProtectedFieldValue("id", directory, obtainPrimaryKey());
+		}
+		else if (element instanceof File) {
+			File file = (File) element;
+			Long parentId = (file.getParent() == null) ? null : file.getParent().getId();
+			getJdbcTemplate().update(INSERT_INTO_FILE, new Object[] { file.getName(), file.getContent(), parentId });
+			FieldUtils.setProtectedFieldValue("id", file, obtainPrimaryKey());
+		}
+		else {
+			throw new IllegalArgumentException("Unsupported AbstractElement");
+		}
+	}
+
+	public void delete(File file) {
+		Assert.notNull(file, "File required");
+		Assert.notNull(file.getId(), "File ID required");
+		getJdbcTemplate().update(DELETE_FROM_FILE, new Object[] { file.getId() });
+	}
+
+	private Directory getDirectoryWithImmediateParentPopulated(final Long id) {
+		return getJdbcTemplate().queryForObject(SELECT_FROM_DIRECTORY_SINGLE, new Object[] { id }, (rs, rowNumber) -> {
+			Long parentDirectoryId = rs.getLong("parent_directory_id");
+			Directory parentDirectory = Directory.ROOT_DIRECTORY;
+			if (parentDirectoryId != null && !parentDirectoryId.equals(-1L)) {
+				// Need to go and lookup the parent, so do that first
+				parentDirectory = getDirectoryWithImmediateParentPopulated(parentDirectoryId);
+			}
+			Directory directory = new Directory(rs.getString("directory_name"), parentDirectory);
+			FieldUtils.setProtectedFieldValue("id", directory, rs.getLong("id"));
+			return directory;
+		});
+	}
+
+	public AbstractElement[] findElements(Directory directory) {
+		Assert.notNull(directory, "Directory required (the ID can be null to refer to root)");
+		if (directory.getId() == null) {
+			List<Directory> directories = getJdbcTemplate().query(SELECT_FROM_DIRECTORY_NULL,
+					(rs, rowNumber) -> getDirectoryWithImmediateParentPopulated(rs.getLong("id")));
+			return directories.toArray(new AbstractElement[] {});
+		}
+		List<AbstractElement> directories = getJdbcTemplate().query(SELECT_FROM_DIRECTORY,
+				new Object[] { directory.getId() },
+				(rs, rowNumber) -> getDirectoryWithImmediateParentPopulated(rs.getLong("id")));
+		List<File> files = getJdbcTemplate().query(SELECT_FROM_FILE, new Object[] { directory.getId() },
+				(rs, rowNumber) -> {
+					Long parentDirectoryId = rs.getLong("parent_directory_id");
+					Directory parentDirectory = null;
+					if (parentDirectoryId != null) {
+						parentDirectory = getDirectoryWithImmediateParentPopulated(parentDirectoryId);
+					}
+					File file = new File(rs.getString("file_name"), parentDirectory);
+					FieldUtils.setProtectedFieldValue("id", file, rs.getLong("id"));
+					return file;
+				});
+		// Add the File elements after the Directory elements
+		directories.addAll(files);
+		return directories.toArray(new AbstractElement[] {});
+	}
+
+	public void update(File file) {
+		Assert.notNull(file, "File required");
+		Assert.notNull(file.getId(), "File ID required");
+		getJdbcTemplate().update(UPDATE_FILE, new Object[] { file.getContent(), file.getId() });
+	}
+
+}

+ 50 - 0
servlet/xml/java/dms/src/main/java/sample/dms/File.java

@@ -0,0 +1,50 @@
+/*
+ * Copyright 2002-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
+ *
+ *      https://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 sample.dms;
+
+import org.springframework.util.Assert;
+
+/**
+ * Represents a File.
+ *
+ * @author Ben Alex
+ */
+public class File extends AbstractElement {
+
+	/** Content of the file, which can be null. */
+	private String content;
+
+	public File(String name, Directory parent) {
+		super(name, parent);
+		Assert.isTrue(!parent.equals(Directory.ROOT_DIRECTORY), "Cannot insert File into root directory");
+	}
+
+	public String getContent() {
+		return this.content;
+	}
+
+	public void setContent(String content) {
+		this.content = content;
+	}
+
+	@Override
+	public String toString() {
+		return "File[fullName='" + getFullName() + "'; name='" + getName() + "'; id='" + getId() + "'; content="
+				+ getContent() + "'; parent='" + getParent() + "']";
+	}
+
+}

+ 112 - 0
servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDataSourcePopulator.java

@@ -0,0 +1,112 @@
+/*
+ * Copyright 2002-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
+ *
+ *      https://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 sample.dms.secured;
+
+import javax.sql.DataSource;
+
+import sample.dms.AbstractElement;
+import sample.dms.DataSourcePopulator;
+import sample.dms.DocumentDao;
+
+import org.springframework.security.acls.domain.BasePermission;
+import org.springframework.security.acls.domain.GrantedAuthoritySid;
+import org.springframework.security.acls.domain.ObjectIdentityImpl;
+import org.springframework.security.acls.domain.PrincipalSid;
+import org.springframework.security.acls.model.MutableAcl;
+import org.springframework.security.acls.model.MutableAclService;
+import org.springframework.security.acls.model.NotFoundException;
+import org.springframework.security.acls.model.ObjectIdentity;
+import org.springframework.security.acls.model.Permission;
+import org.springframework.security.acls.model.Sid;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.Assert;
+
+public class SecureDataSourcePopulator extends DataSourcePopulator {
+
+	private final MutableAclService aclService;
+
+	public SecureDataSourcePopulator(DataSource dataSource, SecureDocumentDao documentDao,
+			MutableAclService aclService) {
+		super(dataSource, documentDao);
+		Assert.notNull(aclService, "MutableAclService required");
+		this.aclService = aclService;
+	}
+
+	protected void addPermission(DocumentDao documentDao, AbstractElement element, String recipient, int level) {
+		Assert.notNull(documentDao, "DocumentDao required");
+		Assert.isInstanceOf(SecureDocumentDao.class, documentDao, "DocumentDao should have been a SecureDocumentDao");
+		Assert.notNull(element, "Element required");
+		Assert.hasText(recipient, "Recipient required");
+		Assert.notNull(SecurityContextHolder.getContext().getAuthentication(),
+				"SecurityContextHolder must contain an Authentication");
+
+		// We need SecureDocumentDao to assign different permissions
+		// SecureDocumentDao dao = (SecureDocumentDao) documentDao;
+
+		// We need to construct an ACL-specific Sid. Note the prefix contract is defined
+		// on the superclass method's JavaDocs
+		Sid sid = null;
+		if (recipient.startsWith("ROLE_")) {
+			sid = new GrantedAuthoritySid(recipient);
+		}
+		else {
+			sid = new PrincipalSid(recipient);
+		}
+
+		// We need to identify the target domain object and create an ObjectIdentity for
+		// it
+		// This works because AbstractElement has a "getId()" method
+		ObjectIdentity identity = new ObjectIdentityImpl(element);
+		// ObjectIdentity identity = new ObjectIdentityImpl(element.getClass(),
+		// element.getId()); // equivalent
+
+		// Next we need to create a Permission
+		Permission permission = null;
+		if (level == LEVEL_NEGATE_READ || level == LEVEL_GRANT_READ) {
+			permission = BasePermission.READ;
+		}
+		else if (level == LEVEL_GRANT_WRITE) {
+			permission = BasePermission.WRITE;
+		}
+		else if (level == LEVEL_GRANT_ADMIN) {
+			permission = BasePermission.ADMINISTRATION;
+		}
+		else {
+			throw new IllegalArgumentException("Unsupported LEVEL_");
+		}
+
+		// Attempt to retrieve the existing ACL, creating an ACL if it doesn't already
+		// exist for this ObjectIdentity
+		MutableAcl acl = null;
+		try {
+			acl = (MutableAcl) this.aclService.readAclById(identity);
+		}
+		catch (NotFoundException nfe) {
+			acl = this.aclService.createAcl(identity);
+			Assert.notNull(acl, "Acl could not be retrieved or created");
+		}
+
+		// Now we have an ACL, add another ACE to it
+		// granting
+		// granting
+		acl.insertAce(acl.getEntries().size(), permission, sid, level != LEVEL_NEGATE_READ); // not
+
+		// Finally, persist the modified ACL
+		this.aclService.updateAcl(acl);
+	}
+
+}

+ 35 - 0
servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDocumentDao.java

@@ -0,0 +1,35 @@
+/*
+ * Copyright 2002-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
+ *
+ *      https://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 sample.dms.secured;
+
+import sample.dms.DocumentDao;
+
+/**
+ * Extends the {@link DocumentDao} and introduces ACL-related methods.
+ *
+ * @author Ben Alex
+ *
+ */
+public interface SecureDocumentDao extends DocumentDao {
+
+	/**
+	 * Gets all the users existing in the system.
+	 * @return all the usernames existing in the system.
+	 */
+	String[] getUsers();
+
+}

+ 73 - 0
servlet/xml/java/dms/src/main/java/sample/dms/secured/SecureDocumentDaoImpl.java

@@ -0,0 +1,73 @@
+/*
+ * Copyright 2002-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
+ *
+ *      https://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 sample.dms.secured;
+
+import sample.dms.AbstractElement;
+import sample.dms.DocumentDaoImpl;
+
+import org.springframework.security.acls.domain.BasePermission;
+import org.springframework.security.acls.domain.ObjectIdentityImpl;
+import org.springframework.security.acls.domain.PrincipalSid;
+import org.springframework.security.acls.model.MutableAcl;
+import org.springframework.security.acls.model.MutableAclService;
+import org.springframework.security.acls.model.ObjectIdentity;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.Assert;
+
+/**
+ * Adds extra {@link SecureDocumentDao} methods.
+ *
+ * @author Ben Alex
+ *
+ */
+public class SecureDocumentDaoImpl extends DocumentDaoImpl implements SecureDocumentDao {
+
+	private static final String SELECT_FROM_USERS = "SELECT USERNAME FROM USERS ORDER BY USERNAME";
+
+	private final MutableAclService mutableAclService;
+
+	public SecureDocumentDaoImpl(MutableAclService mutableAclService) {
+		Assert.notNull(mutableAclService, "MutableAclService required");
+		this.mutableAclService = mutableAclService;
+	}
+
+	public String[] getUsers() {
+		return getJdbcTemplate().query(SELECT_FROM_USERS, (rs, rowNumber) -> rs.getString("USERNAME"))
+				.toArray(new String[] {});
+	}
+
+	public void create(AbstractElement element) {
+		super.create(element);
+
+		// Create an ACL identity for this element
+		ObjectIdentity identity = new ObjectIdentityImpl(element);
+		MutableAcl acl = this.mutableAclService.createAcl(identity);
+
+		// If the AbstractElement has a parent, go and retrieve its identity (it should
+		// already exist)
+		if (element.getParent() != null) {
+			ObjectIdentity parentIdentity = new ObjectIdentityImpl(element.getParent());
+			MutableAcl aclParent = (MutableAcl) this.mutableAclService.readAclById(parentIdentity);
+			acl.setParent(aclParent);
+		}
+		acl.insertAce(acl.getEntries().size(), BasePermission.ADMINISTRATION,
+				new PrincipalSid(SecurityContextHolder.getContext().getAuthentication()), true);
+
+		this.mutableAclService.updateAcl(acl);
+	}
+
+}

+ 40 - 0
servlet/xml/java/dms/src/main/resources/applicationContext-dms-insecure.xml

@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  - Application context representing the application without any security services.
+  -
+  -->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
+		<property name="dataSource" ref="dataSource"/>
+	</bean>
+
+	<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
+		<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
+		<property name="url" value="jdbc:hsqldb:mem:insecuredms"/>
+		<property name="username" value="sa"/>
+		<property name="password" value=""/>
+	</bean>
+
+	<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
+		<property name="transactionAttributeSource">
+			<value>
+				sample.dms.DocumentDao.*=PROPAGATION_REQUIRED
+			</value>
+		</property>
+		<property name="transactionManager" ref="transactionManager" />
+	</bean>
+
+	<bean id="documentDao" class="sample.dms.DocumentDaoImpl">
+		<property name="dataSource" ref="dataSource"/>
+	</bean>
+
+	<bean id="dataSourcePopulator" class="sample.dms.DataSourcePopulator">
+		<constructor-arg ref="dataSource"/>
+		<constructor-arg ref="documentDao"/>
+	</bean>
+
+</beans>

+ 245 - 0
servlet/xml/java/dms/src/main/resources/applicationContext-dms-secure.xml

@@ -0,0 +1,245 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+  - Application context representing the application WITH security services.
+  -
+  -->
+
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:s="http://www.springframework.org/schema/security"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
+						http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd">
+
+	<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
+		<property name="dataSource" ref="dataSource"/>
+	</bean>
+
+	<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
+		<property name="driverClassName" value="org.hsqldb.jdbcDriver"/>
+		<property name="url" value="jdbc:hsqldb:mem:securedms"/>
+		<property name="username" value="sa"/>
+		<property name="password" value=""/>
+	</bean>
+
+	<bean id="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor">
+		<property name="transactionAttributeSource">
+			<value>
+				sample.dms.secured.SecureDocumentDao.*=PROPAGATION_REQUIRED
+				sample.dms.DocumentDao.*=PROPAGATION_REQUIRED
+				org.springframework.security.acls.model.AclService.*=PROPAGATION_REQUIRED
+				org.springframework.security.acls.model.MutableAclService.*=PROPAGATION_REQUIRED
+				org.springframework.security.acls.jdbc.JdbcMutableAclService.*=PROPAGATION_REQUIRED
+				org.springframework.security.acls.jdbc.JdbcAclService.*=PROPAGATION_REQUIRED
+			</value>
+		</property>
+		<property name="transactionManager" ref="transactionManager" />
+	</bean>
+
+	<bean id="documentDao" class="sample.dms.secured.SecureDocumentDaoImpl">
+		<constructor-arg ref="aclService"/>
+		<property name="dataSource" ref="dataSource"/>
+	</bean>
+
+	<bean id="dataSourcePopulator" class="sample.dms.secured.SecureDataSourcePopulator">
+		<constructor-arg ref="dataSource"/>
+		<constructor-arg ref="documentDao"/>
+		<constructor-arg ref="aclService"/>
+	</bean>
+
+	<!-- ===================================  SECURITY DEFINITION BEANS ======================================== -->
+
+   <!-- ======================== AUTHENTICATION (note there is no UI and this is for integration tests only) ======================= -->
+
+	<s:authentication-manager alias="authenticationManager">
+		<s:authentication-provider ref="daoAuthenticationProvider"/>
+	</s:authentication-manager>
+
+
+   <bean id="jdbcDaoImpl" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
+	  <property name="dataSource" ref="dataSource"/>
+   </bean>
+
+   <bean id="daoAuthenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
+	  <property name="userDetailsService" ref="jdbcDaoImpl"/>
+	  <property name="userCache" ref="userCache"/>
+	  <property name="passwordEncoder">
+		<bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>
+	  </property>
+   </bean>
+
+   <bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"/>
+
+   <bean id="userCacheBackend" class="org.springframework.cache.ehcache.EhCacheFactoryBean">
+	  <property name="cacheManager" ref="cacheManager"/>
+	  <property name="cacheName" value="userCache"/>
+   </bean>
+
+   <bean id="userCache" class="org.springframework.security.core.userdetails.cache.EhCacheBasedUserCache">
+	  <property name="cache" ref="userCacheBackend"/>
+   </bean>
+
+   <!-- Automatically receives AuthenticationEvent messages -->
+   <bean id="loggerListener" class="org.springframework.security.authentication.event.LoggerListener"/>
+
+   <!-- ========================= "BEFORE INVOCATION" AUTHORIZATION DEFINITIONS ============================== -->
+
+   <!-- ACL permission masks used by this application -->
+   <bean id="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
+	  <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/>
+   </bean>
+   <bean id="org.springframework.security.acls.domain.BasePermission.READ" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
+	  <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.READ"/>
+   </bean>
+   <bean id="org.springframework.security.acls.domain.BasePermission.WRITE" class="org.springframework.beans.factory.config.FieldRetrievingFactoryBean">
+	  <property name="staticField" value="org.springframework.security.acls.domain.BasePermission.WRITE"/>
+   </bean>
+
+
+   <!-- An access decision voter that reads ROLE_* configuration settings -->
+   <bean id="roleVoter" class="org.springframework.security.access.vote.RoleVoter"/>
+
+   <!-- An access decision voter that reads ACL_ABSTRACT_ELEMENT_WRITE_PARENT configuration settings -->
+   <bean id="aclAbstractElementWriteParentVoter" class="org.springframework.security.acls.AclEntryVoter">
+	  <constructor-arg ref="aclService"/>
+	  <constructor-arg value="ACL_ABSTRACT_ELEMENT_WRITE_PARENT"/>
+	  <constructor-arg>
+		  <list>
+		  <ref bean="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/>
+		  <ref bean="org.springframework.security.acls.domain.BasePermission.WRITE"/>
+		  </list>
+	  </constructor-arg>
+	  <property name="processDomainObjectClass" value="sample.dms.AbstractElement"/>
+	  <property name="internalMethod" value="getParent"/>
+   </bean>
+
+   <!-- An access decision voter that reads ACL_ABSTRACT_ELEMENT_WRITE configuration settings -->
+   <bean id="aclAbstractElementWriteVoter" class="org.springframework.security.acls.AclEntryVoter">
+	  <constructor-arg ref="aclService"/>
+	  <constructor-arg value="ACL_ABSTRACT_ELEMENT_WRITE"/>
+	  <constructor-arg>
+		  <list>
+		  <ref bean="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/>
+		  <ref bean="org.springframework.security.acls.domain.BasePermission.WRITE"/>
+		  </list>
+	  </constructor-arg>
+	  <property name="processDomainObjectClass" value="sample.dms.AbstractElement"/>
+   </bean>
+
+   <!-- An access decision manager used by the business objects -->
+   <bean id="businessAccessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
+	  <constructor-arg>
+		 <list>
+			<ref bean="roleVoter"/>
+			<ref bean="aclAbstractElementWriteParentVoter"/>
+			<ref bean="aclAbstractElementWriteVoter"/>
+		 </list>
+	  </constructor-arg>
+	  <property name="allowIfAllAbstainDecisions" value="true"/>
+   </bean>
+
+   <!-- ========= ACCESS CONTROL LIST LOOKUP MANAGER DEFINITIONS ========= -->
+
+	<bean id="aclCache" class="org.springframework.security.acls.domain.EhCacheBasedAclCache">
+		<constructor-arg>
+		   <bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
+			  <property name="cacheManager" ref="cacheManager"/>
+			  <property name="cacheName" value="aclCache"/>
+		   </bean>
+		</constructor-arg>
+	<constructor-arg>
+		<bean class="org.springframework.security.acls.domain.DefaultPermissionGrantingStrategy">
+			<constructor-arg>
+				<bean class="org.springframework.security.acls.domain.ConsoleAuditLogger"/>
+			</constructor-arg>
+		</bean>
+	</constructor-arg>
+	<constructor-arg>
+		<bean class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
+			<constructor-arg>
+				<list>
+					<bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
+						<constructor-arg value="ROLE_ACL_ADMIN"/>
+					</bean>
+				</list>
+			</constructor-arg>
+		</bean>
+	</constructor-arg>
+	</bean>
+
+	<bean id="lookupStrategy" class="org.springframework.security.acls.jdbc.BasicLookupStrategy">
+		<constructor-arg ref="dataSource"/>
+		<constructor-arg ref="aclCache"/>
+		<constructor-arg ref="aclAuthorizationStrategy"/>
+		<constructor-arg>
+			<bean class="org.springframework.security.acls.domain.ConsoleAuditLogger"/>
+		</constructor-arg>
+	</bean>
+
+	<bean id="aclAuthorizationStrategy" class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
+		<constructor-arg>
+			<list>
+				<bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
+					<constructor-arg value="ROLE_ADMINISTRATOR"/>
+				</bean>
+				<bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
+					<constructor-arg value="ROLE_ADMINISTRATOR"/>
+				</bean>
+				<bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
+					<constructor-arg value="ROLE_ADMINISTRATOR"/>
+				</bean>
+			</list>
+		</constructor-arg>
+	</bean>
+
+	<bean id="aclService" class="org.springframework.security.acls.jdbc.JdbcMutableAclService">
+		<constructor-arg ref="dataSource"/>
+		<constructor-arg ref="lookupStrategy"/>
+		<constructor-arg ref="aclCache"/>
+	</bean>
+
+   <!-- ============== "AFTER INTERCEPTION" AUTHORIZATION DEFINITIONS =========== -->
+
+   <bean id="afterInvocationManager" class="org.springframework.security.access.intercept.AfterInvocationProviderManager">
+	  <property name="providers">
+		 <list>
+			<ref bean="afterAclCollectionRead"/>
+		 </list>
+	  </property>
+   </bean>
+
+   <!-- Processes AFTER_ACL_COLLECTION_READ configuration settings -->
+   <bean id="afterAclCollectionRead" class="org.springframework.security.acls.afterinvocation.AclEntryAfterInvocationCollectionFilteringProvider">
+	  <constructor-arg ref="aclService"/>
+	  <constructor-arg>
+		  <list>
+		  <ref bean="org.springframework.security.acls.domain.BasePermission.ADMINISTRATION"/>
+		  <ref bean="org.springframework.security.acls.domain.BasePermission.READ"/>
+		  </list>
+	  </constructor-arg>
+   </bean>
+
+   <!-- ================= METHOD INVOCATION AUTHORIZATION ==================== -->
+
+   <bean id="methodSecurityAdvisor" class="org.springframework.security.access.intercept.aopalliance.MethodSecurityMetadataSourceAdvisor">
+	   <constructor-arg value="methodSecurityInterceptor" />
+	   <constructor-arg ref="msmds" />
+	   <constructor-arg value="msmds" />
+   </bean>
+
+   <bean id="methodSecurityInterceptor" class="org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor">
+	  <property name="authenticationManager" ref="authenticationManager"/>
+	  <property name="accessDecisionManager" ref="businessAccessDecisionManager"/>
+	  <property name="afterInvocationManager" ref="afterInvocationManager"/>
+	  <property name="securityMetadataSource" ref="msmds" />
+   </bean>
+
+   <s:method-security-metadata-source id="msmds">
+	  <s:protect method="sample.dms.DocumentDao.create" access="ACL_ABSTRACT_ELEMENT_WRITE_PARENT" />
+	  <s:protect method="sample.dms.DocumentDao.delete" access="ACL_ABSTRACT_ELEMENT_WRITE" />
+	  <s:protect method="sample.dms.DocumentDao.update" access="ACL_ABSTRACT_ELEMENT_WRITE" />
+	  <s:protect method="sample.dms.DocumentDao.findElements" access="AFTER_ACL_COLLECTION_READ" />
+	  <s:protect method="sample.dms.secured.SecureDocumentDao.getUsers" access="ROLE_USER" />
+   </s:method-security-metadata-source>
+
+</beans>

+ 18 - 0
servlet/xml/java/dms/src/main/resources/applicationContext-dms-shared.xml

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  - Application context representing the transaction, auto proxy and data source beans.
+  -
+  -->
+<beans xmlns="http://www.springframework.org/schema/beans"
+	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
+
+	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
+		<property name="dataSource" ref="dataSource"/>
+	</bean>
+
+	<bean id="autoproxy" class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
+
+	<bean id="transactionAdvisor" class="org.springframework.transaction.interceptor.TransactionAttributeSourceAdvisor" autowire="constructor" />
+
+</beans>

+ 156 - 0
servlet/xml/java/dms/src/test/java/sample/DmsIntegrationTests.java

@@ -0,0 +1,156 @@
+/*
+ * Copyright 2002-2017 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
+ *
+ *      https://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 sample;
+
+/*
+ * Copyright 2002-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
+ *
+ *      https://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.
+ */
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import sample.dms.AbstractElement;
+import sample.dms.Directory;
+import sample.dms.DocumentDao;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.transaction.annotation.Transactional;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Basic integration test for DMS sample.
+ *
+ * @author Ben Alex
+ *
+ */
+@ContextConfiguration(
+		locations = { "classpath:applicationContext-dms-shared.xml", "classpath:applicationContext-dms-insecure.xml" })
+@ExtendWith(SpringExtension.class)
+@Transactional
+public class DmsIntegrationTests {
+
+	@Autowired
+	protected JdbcTemplate jdbcTemplate;
+
+	@Autowired
+	protected DocumentDao documentDao;
+
+	@AfterEach
+	void clearContext() {
+		SecurityContextHolder.clearContext();
+	}
+
+	public void setDocumentDao(DocumentDao documentDao) {
+		this.documentDao = documentDao;
+	}
+
+	@Test
+	void testBasePopulation() {
+		assertThat(this.jdbcTemplate.queryForObject("select count(id) from DIRECTORY", Integer.class)).isEqualTo(9);
+		assertThat((int) this.jdbcTemplate.queryForObject("select count(id) from FILE", Integer.class)).isEqualTo(90);
+		assertThat(this.documentDao.findElements(Directory.ROOT_DIRECTORY).length).isEqualTo(3);
+	}
+
+	@Test
+	void testMarissaRetrieval() {
+		process("rod", "koala", false);
+	}
+
+	@Test
+	void testScottRetrieval() {
+		process("scott", "wombat", false);
+	}
+
+	@Test
+	void testDianneRetrieval() {
+		process("dianne", "emu", false);
+	}
+
+	protected void process(String username, String password, boolean shouldBeFiltered) {
+		SecurityContextHolder.getContext()
+				.setAuthentication(new UsernamePasswordAuthenticationToken(username, password));
+		System.out.println("------ Test for username: " + username + " ------");
+		AbstractElement[] rootElements = this.documentDao.findElements(Directory.ROOT_DIRECTORY);
+		assertThat(rootElements).hasSize(3);
+		Directory homeDir = null;
+		Directory nonHomeDir = null;
+		for (AbstractElement rootElement : rootElements) {
+			if (rootElement.getName().equals(username)) {
+				homeDir = (Directory) rootElement;
+			}
+			else {
+				nonHomeDir = (Directory) rootElement;
+			}
+		}
+		System.out.println("Home directory......: " + homeDir.getFullName());
+		System.out.println("Non-home directory..: " + nonHomeDir.getFullName());
+
+		AbstractElement[] homeElements = this.documentDao.findElements(homeDir);
+		assertThat(homeElements).hasSize(12); // confidential and shared
+												// directories,
+		// plus 10 files
+
+		AbstractElement[] nonHomeElements = this.documentDao.findElements(nonHomeDir);
+		assertThat(nonHomeElements).hasSize(shouldBeFiltered ? 11 : 12);
+
+		// cannot see the user's "confidential" sub-directory when filtering
+
+		// Attempt to read the other user's confidential directory from the returned
+		// results
+		// Of course, we shouldn't find a "confidential" directory in the results if we're
+		// filtering
+		Directory nonHomeConfidentialDir = null;
+		for (AbstractElement nonHomeElement : nonHomeElements) {
+			if (nonHomeElement.getName().equals("confidential")) {
+				nonHomeConfidentialDir = (Directory) nonHomeElement;
+			}
+		}
+
+		if (shouldBeFiltered) {
+			assertThat(nonHomeConfidentialDir).withFailMessage("Found confidential directory when we should not have")
+					.isNull();
+		}
+		else {
+			System.out.println("Inaccessible dir....: " + nonHomeConfidentialDir.getFullName());
+			assertThat(this.documentDao.findElements(nonHomeConfidentialDir).length).isEqualTo(10); // 10
+			// files
+			// (no
+			// sub-directories)
+		}
+
+		SecurityContextHolder.clearContext();
+	}
+
+}

+ 71 - 0
servlet/xml/java/dms/src/test/java/sample/SecureDmsIntegrationTests.java

@@ -0,0 +1,71 @@
+/*
+ * Copyright 2002-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
+ *
+ *      https://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 sample;
+
+import org.junit.jupiter.api.Test;
+
+import org.springframework.test.context.ContextConfiguration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Basic integration test for DMS sample when security has been added.
+ *
+ * @author Ben Alex
+ *
+ */
+@ContextConfiguration(
+		locations = { "classpath:applicationContext-dms-shared.xml", "classpath:applicationContext-dms-secure.xml" })
+public class SecureDmsIntegrationTests extends DmsIntegrationTests {
+
+	@Override
+	@Test
+	void testBasePopulation() {
+		assertThat(this.jdbcTemplate.queryForObject("select count(id) from DIRECTORY", Integer.class)).isEqualTo(9);
+		assertThat(this.jdbcTemplate.queryForObject("select count(id) from FILE", Integer.class)).isEqualTo(90);
+		assertThat(this.jdbcTemplate.queryForObject("select count(id) from ACL_SID", Integer.class)).isEqualTo(4); // 3
+																													// users
+																													// +
+																													// 1
+																													// role
+		assertThat(this.jdbcTemplate.queryForObject("select count(id) from ACL_CLASS", Integer.class)).isEqualTo(2); // Directory
+		// and
+		// File
+		assertThat(this.jdbcTemplate.queryForObject("select count(id) from ACL_OBJECT_IDENTITY", Integer.class))
+				.isEqualTo(100);
+		assertThat(this.jdbcTemplate.queryForObject("select count(id) from ACL_ENTRY", Integer.class)).isEqualTo(115);
+	}
+
+	@Override
+	@Test
+	void testMarissaRetrieval() {
+		process("rod", "koala", true);
+	}
+
+	@Override
+	@Test
+	void testScottRetrieval() {
+		process("scott", "wombat", true);
+	}
+
+	@Override
+	@Test
+	void testDianneRetrieval() {
+		process("dianne", "emu", true);
+	}
+
+}

+ 15 - 0
servlet/xml/java/dms/src/test/resources/logback-test.xml

@@ -0,0 +1,15 @@
+<configuration>
+	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+	<encoder>
+		<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
+	</encoder>
+	</appender>
+
+	<logger name="org.springframework.security" level="${sec.log.level:-WARN}"/>
+
+
+	<root level="${root.level:-WARN}">
+	<appender-ref ref="STDOUT" />
+	</root>
+
+</configuration>

+ 1 - 0
settings.gradle

@@ -57,3 +57,4 @@ include ":servlet:spring-boot:kotlin:hello-security"
 include ":servlet:xml:java:helloworld"
 include ":servlet:xml:java:preauth"
 include ":servlet:xml:java:contacts"
+include ":servlet:xml:java:dms"