diff --git a/samples/article-spring20/.springBeans b/samples/article-spring20/.springBeans
new file mode 100644
index 00000000..fcb9ecc0
--- /dev/null
+++ b/samples/article-spring20/.springBeans
@@ -0,0 +1,25 @@
+
+cn=[fullname],ou=[company],c=[country], so
+ * the values of these attributes must be extracted from the DN. For this,
+ * we use the DistinguishedName.
+ *
+ *Mattias Hellborg Arthurssonellborg Arthursson
+ * @author Ulrik Sandberg
+ */
+ private static class PersonContextMapper implements ContextMapper {
+
+ public Object mapFromContext(Object ctx) {
+ DirContextAdapter context = (DirContextAdapter) ctx;
+ DistinguishedName dn = new DistinguishedName(context.getDn());
+ Person person = new Person();
+ person.setCountry(dn.getLdapRdn(0).getComponent().getValue());
+ person.setCompany(dn.getLdapRdn(1).getComponent().getValue());
+ person.setFullName(context.getStringAttribute("cn"));
+ person.setLastName(context.getStringAttribute("sn"));
+ person.setDescription(context.getStringAttribute("description"));
+ person.setPhone(context.getStringAttribute("telephoneNumber"));
+
+ return person;
+ }
+ }
+
+ public void setLdapTemplate(LdapTemplate ldapTemplate) {
+ this.ldapTemplate = ldapTemplate;
+ }
+}
diff --git a/samples/article-spring20/src/main/java/org/springframework/ldap/samples/article/dao/TraditionalPersonDaoImpl.java b/samples/article-spring20/src/main/java/org/springframework/ldap/samples/article/dao/TraditionalPersonDaoImpl.java
new file mode 100644
index 00000000..9e47adb9
--- /dev/null
+++ b/samples/article-spring20/src/main/java/org/springframework/ldap/samples/article/dao/TraditionalPersonDaoImpl.java
@@ -0,0 +1,377 @@
+/*
+ * Copyright 2005-2008 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.article.dao;
+
+import java.util.Hashtable;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.naming.Context;
+import javax.naming.NameNotFoundException;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.BasicAttribute;
+import javax.naming.directory.BasicAttributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+
+import org.apache.commons.lang.StringUtils;
+import org.springframework.ldap.samples.article.domain.Person;
+
+/**
+ * Traditional implementation of PersonDao. This implementation uses the basic
+ * JNDI interfaces and classes {@link DirContext}, {@link Attributes},
+ * {@link Attribute}, and {@link NamingEnumeration}. The purpose is to
+ * contrast this implementation with that of {@link PersonDaoImpl}.
+ *
+ * @author Mattias Hellborg Arthursson
+ * @author Ulrik Sandberg
+ */
+public class TraditionalPersonDaoImpl implements
+ PersonDao {
+
+ private String userName;
+
+ private String password;
+
+ private String url;
+
+ private String base;
+
+ /*
+ * @see PersonDao#create(Person)
+ */
+ public void create(Person person) {
+ DirContext ctx = createAuthenticatedContext();
+ String dn = buildDn(person);
+ try {
+ Attributes attrs = getAttributesToBind(person);
+ ctx.bind(dn, null, attrs);
+ } catch (NamingException e) {
+ throw new RuntimeException(e);
+ } finally {
+ if (ctx != null) {
+ try {
+ ctx.close();
+ } catch (Exception e) {
+ // Never mind this.
+ }
+ }
+ }
+ }
+
+ /*
+ * @see PersonDao#update(Person)
+ */
+ public void update(Person person) {
+ DirContext ctx = createAuthenticatedContext();
+ String dn = buildDn(person);
+ try {
+ ctx
+ .rebind(
+ dn, null,
+ getAttributesToBind(person));
+ } catch (NamingException e) {
+
+ throw new RuntimeException(e);
+ } finally {
+ if (ctx != null) {
+ try {
+ ctx.close();
+ } catch (Exception e) {
+ // Never mind this.
+ }
+ }
+ }
+ }
+
+ /*
+ * @see PersonDao#delete(Person)
+ */
+ public void delete(Person person) {
+ DirContext ctx = createAuthenticatedContext();
+ String dn = buildDn(person);
+ try {
+ ctx.unbind(dn);
+ } catch (NamingException e) {
+ throw new RuntimeException(e);
+ } finally {
+ if (ctx != null) {
+ try {
+ ctx.close();
+ } catch (Exception e) {
+ // Never mind this.
+ }
+ }
+ }
+ }
+
+ /*
+ * @see PersonDao#getAllPersonNames()
+ */
+ public List getAllPersonNames() {
+ DirContext ctx = createAnonymousContext();
+
+ LinkedList list = new LinkedList();
+ NamingEnumeration results = null;
+ try {
+ SearchControls controls = new SearchControls();
+ controls
+ .setSearchScope(SearchControls.SUBTREE_SCOPE);
+ results = ctx.search(
+ "", "(objectclass=person)", controls);
+
+ while (results.hasMore()) {
+ SearchResult searchResult = (SearchResult) results
+ .next();
+ Attributes attributes = searchResult
+ .getAttributes();
+ Attribute attr = attributes.get("cn");
+ String cn = (String) attr.get();
+ list.add(cn);
+ }
+ } catch (NamingException e) {
+ throw new RuntimeException(e);
+ } finally {
+ if (results != null) {
+ try {
+ results.close();
+ } catch (Exception e) {
+ // Never mind this.
+ }
+ }
+ if (ctx != null) {
+ try {
+ ctx.close();
+ } catch (Exception e) {
+ // Never mind this.
+ }
+ }
+ }
+ return list;
+ }
+
+ /*
+ * @see PersonDao#findAll()
+ */
+ public List findAll() {
+ DirContext ctx = createAnonymousContext();
+
+ LinkedList list = new LinkedList();
+ NamingEnumeration results = null;
+ try {
+ SearchControls controls = new SearchControls();
+ controls
+ .setSearchScope(SearchControls.SUBTREE_SCOPE);
+ results = ctx.search(
+ "", "(objectclass=person)", controls);
+
+ while (results.hasMore()) {
+ SearchResult searchResult = (SearchResult) results
+ .next();
+ String dn = searchResult.getName();
+ Attributes attributes = searchResult
+ .getAttributes();
+ list.add(mapToPerson(dn, attributes));
+ }
+ } catch (NamingException e) {
+ throw new RuntimeException(e);
+ } finally {
+ if (results != null) {
+ try {
+ results.close();
+ } catch (Exception e) {
+ // Never mind this.
+ }
+ }
+ if (ctx != null) {
+ try {
+ ctx.close();
+ } catch (Exception e) {
+ // Never mind this.
+ }
+ }
+ }
+ return list;
+ }
+
+ /*
+ * @see PersonDao#findByPrimaryKey(java.lang.String, java.lang.String,
+ * java.lang.String)
+ */
+ public Person findByPrimaryKey(String country,
+ String company, String fullname) {
+
+ DirContext ctx = createAnonymousContext();
+ String dn = buildDn(
+ country, company, fullname);
+ try {
+ Attributes attributes = ctx
+ .getAttributes(dn);
+ return mapToPerson(dn, attributes);
+ } catch (NameNotFoundException e) {
+ throw new RuntimeException(
+ "Did not find entry with primary key '"
+ + dn + "'", e);
+ } catch (NamingException e) {
+ throw new RuntimeException(e);
+ } finally {
+ if (ctx != null) {
+ try {
+ ctx.close();
+ } catch (Exception e) {
+ // Never mind this.
+ }
+ }
+ }
+ }
+
+ private String buildDn(Person person) {
+ return buildDn(person.getCountry(), person
+ .getCompany(), person.getFullName());
+ }
+
+ private String buildDn(String country, String company,
+ String fullname) {
+ StringBuffer sb = new StringBuffer();
+ sb.append("cn=");
+ sb.append(fullname);
+ sb.append(", ");
+ sb.append("ou=");
+ sb.append(company);
+ sb.append(", ");
+ sb.append("c=");
+ sb.append(country);
+ String dn = sb.toString();
+ return dn;
+ }
+
+ private DirContext createContext(Hashtable env) {
+ env.put(
+ Context.INITIAL_CONTEXT_FACTORY,
+ "com.sun.jndi.ldap.LdapCtxFactory");
+ String tempUrl = createUrl();
+ env.put(Context.PROVIDER_URL, tempUrl);
+ DirContext ctx;
+ try {
+ ctx = new InitialDirContext(env);
+ } catch (NamingException e) {
+ throw new RuntimeException(e);
+ }
+ return ctx;
+ }
+
+ private DirContext createAnonymousContext() {
+ Hashtable env = new Hashtable();
+ return createContext(env);
+ }
+
+ private DirContext createAuthenticatedContext() {
+ Hashtable env = new Hashtable();
+ env.put(
+ Context.SECURITY_AUTHENTICATION,
+ "simple");
+ env.put(
+ Context.SECURITY_PRINCIPAL, userName);
+ env.put(
+ Context.SECURITY_CREDENTIALS, password);
+ return createContext(env);
+ }
+
+ private Attributes getAttributesToBind(
+ Person person) {
+ Attributes attrs = new BasicAttributes();
+ BasicAttribute ocattr = new BasicAttribute(
+ "objectclass");
+ ocattr.add("top");
+ ocattr.add("person");
+ attrs.put(ocattr);
+ attrs.put("cn", person.getFullName());
+ attrs.put("sn", person.getLastName());
+ attrs.put("description", person
+ .getDescription());
+ attrs.put("telephoneNumber", person
+ .getPhone());
+ return attrs;
+ }
+
+ private Person mapToPerson(String dn,
+ Attributes attributes)
+ throws NamingException {
+ Person person = new Person();
+ person.setFullName((String) attributes.get(
+ "cn").get());
+ person.setLastName((String) attributes.get(
+ "sn").get());
+ person.setDescription((String) attributes
+ .get("description").get());
+ person.setPhone((String) attributes.get(
+ "telephoneNumber").get());
+
+ // Remove any trailing spaces after comma
+ String cleanedDn = dn
+ .replaceAll(", *", ",");
+
+ String countryMarker = ",c=";
+ int countryIndex = cleanedDn
+ .lastIndexOf(countryMarker);
+
+ String companyMarker = ",ou=";
+ int companyIndex = cleanedDn
+ .lastIndexOf(companyMarker);
+
+ String country = cleanedDn
+ .substring(countryIndex
+ + countryMarker.length());
+ person.setCountry(country);
+ String company = cleanedDn.substring(
+ companyIndex + companyMarker.length(),
+ countryIndex);
+ person.setCompany(company);
+ return person;
+ }
+
+ private String createUrl() {
+ String tempUrl = url;
+ if (!tempUrl.endsWith("/")) {
+ tempUrl += "/";
+ }
+ if (StringUtils.isNotEmpty(base)) {
+ tempUrl += base;
+ }
+ return tempUrl;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public void setBase(String base) {
+ this.base = base;
+ }
+
+ public void setPassword(String credentials) {
+ this.password = credentials;
+ }
+
+ public void setUserDn(String principal) {
+ this.userName = principal;
+ }
+}
diff --git a/samples/article-spring20/src/main/java/org/springframework/ldap/samples/article/domain/Person.java b/samples/article-spring20/src/main/java/org/springframework/ldap/samples/article/domain/Person.java
new file mode 100644
index 00000000..d56d4afd
--- /dev/null
+++ b/samples/article-spring20/src/main/java/org/springframework/ldap/samples/article/domain/Person.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2005-2008 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.article.domain;
+
+import org.apache.commons.lang.builder.EqualsBuilder;
+import org.apache.commons.lang.builder.HashCodeBuilder;
+import org.apache.commons.lang.builder.ToStringBuilder;
+import org.apache.commons.lang.builder.ToStringStyle;
+
+/**
+ * Simple class representing a single person.
+ *
+ * @author Mattias Hellborg Arthursson
+ * @author Ulrik Sandberg
+ */
+public class Person {
+ private String fullName;
+
+ private String lastName;
+
+ private String description;
+
+ private String country;
+
+ private String company;
+
+ private String phone;
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ 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 getCompany() {
+ return company;
+ }
+
+ public void setCompany(String company) {
+ this.company = company;
+ }
+
+ public String getCountry() {
+ return country;
+ }
+
+ public void setCountry(String country) {
+ this.country = country;
+ }
+
+ public String getPhone() {
+ return phone;
+ }
+
+ public void setPhone(String phone) {
+ this.phone = phone;
+ }
+
+ public boolean equals(Object obj) {
+ return EqualsBuilder.reflectionEquals(
+ this, obj);
+ }
+
+ public int hashCode() {
+ return HashCodeBuilder
+ .reflectionHashCode(this);
+ }
+
+ public String toString() {
+ return ToStringBuilder.reflectionToString(
+ this, ToStringStyle.MULTI_LINE_STYLE);
+ }
+}
diff --git a/samples/article-spring20/src/main/java/org/springframework/ldap/samples/article/web/DefaultController.java b/samples/article-spring20/src/main/java/org/springframework/ldap/samples/article/web/DefaultController.java
new file mode 100644
index 00000000..71accf2c
--- /dev/null
+++ b/samples/article-spring20/src/main/java/org/springframework/ldap/samples/article/web/DefaultController.java
@@ -0,0 +1,132 @@
+package org.springframework.ldap.samples.article.web;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.commons.lang.StringUtils;
+import org.springframework.ldap.core.DirContextOperations;
+import org.springframework.ldap.core.DistinguishedName;
+import org.springframework.ldap.samples.article.dao.PersonDao;
+import org.springframework.ldap.samples.article.domain.Person;
+import org.springframework.ldap.samples.utils.HtmlRowLdapTreeVisitor;
+import org.springframework.ldap.samples.utils.LdapTree;
+import org.springframework.ldap.samples.utils.LdapTreeBuilder;
+import org.springframework.web.servlet.ModelAndView;
+import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
+
+/**
+ * Default controller.
+ *
+ * @author Mattias Hellborg Arthursson
+ * @author Ulrik Sandberg
+ */
+public class DefaultController extends MultiActionController {
+
+ private LdapTreeBuilder ldapTreeBuilder;
+
+ public void setLdapTreeBuilder(LdapTreeBuilder ldapTreeBuilder) {
+ this.ldapTreeBuilder = ldapTreeBuilder;
+ }
+
+ public void setPersonDao(PersonDao personDao) {
+ this.personDao = personDao;
+ }
+
+ private PersonDao personDao;
+
+ public ModelAndView welcomeHandler(HttpServletRequest request, HttpServletResponse response) {
+ return new ModelAndView("welcome");
+ }
+
+ public ModelAndView showTree(HttpServletRequest request, HttpServletResponse response) {
+ LdapTree ldapTree = ldapTreeBuilder.getLdapTree(DistinguishedName.EMPTY_PATH);
+ HtmlRowLdapTreeVisitor visitor = new PersonLinkHtmlRowLdapTreeVisitor();
+ ldapTree.traverse(visitor);
+ return new ModelAndView("showTree", "rows", visitor.getRows());
+ }
+
+ public ModelAndView addPerson(HttpServletRequest request, HttpServletResponse response) {
+ Person person = getPerson();
+
+ personDao.create(person);
+ return showTree(request, response);
+ }
+
+ public ModelAndView updatePhoneNumber(HttpServletRequest request, HttpServletResponse response) {
+ Person person = personDao.findByPrimaryKey("Sweden", "company1", "John Doe");
+ person.setPhone(StringUtils.join(new String[] { person.getPhone(), "0" }));
+
+ personDao.update(person);
+ return showTree(request, response);
+ }
+
+ public ModelAndView removePerson(HttpServletRequest request, HttpServletResponse response) {
+ Person person = getPerson();
+
+ personDao.delete(person);
+ return showTree(request, response);
+ }
+
+ public ModelAndView showPerson(HttpServletRequest request, HttpServletResponse response, Person query) {
+ String country = query.getCountry();
+ String company = query.getCompany();
+ String fullName = query.getFullName();
+ Person person = personDao.findByPrimaryKey(country, company, fullName);
+ return new ModelAndView("showPerson", "person", person);
+ }
+
+ private Person getPerson() {
+ Person person = new Person();
+ person.setFullName("John Doe");
+ person.setLastName("Doe");
+ person.setCompany("company1");
+ person.setCountry("Sweden");
+ person.setDescription("Test user");
+ return person;
+ }
+
+ /**
+ * Generates appropriate links for person leaves in the tree.
+ *
+ * @author Mattias Hellborg Arthursson
+ */
+ private static final class PersonLinkHtmlRowLdapTreeVisitor extends HtmlRowLdapTreeVisitor {
+ @Override
+ protected String getLinkForNode(DirContextOperations node) {
+ String[] objectClassValues = node.getStringAttributes("objectClass");
+ if (containsValue(objectClassValues, "person")) {
+ DistinguishedName distinguishedName = (DistinguishedName) node.getDn();
+ String country = encodeValue(distinguishedName.getValue("c"));
+ String company = encodeValue(distinguishedName.getValue("ou"));
+ String fullName = encodeValue(distinguishedName.getValue("cn"));
+
+ return "showPerson.do?country=" + country + "&company=" + company + "&fullName=" + fullName;
+ }
+ else {
+ return super.getLinkForNode(node);
+ }
+ }
+
+ private String encodeValue(String value) {
+ try {
+ return URLEncoder.encode(value, "UTF8");
+ }
+ catch (UnsupportedEncodingException e) {
+ // Not supposed to happen
+ throw new RuntimeException("Unexpected encoding exception", e);
+ }
+ }
+
+ private boolean containsValue(String[] values, String value) {
+ for (String oneValue : values) {
+ if (StringUtils.equals(oneValue, value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/samples/article-spring20/src/main/java/overview.html b/samples/article-spring20/src/main/java/overview.html
new file mode 100644
index 00000000..309641cf
--- /dev/null
+++ b/samples/article-spring20/src/main/java/overview.html
@@ -0,0 +1,3 @@
+
+
+Full name: ${person.fullName}
+
+LastName: ${person.lastName}
+
+Description: ${person.description}
+
+Country: ${person.country}
+
+Company: ${person.company}
+
+Phone: ${person.phone}
+
+
+