diff --git a/.gitignore b/.gitignore
index 6865286e..a41b34ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,4 +10,3 @@ build
*.ipr
.idea
*.iws
-pom.xml
diff --git a/build.gradle b/build.gradle
index d4ab03e6..90ac2c2a 100644
--- a/build.gradle
+++ b/build.gradle
@@ -17,6 +17,7 @@ ext.JAVA_MODULE_SCRIPT = "${GRADLE_SCRIPT_DIR}/java-module.gradle"
ext.MAVEN_DEPLOYMENT_SCRIPT = "${GRADLE_SCRIPT_DIR}/maven-deployment.gradle"
ext.JAVA_SCRIPT = "${GRADLE_SCRIPT_DIR}/java.gradle"
ext.RELEASE_CHECKS_SCRIPT = "${GRADLE_SCRIPT_DIR}/release-checks.gradle"
+ext.SAMPLE_WAR_GRADLE = "${GRADLE_SCRIPT_DIR}/sample-war.gradle"
ext.coreModules = subprojects.findAll { p-> (!p.name.contains("test") && !p.name.contains("sample") && !p.name.contains("sandbox")) || p.name == "spring-ldap-test" }
diff --git a/gradle/maven-deployment.gradle b/gradle/maven-deployment.gradle
index 1fa93ead..8317ffb8 100644
--- a/gradle/maven-deployment.gradle
+++ b/gradle/maven-deployment.gradle
@@ -5,8 +5,14 @@ install {
}
def customizePom(pom, gradleProject) {
+ def isWar = project.hasProperty('war')
+ def projectName = gradleProject.name
+
pom.project {
- name = gradleProject.name
+ name = projectName
+ if(isWar) {
+ packaging = "war"
+ }
description = gradleProject.name
url = 'http://www.springframework.org/ldap'
organization {
@@ -66,6 +72,49 @@ def customizePom(pom, gradleProject) {
name = "Marvin S. Addison"
}
}
+
+ if(!project.releaseBuild) {
+ repositories {
+ repository {
+ id 'spring-snasphot'
+ url 'http://repo.springsource.org/libs-snapshot'
+ }
+ }
+ }
+
+ build {
+ plugins {
+ plugin {
+ groupId = 'org.apache.maven.plugins'
+ artifactId = 'maven-compiler-plugin'
+ configuration {
+ source = '1.6'
+ target = '1.6'
+ }
+ }
+ if(isWar) {
+ plugin {
+ groupId = 'org.apache.maven.plugins'
+ artifactId = 'maven-war-plugin'
+ version = '2.3'
+ configuration {
+ failOnMissingWebXml = 'false'
+ }
+ }
+
+ plugin {
+ groupId = 'org.mortbay.jetty'
+ artifactId = 'jetty-maven-plugin'
+ version = '8.1.14.v20131031'
+ configuration {
+ webAppConfig {
+ contextPath = projectName
+ }
+ }
+ }
+ }
+ }
+ }
}
}
diff --git a/gradle/sample-war.gradle b/gradle/sample-war.gradle
new file mode 100644
index 00000000..a7adde3c
--- /dev/null
+++ b/gradle/sample-war.gradle
@@ -0,0 +1,9 @@
+apply from: JAVA_SCRIPT
+apply from: MAVEN_DEPLOYMENT_SCRIPT
+
+apply plugin: 'war'
+apply plugin: 'jetty'
+
+sonarRunner {
+ skipProject = true
+}
diff --git a/samples/plain/build.gradle b/samples/plain/build.gradle
index c3f27017..c1e636d4 100644
--- a/samples/plain/build.gradle
+++ b/samples/plain/build.gradle
@@ -1,6 +1,4 @@
-apply from: JAVA_SCRIPT
-apply plugin: 'war'
-apply plugin: 'jetty'
+apply from: SAMPLE_WAR_GRADLE
dependencies {
compile project(':spring-ldap-test'),
diff --git a/samples/simple-odm/build.gradle b/samples/simple-odm/build.gradle
index 83880974..68450cd7 100644
--- a/samples/simple-odm/build.gradle
+++ b/samples/simple-odm/build.gradle
@@ -1,5 +1,9 @@
apply from: JAVA_SCRIPT
+sonarRunner {
+ skipProject = true
+}
+
dependencies {
compile project(":spring-ldap-core"),
project(":spring-ldap-odm"),
diff --git a/samples/user-admin/build.gradle b/samples/user-admin/build.gradle
new file mode 100644
index 00000000..33061a02
--- /dev/null
+++ b/samples/user-admin/build.gradle
@@ -0,0 +1,18 @@
+apply from: SAMPLE_WAR_GRADLE
+
+dependencies {
+ compile project(':spring-ldap-test'),
+ 'javax.servlet:jstl:1.2',
+ "org.springframework:spring-context:$springVersion",
+ "org.springframework:spring-webmvc:$springVersion",
+ "com.fasterxml.jackson.core:jackson-databind:2.2.3",
+ "com.google.guava:guava:15.0"
+
+ provided "javax.servlet:javax.servlet-api:3.0.1",
+ 'javax.servlet.jsp:jsp-api:2.1'
+
+ runtime 'ch.qos.logback:logback-classic:1.0.13'
+
+ testCompile "org.springframework:spring-test:$springVersion",
+ "junit:junit:$junitVersion"
+}
\ No newline at end of file
diff --git a/samples/user-admin/pom.xml b/samples/user-admin/pom.xml
new file mode 100644
index 00000000..a4f875cc
--- /dev/null
+++ b/samples/user-admin/pom.xml
@@ -0,0 +1,159 @@
+
+
+ 4.0.0
+ org.springframework.ldap
+ spring-ldap-user-admin-sample
+ 2.0.0.CI-SNAPSHOT
+ war
+ spring-ldap-user-admin-sample
+ spring-ldap-user-admin-sample
+ http://www.springframework.org/ldap
+
+ SpringSource
+ http://springsource.org/
+
+
+
+ The Apache Software License, Version 2.0
+ http://www.apache.org/licenses/LICENSE-2.0.txt
+ repo
+
+
+
+
+ rwinch
+ Rob Winch
+ rwinch@gopivotal.com
+
+
+ marthursson
+ Mattias Hellborg Arthursson
+ mattias@261consulting.com
+ 261 Consulting
+ http://www.261consulting.com
+
+
+ ulsa
+ Ulrik Sandberg
+ ulrik.sandberg@jayway.com
+ Jayway
+ http://www.jayway.com
+
+
+
+
+ Eric Dalquist
+
+
+ Marius Scurtescu
+
+
+ Tim Terry
+
+
+ Keith Barlow
+
+
+ Paul Harvey
+
+
+ Marvin S. Addison
+
+
+
+ scm:git:git://github.com/SpringSource/spring-ldap
+ scm:git:git://github.com/SpringSource/spring-ldap
+ https://github.com/SpringSource/spring-ldap
+
+
+
+
+ maven-compiler-plugin
+
+ 1.6
+ 1.6
+
+
+
+ maven-war-plugin
+ 2.3
+
+ false
+
+
+
+ org.mortbay.jetty
+ jetty-maven-plugin
+ 8.1.14.v20131031
+
+
+ spring-ldap-user-admin-sample
+
+
+
+
+
+
+
+ spring-snasphot
+ http://repo.springsource.org/libs-snapshot
+
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.0.13
+ runtime
+
+
+ org.springframework.ldap
+ spring-ldap-test
+ 2.0.0.CI-SNAPSHOT
+ compile
+
+
+ com.fasterxml.jackson.core
+ jackson-databind
+ 2.2.3
+ compile
+
+
+ com.google.guava
+ guava
+ 15.0
+ compile
+
+
+ javax.servlet
+ jstl
+ 1.2
+ compile
+
+
+ junit
+ junit
+ 4.10
+ test
+
+
+ org.springframework
+ spring-webmvc
+ 3.2.4.RELEASE
+ compile
+
+
+ org.springframework
+ spring-context
+ 3.2.4.RELEASE
+ compile
+
+
+ org.springframework
+ spring-test
+ 3.2.4.RELEASE
+ test
+
+
+
diff --git a/samples/user-admin/readme.md b/samples/user-admin/readme.md
new file mode 100644
index 00000000..c17d18d4
--- /dev/null
+++ b/samples/user-admin/readme.md
@@ -0,0 +1,27 @@
+## User Admin Sample
+
+Sample application demonstrating how to do some real work with Spring LDAP. This is a fully functional LDAP user
+administration application. It uses many of the useful concepts in Spring LDAP and would serve as a
+good example for best practices and various useful tricks.
+
+The core Spring application context of the sample is defined in resources/applicationContext.xml.
+By default this ApplicationContext will start an in-process Apache Directory Server instance, automatically populated
+with some test data. The data will be reset every time the application is restarted.
+
+To run the example, do 'gradle jettyRun', and then navigate to http://localhost:8080/spring-ldap-user-admin-sample
+
+It is also possible to run this sample application against a remote LDAP server as opposed to starting an in-process
+LDAP server. To do this, use the following system properties:
+
+* spring.profiles.active - set this to `no-apacheds` to prevent the in-process ApacheDs to be launched
+* sample.ldap.url - the URL of the target LDAP server
+* sample.ldap.userDn - principal to use for authentication against the LDAP server
+* sample.ldap.password - authentication password
+* sample.ldap.base - the directory root to use - note that by default all data under this node will be deleted and replaced with test data
+* sample.ldap.clean - set this property to false to *not* clear the root node
+* sample.ldap.directory.type - NORMAL or AD. Specify AD if running against Active Directory in order to enable some particular AD tweaks
+
+Example:
+`gradle jettyRun -Dspring.profiles.active=no-apacheds -Dsample.ldap.url=ldaps://127.0.0.1:636 -Dsample.ldap.userDn=CN=ldaptest,CN=Users,DC=261consulting,DC=local -Dsample.ldap.password=secret -Dsample.ldap.base=ou=test,dc=261consulting,dc=local -Dsample.ldap.directory.type=AD`
+
+This sample includes Bootstrap - copyright 2013 Twitter, Inc; distributed under the Apache 2 License.
\ No newline at end of file
diff --git a/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/DepartmentRepo.java b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/DepartmentRepo.java
new file mode 100644
index 00000000..6ad402b6
--- /dev/null
+++ b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/DepartmentRepo.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2005-2013 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.ldap.samples.useradmin.domain;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Mattias Hellborg Arthursson
+ */
+public interface DepartmentRepo {
+ Map> getDepartmentMap();
+}
diff --git a/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/DirectoryType.java b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/DirectoryType.java
new file mode 100644
index 00000000..447cc4b2
--- /dev/null
+++ b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/DirectoryType.java
@@ -0,0 +1,8 @@
+package org.springframework.ldap.samples.useradmin.domain;
+
+/**
+ * @author Mattias Hellborg Arthursson
+ */
+public enum DirectoryType {
+ NORMAL, AD;
+}
diff --git a/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/Group.java b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/Group.java
new file mode 100644
index 00000000..33e42b03
--- /dev/null
+++ b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/Group.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2005-2013 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.ldap.samples.useradmin.domain;
+
+import org.springframework.ldap.odm.annotations.Attribute;
+import org.springframework.ldap.odm.annotations.DnAttribute;
+import org.springframework.ldap.odm.annotations.Entry;
+import org.springframework.ldap.odm.annotations.Id;
+
+import javax.naming.Name;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author Mattias Hellborg Arthursson
+ */
+@Entry(objectClasses = {"groupOfNames", "top"}, base = "ou=Groups")
+public class Group {
+ @Id
+ private Name id;
+
+ @Attribute(name = "cn")
+ @DnAttribute(value = "cn", index=1)
+ private String name;
+
+ @Attribute(name = "description")
+ private String description;
+
+ @Attribute(name = "member")
+ private Set members = new HashSet();
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public Set getMembers() {
+ return members;
+ }
+
+ public void addMember(Name newMember) {
+ members.add(newMember);
+ }
+
+ public void removeMember(Name member) {
+ members.remove(member);
+ }
+
+ public Name getId() {
+ return id;
+ }
+
+ public void setId(Name id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+}
diff --git a/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/GroupRepo.java b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/GroupRepo.java
new file mode 100644
index 00000000..76529bda
--- /dev/null
+++ b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/GroupRepo.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2005-2013 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.ldap.samples.useradmin.domain;
+
+import org.springframework.ldap.repository.LdapRepository;
+import org.springframework.ldap.repository.Query;
+
+import javax.naming.Name;
+import java.util.Collection;
+
+/**
+ * Spring Data-generated repository for Group administration. The methods defined in LdapRepository
+ * and its superinterfaces directly map mot {@link org.springframework.ldap.repository.support.SimpleLdapRepository}.
+ *
+ * The methods defined in {@link GroupRepoExtension} are implemented in the generated instance by 'weaving in' a reference
+ * to a bean in the applicationContext implementing the interface.
+ *
+ * In the {@link #findByName(String)} method, the filter will be automatically be generated based on
+ * naming convention; the 'ByName' constraint will be fulfilled using a filter based on the attribute mapping of
+ * the name attribute in the target entity class.
+ *
+ * The {@link #findByMember(javax.naming.Name)} acts on the Query annotation, building an
+ * {@link org.springframework.ldap.query.LdapQuery} from the annotation attributes.
+ *
+ * @author Mattias Hellborg Arthursson
+ */
+public interface GroupRepo extends LdapRepository, GroupRepoExtension {
+ public final static String USER_GROUP = "ROLE_USER";
+
+ Group findByName(String groupName);
+
+ @Query("(member={0})")
+ Collection findByMember(Name member);
+}
diff --git a/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/GroupRepoExtension.java b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/GroupRepoExtension.java
new file mode 100644
index 00000000..e18432e2
--- /dev/null
+++ b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/GroupRepoExtension.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2005-2013 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.ldap.samples.useradmin.domain;
+
+import java.util.List;
+
+/**
+ * @author Mattias Hellborg Arthursson
+ */
+public interface GroupRepoExtension {
+ List getAllGroupNames();
+ void create(Group group);
+}
diff --git a/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/User.java b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/User.java
new file mode 100644
index 00000000..9b43616a
--- /dev/null
+++ b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/User.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright 2005-2013 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.ldap.samples.useradmin.domain;
+
+import org.springframework.ldap.odm.annotations.Attribute;
+import org.springframework.ldap.odm.annotations.DnAttribute;
+import org.springframework.ldap.odm.annotations.Entry;
+import org.springframework.ldap.odm.annotations.Id;
+import org.springframework.ldap.odm.annotations.Transient;
+import org.springframework.ldap.support.LdapUtils;
+
+import javax.naming.Name;
+
+/**
+ * @author Mattias Hellborg Arthursson
+ */
+@Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" }, base = "ou=Departments")
+public class User {
+ @Id
+ private Name id;
+
+ @Attribute(name = "cn")
+ @DnAttribute(value="cn", index=3)
+ private String fullName;
+
+ @Attribute(name = "employeeNumber")
+ private int employeeNumber;
+
+ @Attribute(name = "givenName")
+ private String firstName;
+
+ @Attribute(name = "sn")
+ private String lastName;
+
+ @Attribute(name = "title")
+ private String title;
+
+ @Attribute(name = "mail")
+ private String email;
+
+ @Attribute(name = "telephoneNumber")
+ private String phone;
+
+ @DnAttribute(value="ou", index=2)
+ @Transient
+ private String unit;
+
+ @DnAttribute(value="ou", index=1)
+ @Transient
+ private String department;
+
+ public String getUnit() {
+ return unit;
+ }
+
+ public void setUnit(String unit) {
+ this.unit = unit;
+ }
+
+ public String getDepartment() {
+ return department;
+ }
+
+ public void setDepartment(String department) {
+ this.department = department;
+ }
+
+ public Name getId() {
+ return id;
+ }
+
+ public void setId(Name id) {
+ this.id = id;
+ }
+
+ public void setId(String id) {
+ this.id = LdapUtils.newLdapName(id);
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public int getEmployeeNumber() {
+ return employeeNumber;
+ }
+
+ public void setEmployeeNumber(int employeeNumber) {
+ this.employeeNumber = employeeNumber;
+ }
+
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getFullName() {
+ return fullName;
+ }
+
+ public void setFullName(String fullName) {
+ this.fullName = fullName;
+ }
+
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ User user = (User) o;
+
+ if (id != null ? !id.equals(user.id) : user.id != null) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ return id != null ? id.hashCode() : 0;
+ }
+}
diff --git a/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/UserRepo.java b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/UserRepo.java
new file mode 100644
index 00000000..1c47239f
--- /dev/null
+++ b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/UserRepo.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2005-2013 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.ldap.samples.useradmin.domain;
+
+import org.springframework.ldap.repository.LdapRepository;
+
+import java.util.List;
+
+/**
+ * @author Mattias Hellborg Arthursson
+ */
+public interface UserRepo extends LdapRepository {
+ User findByEmployeeNumber(int employeeNumber);
+ List findByLastName(String lastName);
+}
diff --git a/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/impl/DepartmentRepoImpl.java b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/impl/DepartmentRepoImpl.java
new file mode 100644
index 00000000..70b7716c
--- /dev/null
+++ b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/impl/DepartmentRepoImpl.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2005-2013 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.ldap.samples.useradmin.domain.impl;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.ldap.core.LdapTemplate;
+import org.springframework.ldap.core.NameClassPairMapper;
+import org.springframework.ldap.samples.useradmin.domain.DepartmentRepo;
+import org.springframework.ldap.support.LdapNameBuilder;
+import org.springframework.ldap.support.LdapUtils;
+
+import javax.naming.NameClassPair;
+import javax.naming.NamingException;
+import javax.naming.ldap.LdapName;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Mattias Hellborg Arthursson
+ */
+public class DepartmentRepoImpl implements DepartmentRepo {
+
+ private static final LdapName DEPARTMENTS_OU = LdapUtils.newLdapName("ou=Departments");
+ private final LdapTemplate ldapTemplate;
+
+ @Autowired
+ public DepartmentRepoImpl(LdapTemplate ldapTemplate) {
+ this.ldapTemplate = ldapTemplate;
+ }
+
+ @Override
+ public Map> getDepartmentMap() {
+ return new HashMap>(){{
+ List allDepartments = getAllDepartments();
+ for (String oneDepartment : allDepartments) {
+ put(oneDepartment, getAllUnitsForDepartment(oneDepartment));
+ }
+ }};
+ }
+
+ private List getAllDepartments() {
+ return ldapTemplate.list(DEPARTMENTS_OU, new OuValueNameClassPairMapper());
+ }
+
+ private List getAllUnitsForDepartment(String department) {
+ return ldapTemplate.list(LdapNameBuilder
+ .newLdapName(DEPARTMENTS_OU).add("ou", department).build(), new OuValueNameClassPairMapper());
+ }
+
+ private static class OuValueNameClassPairMapper implements NameClassPairMapper {
+ @Override
+ public String mapFromNameClassPair(NameClassPair nameClassPair) throws NamingException {
+ LdapName name = LdapUtils.newLdapName(nameClassPair.getName());
+ return LdapUtils.getStringValue(name, "ou");
+ }
+ }
+}
diff --git a/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/impl/GroupRepoImpl.java b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/impl/GroupRepoImpl.java
new file mode 100644
index 00000000..cb6cae0f
--- /dev/null
+++ b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/domain/impl/GroupRepoImpl.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2005-2013 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.ldap.samples.useradmin.domain.impl;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.ldap.core.AttributesMapper;
+import org.springframework.ldap.core.LdapTemplate;
+import org.springframework.ldap.core.support.BaseLdapNameAware;
+import org.springframework.ldap.query.LdapQuery;
+import org.springframework.ldap.samples.useradmin.domain.Group;
+import org.springframework.ldap.samples.useradmin.domain.GroupRepoExtension;
+import org.springframework.ldap.support.LdapUtils;
+
+import javax.naming.NamingException;
+import javax.naming.directory.Attributes;
+import javax.naming.ldap.LdapName;
+import java.util.List;
+
+import static org.springframework.ldap.query.LdapQueryBuilder.query;
+
+/**
+ * @author Mattias Hellborg Arthursson
+ */
+public class GroupRepoImpl implements GroupRepoExtension, BaseLdapNameAware {
+ private final static LdapName ADMIN_USER = LdapUtils.newLdapName("cn=System,ou=System,ou=IT,ou=Departments");
+
+ private final LdapTemplate ldapTemplate;
+ private LdapName baseLdapPath;
+
+ @Autowired
+ public GroupRepoImpl(LdapTemplate ldapTemplate) {
+ this.ldapTemplate = ldapTemplate;
+ }
+
+ @Override
+ public void setBaseLdapPath(LdapName baseLdapPath) {
+ this.baseLdapPath = baseLdapPath;
+ }
+
+ @Override
+ public List getAllGroupNames() {
+ LdapQuery query = query().attributes("cn")
+ .where("objectclass").is("groupOfNames");
+
+ return ldapTemplate.search(query, new AttributesMapper() {
+ @Override
+ public String mapFromAttributes(Attributes attributes) throws NamingException {
+ return (String) attributes.get("cn").get();
+ }
+ });
+ }
+
+ @Override
+ public void create(Group group) {
+ // A groupOfNames cannot be empty - add a system entry to all new groups.
+ group.addMember(LdapUtils.prepend(ADMIN_USER, baseLdapPath));
+ ldapTemplate.create(group);
+ }
+
+}
diff --git a/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/service/UserService.java b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/service/UserService.java
new file mode 100644
index 00000000..de373596
--- /dev/null
+++ b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/service/UserService.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2005-2013 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.ldap.samples.useradmin.service;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.ldap.core.support.BaseLdapNameAware;
+import org.springframework.ldap.samples.useradmin.domain.DirectoryType;
+import org.springframework.ldap.samples.useradmin.domain.Group;
+import org.springframework.ldap.samples.useradmin.domain.GroupRepo;
+import org.springframework.ldap.samples.useradmin.domain.User;
+import org.springframework.ldap.samples.useradmin.domain.UserRepo;
+import org.springframework.ldap.support.LdapUtils;
+
+import javax.naming.Name;
+import javax.naming.ldap.LdapName;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * @author Mattias Hellborg Arthursson
+ */
+public class UserService implements BaseLdapNameAware {
+ private final UserRepo userRepo;
+ private final GroupRepo groupRepo;
+ private LdapName baseLdapPath;
+ private DirectoryType directoryType;
+
+ @Autowired
+ public UserService(UserRepo userRepo, GroupRepo groupRepo) {
+ this.userRepo = userRepo;
+ this.groupRepo = groupRepo;
+ }
+
+ public Group getUserGroup() {
+ return groupRepo.findByName(GroupRepo.USER_GROUP);
+ }
+
+ public void setDirectoryType(DirectoryType directoryType) {
+ this.directoryType = directoryType;
+ }
+
+ @Override
+ public void setBaseLdapPath(LdapName baseLdapPath) {
+ this.baseLdapPath = baseLdapPath;
+ }
+
+ public Iterable findAll() {
+ return userRepo.findAll();
+ }
+
+ public User findUser(String userId) {
+ return userRepo.findOne(LdapUtils.newLdapName(userId));
+ }
+
+ public User createUser(User user) {
+ User savedUser = userRepo.save(user);
+
+ Group userGroup = getUserGroup();
+
+ // The DN the member attribute must be absolute
+ userGroup.addMember(toAbsoluteDn(savedUser.getId()));
+ groupRepo.save(userGroup);
+
+ return savedUser;
+ }
+
+ public LdapName toAbsoluteDn(Name relativeName) {
+ return LdapUtils.prepend(relativeName, baseLdapPath);
+ }
+
+ /**
+ * This method expects absolute DNs of group members. In order to find the actual users
+ * the DNs need to have the base LDAP path removed.
+ *
+ * @param absoluteIds
+ * @return
+ */
+ public Set findAllMembers(Iterable absoluteIds) {
+ return Sets.newLinkedHashSet(userRepo.findAll(toRelativeIds(absoluteIds)));
+ }
+
+ public Iterable toRelativeIds(Iterable absoluteIds) {
+ return Iterables.transform(absoluteIds, new Function() {
+ @Override
+ public Name apply(Name input) {
+ return LdapUtils.removeFirst(input, baseLdapPath);
+ }
+ });
+ }
+
+ public User updateUser(String userId, User user) {
+ LdapName originalId = LdapUtils.newLdapName(userId);
+ User existingUser = userRepo.findOne(originalId);
+
+ existingUser.setFirstName(user.getFirstName());
+ existingUser.setLastName(user.getLastName());
+ existingUser.setFullName(user.getFullName());
+ existingUser.setEmail(user.getEmail());
+ existingUser.setPhone(user.getPhone());
+ existingUser.setTitle(user.getTitle());
+ existingUser.setDepartment(user.getDepartment());
+ existingUser.setUnit(user.getUnit());
+
+ if (directoryType == DirectoryType.AD) {
+ return updateUserAd(originalId, existingUser);
+ } else {
+ return updateUserStandard(originalId, existingUser);
+ }
+ }
+
+
+ /**
+ * Update the user and - if its id changed - update all group references to the user.
+ *
+ * @param originalId the original id of the user.
+ * @param existingUser the user, populated with new data
+ *
+ * @return the updated entry
+ */
+ private User updateUserStandard(LdapName originalId, User existingUser) {
+ User savedUser = userRepo.save(existingUser);
+
+ if(!originalId.equals(savedUser.getId())) {
+ // The user has moved - we need to update group references.
+ LdapName oldMemberDn = toAbsoluteDn(originalId);
+ LdapName newMemberDn = toAbsoluteDn(savedUser.getId());
+
+ Collection groups = groupRepo.findByMember(oldMemberDn);
+ updateGroupReferences(groups, oldMemberDn, newMemberDn);
+ }
+ return savedUser;
+ }
+
+ /**
+ * Special behaviour in AD forces us to get the group membership before the user is updated,
+ * because AD clears group membership for removed entries, which means that once the user is
+ * update we've lost track of which groups the user was originally member of, preventing us to
+ * update the membership references so that they point to the new DN of the user.
+ *
+ * This is slightly less efficient, since we need to get the group membership for all updates
+ * even though the user may not have been moved. Using our knowledge of which attributes are
+ * part of the distinguished name we can do this more efficiently if we are implementing specifically
+ * for Active Directory - this approach is just to highlight this quite significant difference.
+ *
+ * @param originalId the original id of the user.
+ * @param existingUser the user, populated with new data
+ *
+ * @return the updated entry
+ */
+ private User updateUserAd(LdapName originalId, User existingUser) {
+ LdapName oldMemberDn = toAbsoluteDn(originalId);
+ Collection groups = groupRepo.findByMember(oldMemberDn);
+
+ User savedUser = userRepo.save(existingUser);
+ LdapName newMemberDn = toAbsoluteDn(savedUser.getId());
+
+ if(!originalId.equals(savedUser.getId())) {
+ // The user has moved - we need to update group references.
+ updateGroupReferences(groups, oldMemberDn, newMemberDn);
+ }
+ return savedUser;
+ }
+
+ private void updateGroupReferences(Collection groups, Name originalId, Name newId) {
+ for (Group group : groups) {
+ group.removeMember(originalId);
+ group.addMember(newId);
+
+ groupRepo.save(group);
+ }
+ }
+}
diff --git a/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/web/GroupController.java b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/web/GroupController.java
new file mode 100644
index 00000000..5ad35dda
--- /dev/null
+++ b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/web/GroupController.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2005-2013 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.ldap.samples.useradmin.web;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.ldap.samples.useradmin.domain.Group;
+import org.springframework.ldap.samples.useradmin.domain.GroupRepo;
+import org.springframework.ldap.samples.useradmin.domain.User;
+import org.springframework.ldap.samples.useradmin.service.UserService;
+import org.springframework.ldap.support.LdapUtils;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import java.util.Set;
+
+import static org.springframework.web.bind.annotation.RequestMethod.DELETE;
+import static org.springframework.web.bind.annotation.RequestMethod.GET;
+import static org.springframework.web.bind.annotation.RequestMethod.POST;
+
+/**
+ * @author Mattias Hellborg Arthursson
+ */
+@Controller
+public class GroupController {
+
+ @Autowired
+ private GroupRepo groupRepo;
+
+ @Autowired
+ private UserService userService;
+
+ @RequestMapping(value = "/groups", method = GET)
+ public String listGroups(ModelMap map) {
+ map.put("groups", groupRepo.getAllGroupNames());
+ return "listGroups";
+ }
+
+ @RequestMapping(value = "/newGroup", method = GET)
+ public String initNewGroup() {
+ return "newGroup";
+ }
+
+ @RequestMapping(value = "/groups", method = POST)
+ public String newGroup(Group group) {
+ groupRepo.create(group);
+
+ return "redirect:groups/" + group.getName();
+ }
+
+ @RequestMapping(value = "/groups/{name}", method = GET)
+ public String editGroup(@PathVariable String name, ModelMap map) {
+ Group foundGroup = groupRepo.findByName(name);
+ map.put("group", foundGroup);
+
+ final Set groupMembers = userService.findAllMembers(foundGroup.getMembers());
+ map.put("members", groupMembers);
+
+ Iterable otherUsers = Iterables.filter(userService.findAll(), new Predicate() {
+ @Override
+ public boolean apply(User user) {
+ return !groupMembers.contains(user);
+ }
+ });
+ map.put("nonMembers", Lists.newLinkedList(otherUsers));
+
+ return "editGroup";
+ }
+
+ @RequestMapping(value = "/groups/{name}/members", method = POST)
+ public String addUserToGroup(@PathVariable String name, @RequestParam String userId) {
+ Group group = groupRepo.findByName(name);
+ group.addMember(userService.toAbsoluteDn(LdapUtils.newLdapName(userId)));
+
+ groupRepo.save(group);
+
+ return "redirect:/groups/" + name;
+ }
+
+ @RequestMapping(value = "/groups/{name}/members", method = DELETE)
+ public String removeUserFromGroup(@PathVariable String name, @RequestParam String userId) {
+ Group group = groupRepo.findByName(name);
+ group.removeMember(userService.toAbsoluteDn(LdapUtils.newLdapName(userId)));
+
+ groupRepo.save(group);
+
+ return "redirect:/groups/" + name;
+ }
+}
diff --git a/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/web/UserController.java b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/web/UserController.java
new file mode 100644
index 00000000..11cede21
--- /dev/null
+++ b/samples/user-admin/src/main/java/org/springframework/ldap/samples/useradmin/web/UserController.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 2005-2013 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.ldap.samples.useradmin.web;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.ldap.samples.useradmin.domain.DepartmentRepo;
+import org.springframework.ldap.samples.useradmin.domain.User;
+import org.springframework.ldap.samples.useradmin.service.UserService;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.springframework.web.bind.annotation.RequestMethod.GET;
+import static org.springframework.web.bind.annotation.RequestMethod.POST;
+
+/**
+ * @author Mattias Hellborg Arthursson
+ */
+@Controller
+public class UserController {
+
+ private final AtomicInteger nextEmployeeNumber = new AtomicInteger(10);
+
+ @Autowired
+ private UserService userService;
+
+ @Autowired
+ private DepartmentRepo departmentRepo;
+
+ @RequestMapping(value = {"/", "/users"}, method = GET)
+ public String index(ModelMap map) {
+ map.put("users", userService.findAll());
+ return "listUsers";
+ }
+
+ @RequestMapping(value = "/users/{userid}", method = GET)
+ public String getUser(@PathVariable String userid, ModelMap map) throws JsonProcessingException {
+ map.put("user", userService.findUser(userid));
+ populateDepartments(map);
+ return "editUser";
+ }
+
+ @RequestMapping(value = "/newuser", method = GET)
+ public String initNewUser(ModelMap map) throws JsonProcessingException {
+ User user = new User();
+ user.setEmployeeNumber(nextEmployeeNumber.getAndIncrement());
+
+ map.put("new", true);
+ map.put("user", user);
+ populateDepartments(map);
+
+ return "editUser";
+ }
+
+ private void populateDepartments(ModelMap map) throws JsonProcessingException {
+ Map> departmentMap = departmentRepo.getDepartmentMap();
+ ObjectMapper objectMapper = new ObjectMapper();
+ String departmentsAsJson = objectMapper.writeValueAsString(departmentMap);
+ map.put("departments", departmentsAsJson);
+ }
+
+ @RequestMapping(value = "/newuser", method = POST)
+ public String createUser(User user) {
+ User savedUser = userService.createUser(user);
+
+ return "redirect:/users/" + savedUser.getId();
+ }
+
+ @RequestMapping(value = "/users/{userid}", method = POST)
+ public String updateUser(@PathVariable String userid, User user) {
+ User savedUser = userService.updateUser(userid, user);
+
+ return "redirect:/users/" + savedUser.getId();
+ }
+}
diff --git a/samples/user-admin/src/main/resources/applicationContext.xml b/samples/user-admin/src/main/resources/applicationContext.xml
new file mode 100644
index 00000000..9b4a48a2
--- /dev/null
+++ b/samples/user-admin/src/main/resources/applicationContext.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/user-admin/src/main/resources/ldap.properties b/samples/user-admin/src/main/resources/ldap.properties
new file mode 100644
index 00000000..358feee9
--- /dev/null
+++ b/samples/user-admin/src/main/resources/ldap.properties
@@ -0,0 +1,6 @@
+sample.ldap.url=ldap://127.0.0.1:18880
+sample.ldap.userDn=uid=admin,ou=system
+sample.ldap.password=secret
+sample.ldap.base=dc=example,dc=com
+sample.ldap.clean=true
+sample.ldap.directory.type=NORMAL
\ No newline at end of file
diff --git a/samples/user-admin/src/main/resources/logback.xml b/samples/user-admin/src/main/resources/logback.xml
new file mode 100644
index 00000000..afaebf8e
--- /dev/null
+++ b/samples/user-admin/src/main/resources/logback.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/user-admin/src/main/resources/setup_data.ldif b/samples/user-admin/src/main/resources/setup_data.ldif
new file mode 100644
index 00000000..135a7af1
--- /dev/null
+++ b/samples/user-admin/src/main/resources/setup_data.ldif
@@ -0,0 +1,146 @@
+dn: ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: organizationalUnit
+ou: Departments
+
+dn: ou=IT,ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: organizationalUnit
+ou: IT
+
+dn: ou=Development,ou=IT,ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: organizationalUnit
+ou: Development
+
+dn: ou=Support,ou=IT,ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: organizationalUnit
+ou: Support
+
+dn: ou=Information Services,ou=IT,ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: organizationalUnit
+ou: Information Services
+
+dn: ou=System,ou=IT,ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: organizationalUnit
+ou: System
+
+dn: ou=Accounting,ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: organizationalUnit
+ou: Accounting
+
+dn: ou=General,ou=Accounting,ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: organizationalUnit
+ou: General
+
+dn: cn=John Doe,ou=Development,ou=IT,ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+employeeNumber: 1
+mail: john.doe@example.com
+cn: John Doe
+givenName: John
+sn: Doe
+title: Senior Programmer
+telephoneNumber: +46 555-123456
+
+dn: cn=Some Dude,ou=Development,ou=IT,ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+employeeNumber: 2
+mail: some.dude@example.com
+cn: Some Dude
+givenName: Some
+sn: Dude
+title: Architect
+telephoneNumber: +46 555-123457
+
+dn: cn=John Smith,ou=Support,ou=IT,ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+employeeNumber: 3
+mail: john.smith@example.com
+cn: John Smith
+givenName: John
+sn: Smith
+title: Support Engineer
+telephoneNumber: +46 555-123458
+
+dn: cn=Mordac Preventor of IS,ou=Information Services,ou=IT,ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+employeeNumber: 3
+mail: mordac.preventor@example.com
+cn: Mordac Preventor of IS
+givenName: Mordac
+sn: Preventor
+title: I/S Engineer
+telephoneNumber: +46 555-123460
+
+dn: cn=System,ou=System,ou=IT,ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+employeeNumber: -1
+cn: System
+sn: System
+
+dn: cn=Jane Doe,ou=General,ou=Accounting,ou=Departments,dc=example,dc=com
+objectclass: top
+objectclass: person
+objectclass: organizationalPerson
+objectclass: inetOrgPerson
+employeeNumber: 4
+mail: jane.doe@example.com
+cn: Jane Doe
+givenName: Jane
+sn: Doe
+title: Accounting Responsible
+telephoneNumber: +46 555-123459
+
+dn: ou=Groups,dc=example,dc=com
+objectclass: top
+objectclass: organizationalUnit
+ou: Groups
+
+dn: cn=ROLE_USER,ou=Groups,dc=example,dc=com
+objectclass: top
+objectclass: groupOfNames
+cn: ROLE_USER
+description: Ordinary Users
+member: cn=System,ou=System,ou=IT,ou=Departments,dc=example,dc=com
+member: cn=John Doe,ou=Development,ou=IT,ou=Departments,dc=example,dc=com
+member: cn=Some Dude,ou=Development,ou=IT,ou=Departments,dc=example,dc=com
+member: cn=John Smith,ou=Support,ou=IT,ou=Departments,dc=example,dc=com
+member: cn=Jane Doe,ou=General,ou=Accounting,ou=Departments,dc=example,dc=com
+member: cn=Mordac Preventor of IS,ou=Information Services,ou=IT,ou=Departments,dc=example,dc=com
+
+dn: cn=POWER_USER,ou=Groups,dc=example,dc=com
+objectclass: top
+objectclass: groupOfNames
+cn: POWER_USER
+description: Some More Privileges
+member: cn=System,ou=System,ou=IT,ou=Departments,dc=example,dc=com
+member: cn=Mordac Preventor of IS,ou=Information Services,ou=IT,ou=Departments,dc=example,dc=com
+
+dn: cn=ROLE_ADMIN,ou=Groups,dc=example,dc=com
+objectclass: top
+objectclass: groupOfNames
+cn: ROLE_ADMIN
+description: Super Users
+member: cn=System,ou=System,ou=IT,ou=Departments,dc=example,dc=com
+member: cn=Mordac Preventor of IS,ou=Information Services,ou=IT,ou=Departments,dc=example,dc=com
\ No newline at end of file
diff --git a/samples/user-admin/src/main/webapp/WEB-INF/jsp/editGroup.jsp b/samples/user-admin/src/main/webapp/WEB-INF/jsp/editGroup.jsp
new file mode 100644
index 00000000..eccc78b9
--- /dev/null
+++ b/samples/user-admin/src/main/webapp/WEB-INF/jsp/editGroup.jsp
@@ -0,0 +1,57 @@
+<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
+
+
+