Spring-LDAP-实践教程-二-

156 阅读30分钟

Spring LDAP 实践教程(二)

原文:Practical Spring LDAP

协议:CC BY-NC-SA 4.0

六、搜索 LDAP

在本章中,您将学习

  • LDAP 搜索的基础
  • 使用过滤器的 LDAP 搜索
  • 创建自定义搜索过滤器

搜索信息是对 LDAP 执行的最常见的操作。客户端应用通过传递搜索标准来启动 LDAP 搜索,搜索标准是决定在哪里搜索和搜索什么的信息。收到请求后,LDAP 服务器执行搜索并返回所有符合条件的条目。

LDAP 搜索标准

LDAP 搜索标准由三个强制参数(基本、范围和过滤器)和几个可选参数组成。让我们详细看看这些参数。

基本参数

搜索的基本部分是标识将被搜索的树的分支的可分辨名称(DN)。例如,基数“ou =顾客,dc=inflinx,dc=com”表示搜索将从顾客分支开始并向下移动。也可以指定一个空的基,这将导致搜索根 DSE 条目。

image 注意根 DSE 或 DSA 特定条目是 LDAP 服务器中的一个特殊条目。它通常保存特定于服务器的数据,如供应商名称、供应商版本以及它支持的不同控件和功能。

范围参数

scope 参数确定需要执行的 LDAP 搜索相对于基准的深度。LDAP 协议定义了三种可能的搜索范围:基本、一级和子树。图 6-1 显示了在不同搜索范围内被评估的条目。

9781430263975_Fig06-01.jpg

图 6-1 。搜索范围

  • 基本范围将搜索限制到由基本参数标识的 LDAP 条目。搜索中不会包含其他条目。在您的库应用模式中,使用基本 DN dc=inflinx,dc=com 和基本范围,搜索将只返回根组织条目,如图 6-1 所示。

一级范围表示搜索直接在基础下一级的所有条目。搜索中不包括基本条目本身。因此,使用 base dc=inflinx,dc=com 和 scope one 级别,搜索所有条目将返回雇员和顾客组织单位。

最后,子树范围包括搜索中的基本条目及其所有后代条目。这是三个选项中最慢、最贵的一个。在您的库示例中,使用这个范围和 base dc=inflinx,dc=com 进行搜索将返回所有条目。

过滤参数

在您的图书馆应用 LDAP 服务器中,假设您想要查找住在 Midvale 地区的所有顾客。从 LDAP 模式中,您知道 patron 条目具有 city 属性,该属性保存他们居住的城市名称。所以这个需求本质上可以归结为检索所有具有值为“Midvale”的 city 属性的条目。这正是搜索过滤器的作用。搜索过滤器定义了所有返回条目拥有的特征。从逻辑上讲,过滤器应用于由 base 和 scope 标识的集合中的每个条目。只有与过滤器匹配的条目才成为返回的搜索结果的一部分。

LDAP 搜索过滤器由三部分组成:属性类型、操作符和属性值(或值的范围)。根据运算符的不同,值部分可以是可选的。这些组件必须始终用括号括起来,就像这样:

Filter =  (attributetype  operator value)

有了这些信息,查找住在 Midvale 的所有顾客的搜索过滤器应该是这样的:

(city=Midvale)

现在,假设你想找到所有住在 Midvale 地区的顾客,他们都有一个电子邮件地址,这样你就可以给他们发送一些图书馆活动的新闻。结果搜索过滤器实际上是两个过滤器项目的组合:一个项目标识 Midvale 市的顾客,另一个项目标识有电子邮件地址的顾客。您已经看到了过滤器的第一项。这是过滤器的另一部分:

(mail=*)

=*运算符指示属性的存在。因此,表达式 mail=*将返回所有在邮件属性中有值的条目。LDAP 规范定义了可用于组合多个过滤器和创建复杂过滤器的过滤器操作符。以下是组合过滤器的格式:

Filter =  (operator filter1 filter2)

注意前缀符号的使用,其中运算符写在操作数之前,用于组合两个过滤器。以下是您的用例所需的过滤器:

(&(city=Midvale)(mail=*))

这个过滤器中的&是一个操作符。LDAP 规范定义了各种搜索过滤器操作符。表 6-1 列出了一些常用的运算符。

表 6-1 。搜索过滤运算符

Tab06-01.jpg

可选参数

除了上述三个参数之外,还可以包括几个可选参数来控制搜索行为。例如,timelimit 参数指示允许完成搜索的时间。类似地,sizelimit 参数对可以作为结果的一部分返回的条目数量设置了上限。

一个非常常用的可选参数包括提供属性名列表。执行搜索时,缺省情况下,LDAP 服务器会返回与搜索中找到的条目相关联的所有属性。有时这可能并不理想。在这些场景中,您可以提供一个属性名称列表作为搜索的一部分,LDAP 服务器将只返回具有这些属性的条目。下面是 LdapTemplate 中的一个搜索方法示例,它采用一个属性名称数组(ATTR_1、ATTR_2 和 ATTR_3):

ldapTemplate.search("SEARCH_BASE", "uid=USER_DN", 1, new String[]{"ATTR_1", "ATTR_2", ATTR_3}, new SomeContextMapperImpl());

执行此搜索时,返回的条目将只有 ATTR_1、ATTR_2 和 ATTR_3。这可以减少从服务器传输的数据量,在高流量情况下非常有用。

从版本 3 开始,LDAP 服务器可以维护每个条目的属性,这完全是出于管理目的。这些属性被称为操作属性,不是条目对象类的一部分。执行 LDAP 搜索时,默认情况下返回的条目将不包含操作属性。为了检索操作属性,您需要在搜索条件中提供操作属性名称列表。

image 注意操作属性的例子包括 createTimeStamp 和 pwdAccountLockedTime,前者保存条目创建的时间,后者记录用户帐户被锁定的时间。

LDAP 注入

LDAP 注入是一种技术,攻击者通过改变 LDAP 查询来对目录服务器运行任意 LDAP 语句。LDAP 注入可能导致未经授权的数据访问或对 LDAP 树的修改。不执行正确的输入验证或清理输入的应用容易受到 LDAP 注入。这种技术类似于流行的针对数据库的 SQL 注入攻击。

为了更好地理解 LDAP 注入,考虑一个使用 LDAP 进行身份验证的 web 应用。这种应用通常提供一个网页,让用户输入自己的用户名和密码。为了验证用户名和密码是否匹配,应用将构建一个 LDAP 搜索查询,大致如下所示:

(&(uid =用户输入 UID)(密码=用户输入 PWD))

让我们假设应用简单地信任用户输入,并且不执行任何验证。现在,如果您输入文本 jdoe)(&)(作为用户名,并输入任意随机文本作为密码,则搜索查询过滤器将如下所示:

(&(uid=jdoe)(&)(密码=随机)

如果用户名 jdoe 是 LDAP 中的一个有效用户 id,那么不管输入的密码是什么,该查询将始终计算为 true。这种 LDAP 注入将允许攻击者绕过身份验证进入应用。www . black hat . com/presentations/BH-Europe-08/alon so-Parada/white paper/BH-eu-08-alon so-Parada-WP . pdf 上的“LDAP 注入和盲 LDAP 注入”文章详细讨论了各种 LDAP 注入技术。

一般来说,防止 LDAP 注入和任何其他注入技术都是从正确的输入验证开始的。在搜索过滤器中使用输入的数据之前,对其进行净化和正确编码是非常重要的。

Spring LDAP 过滤器

在上一节中,您了解了 LDAP 搜索过滤器对于缩小搜索范围和识别条目非常重要。然而,动态创建 LDAP 过滤器可能会很繁琐,尤其是在尝试组合多个过滤器时。确保所有的大括号都正确闭合是容易出错的。适当转义特殊字符也很重要。

Spring LDAP 提供了几个过滤器类,使得创建和编码 LDAP 过滤器变得容易。所有这些过滤器都实现了过滤器接口,并且是 org . spring framework . LDAP . Filter 包的一部分。清单 6-1 显示了过滤器 API 接口。

清单 6-1。

package org.springframework.ldap.filter;

public interface Filter {
   String encode();
   StringBuffer encode(StringBuffer buf);
   boolean equals(Object o);
   int hashCode();
}

该接口中的第一个编码方法返回过滤器的字符串表示。第二个 encode 方法接受 StringBuffer 作为其参数,并将过滤器的编码版本作为 StringBuffer 返回。对于常规的开发过程,您使用返回 String 的 encode 方法的第一个版本。

过滤界面层次如图图 6-2 所示。从层次结构中,您可以看到 AbstractFilter 实现了过滤器接口,并作为所有其他过滤器实现的根类。BinaryLogicalFilter 是二进制逻辑运算(如 AND 和 or)的抽象超类。CompareFilter 是过滤器的抽象超类,用于比较 EqualsFilter 和 LessThanOrEqualsFilter 等值。

9781430263975_Fig06-02.jpg

图 6-2 。过滤器层次结构

image 注意默认情况下,大多数 LDAP 属性值在搜索时不区分大小写。

在接下来的章节中,你将会看到图 6-2 中的每一个过滤器。在此之前,让我们创建一个可重用的方法来帮助您测试您的过滤器。清单 6-2 显示了 searchAndPrintResults 方法,它使用传入的过滤器实现参数并使用它执行搜索。然后,它将搜索结果输出到控制台。注意,您将搜索 LDAP 树的 Patron 分支。

清单 6-2。

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.ldap.core.DirContextOperations;
import org.springframework.ldap.core.simple.AbstractParameterizedContextMapper;
import org.springframework.ldap.core.simple.SimpleLdapTemplate;
import org.springframework.ldap.filter.Filter;
import org.springframework.stereotype.Component;

@Component("searchFilterDemo" )
public class SearchFilterDemo {

   @Autowired
   @Qualifier("ldapTemplate" )
   private SimpleLdapTemplate ldapTemplate;

   public void searchAndPrintResults(Filter filter) {
      List<String> results = ldapTemplate.search("ou=patrons,dc=inflinx,dc=com", filter.encode(),
             new AbstractParameterizedContextMapper<String>() {
            @Override
            protected String doMapFromContext(DirContextOperations context) {
              return context.getStringAttribute("cn");
            }
          });

       System.out.println("Results found in search: " + results.size());
         for(String commonName: results) {
            System.out.println(commonName);
         }
       }
   }
}

等于过滤器

EqualsFilter 可用于检索具有指定属性和值的所有条目。假设您想要检索名字为 Jacob 的所有顾客。为此,您需要创建一个新的 EqualsFilter 实例。

EqualsFilter filter =  new  EqualsFilter("givenName", "Jacob");

构造函数的第一个参数是属性名,第二个参数是属性值。对此过滤器调用 encode 方法会产生字符串(givenName=Jacob)。

清单 6-3 显示了调用 searchAndPrintResults 的测试用例,上面的 EqualsFilter 作为参数。该方法的控制台输出也显示在清单中。注意,结果中有名字为 jacob 的顾客(注意小写的 j)。这是因为 sn 属性和大多数 LDAP 属性一样,在模式中被定义为不区分大小写。

清单 6-3。

@Test
public void testEqualsFilter() {
   Filter filter = new EqualsFilter("givenName", "Jacob");
   searchFilterDemo.searchAndPrintResults(filter);
}

Results  found in  search:  2
Jacob  Smith
jacob  Brady

lieffilter〔??〕

当只知道属性的一部分值时,LikeFilter 对于搜索 LDAP 很有用。LDAP 规范允许使用通配符*来描述这些部分值。假设您想要检索名字以“Ja”开头的所有用户为此,创建 LikeFilter 的一个新实例,并将通配符子字符串作为属性值传入。

LikeFilter  filter =  new  LikeFilter("givenName", "Ja*");

在这个过滤器上调用 encode 方法会产生字符串(givenName=Ja*)。清单 6-4 显示了使用 LikeFilter 调用 searchAndPrintResults 方法的测试用例及结果。

清单 6-4。

@Test
public void testLikeFilter() {
   Filter filter = new LikeFilter("givenName", "Ja*");
   searchFilterDemo.searchAndPrintResults(filter);
}

Results  found in  search:  3
Jacob Smith
Jason Brown
jacob  Brady

子字符串中的通配符*用于匹配零个或多个字符。然而,了解 LDAP 搜索过滤器不支持正则表达式是非常重要的。表 6-2 列出了一些子串示例。

表 6-2 。LDAP 子字符串示例

LDAP 子字符串描述
(givenName=*son)匹配所有名字以 son 结尾的顾客。
(给定名称 = J *n)匹配名字以 J 开头以 n 结尾的所有顾客。
(给定名称=a)匹配名字中包含字符 a 的所有顾客。
(给定名称=Jsn)匹配名字以 J 开头、包含字符 s 并以 n 结尾的顾客。

您可能想知道 LikeFilter 的必要性,因为您可以通过简单地使用 EqualsFilter 来完成相同的筛选表达式,如下所示:

EqualsFilter filter =  new  EqualsFiler("uid", "Ja*");

在这种情况下使用 EqualsFilter 不起作用,因为 EqualsFilter 中的 encode 方法将 Ja中的通配符视为特殊字符,并正确地对其进行转义。因此,当用于搜索时,上面的过滤器将产生名字以 Ja*开头的所有条目。

演示过滤器

PresentFilters 对于检索在给定属性中至少有一个值的 LDAP 条目很有用。考虑前面的场景,您希望检索所有拥有电子邮件地址的顾客。为此,您需要创建一个 PresentFilter,如下所示:

PresentFilter presentFilter =  new  PresentFilter("email");

在 presentFilter 实例上调用 encode 方法会产生字符串(email=*)。清单 6-5 显示了使用上面的 presentFilter 调用 searchAndPrintResults 方法时的测试代码和结果。

清单 6-5。

@Test
public void testPresentFilter() {
   Filter filter = new PresentFilter("mail");
   searchFilterDemo.searchAndPrintResults(filter);
}
Results  found in  search:  97
Jacob  Smith
Aaren  Atp
Aarika  Atpco
Aaron Atrc
Aartjan  Aalders
Abagael  Aasen
Abagail  Abadines
.........
.........

notes present filter

NotPresentFilters 用于检索没有指定属性的条目。条目中没有任何值的属性被视为不存在。现在,假设您想要检索所有没有电子邮件地址的顾客。为此,创建 NotPresentFilter 的一个实例,如下所示:

NotPresentFilter notPresentFilter  =  new NotPresentFilter("email");

notPresentFilter 的编码版本产生表达式!(邮箱=*)。运行 searchAndPrintResults 会产生如清单 6-6 所示的输出。第一个空值用于组织单位条目“ou = customers,dc=inflinx,dc=com”。

清单 6-6。

@Test
public void testNotPresentFilter() {
   Filter filter = new NotPresentFilter("mail");
   searchFilterDemo.searchAndPrintResults(filter);
}

Results  found in  search:  5
null
Addons Achkar
Adeniyi Adamowicz
Adoree Aderhold
Adorne  Adey

不过滤

NotFilter 对于检索与给定条件不匹配的条目很有用。在“LikeFilter”一节中,您看到了检索所有以 Ja 开头的条目。现在假设您想要检索所有不以 Ja 开头的条目。这就是 NotFilter 发挥作用的地方。下面是实现这一要求的代码:

NotFilter notFilter  =  new  NotFilter(new LikeFilter("givenName", "Ja*"));

对该过滤器进行编码会产生字符串!(givenName=Ja*)。如您所见,NotFilter 只是添加了否定符号(!)传递给传递给其构造函数的筛选器。调用 searchAndShowResults 方法会产生清单 6-7 中的输出。

清单 6-7。

@Test
public void testNotFilter() {
   NotFilter notFilter = new NotFilter(new LikeFilter("givenName", "Ja*"));
   searchFilterDemo.searchAndPrintResults(notFilter);
}
Results  found in  search:  99
Aaren Atp  Aarika
Atpco Aaron Atrc
Aartjan  Aalders
Abagael  Aasen
Abagail  Abadines
.........................

也可以将 NotFilter 和 PresentFilter 组合起来创建与 NotPresentFilter 等效的表达式。下面是一个新的实现,它获取所有没有电子邮件地址的条目:

NotFilter notFilter  =  new  NotFilter(new PresentFilter("email"));

greater than requialsfilter〔??〕

GreaterThanOrEqualsFilter 对于匹配所有在字典上等于或大于给定属性值的条目非常有用。例如,可以使用搜索表达式(给定名称> = Jacob)来检索除 Jacob 之外按字母顺序位于 Jacob 之后的给定名称的所有条目。清单 6-8 显示了这个实现以及输出结果。

清单 6-8。

@Test
public void testGreaterThanOrEqualsFilter() {
   Filter filter = new GreaterThanOrEqualsFilter("givenName", "Jacob");
   searchFilterDemo.searchAndPrintResults(filter);
}

Results  found in  search:  3
Jacob Smith
jacob Brady
Jason Brown

lesthanorequalfilter〔??〕牌

LessThanOrEqualsFilter 可用于匹配在字典上等于或低于给定属性的条目。因此,搜索表达式(givenName <=Jacob) will return all entries with first name alphabetically lower or equal to Jacob. 清单 6-9 显示了调用该需求的 searchAndPrintResults 实现的测试代码以及输出。

清单 6-9。

@Test
public void testLessThanOrEqualsFilter() {
   Filter filter = new LessThanOrEqualsFilter("givenName", "Jacob");
   searchFilterDemo.searchAndPrintResults(filter);
}

Results  found in  search:  100
Jacob  Smith
Aaren  Atp
Aarika  Atpco
Aaron Atrc
Aartjan  Aalders
Abagael  Aasen
Abagail  Abadines
Abahri Abazari
....................

如上所述,搜索包括名字为 James 的条目。LDAP 规范不提供小于(

NotFilter lessThanFilter = new NotFilter(new GreaterThanOrEqualsFilter("givenName", "James"));

安过滤器〔??〕

AndFilter 用于组合多个搜索过滤器表达式,以创建复杂的搜索过滤器。结果过滤器将匹配满足所有子过滤器条件的条目。例如,AndFilter 适合于实现一个更早的要求,即获取所有居住在 Midvale 地区并有电子邮件地址的顾客。以下代码显示了这种实现:

AndFilter andFilter  =  new  AndFilter();
andFilter.and(new EqualsFilter("postalCode",  "84047"));
andFilter.and(new PresentFilter("email"));

在这个过滤器上调用 encode 方法会产生(&(city=Midvale)(email=*))。清单 6-10 显示了创建 AndFilter 并调用 searchAndPrintResults 方法的测试用例。

清单 6-10。

@Test
public void testAndFilter() {
   AndFilter andFilter = new AndFilter();
   andFilter.and(new EqualsFilter("postalCode", "84047"));
   andFilter.and(new PresentFilter("mail"));
   searchFilterDemo.searchAndPrintResults(andFilter);
}

Results  found in  search:  1
Jacob  Smith

orf filter〔??〕

和 AndFilter 一样,OrFilter 可以用来组合多个搜索筛选器。但是,结果过滤器将匹配满足任何子过滤器条件的条目。下面是 OrFilter 的一个实现:

OrFilter orFilter  =  new  OrFilter();
orFilter.add(new EqualsFilter("postalcode",  "84047"));
orFilter.add(new EqualsFilter("postalcode",  "84121"));

这个 OrFilter 将检索所有居住在 84047 或 84121 邮政编码的顾客。encode 方法返回表达式(|(postal code = 84047)(postal code = 84121))。OrFilter 的测试用例如清单 6-11 所示。

清单 6-11。

@Test
public void testOrFilter() {
   OrFilter orFilter = new OrFilter();
   orFilter.or(new EqualsFilter("postalCode", "84047"));
   orFilter.or(new EqualsFilter("postalCode", "84121"));
   searchFilterDemo.searchAndPrintResults(orFilter);
}

Results  found in  search:  2
Jacob  Smith
Adriane  Admin-mtv

硬编码过滤器

HardcodedFilter 是一个方便的类,它使得在构建搜索过滤器时添加静态过滤器文本变得容易。假设您正在编写一个允许管理员在文本框中输入搜索表达式的管理应用。如果要将此表达式与其他筛选器一起用于搜索,可以使用 HardcodedFilter,如下所示:

AndFilter filter  =  new  AndFilter();
filter.add(new HardcodedFilter(searchExpression));
filter.add(new EqualsFilter("givenName", "smith"));

在这段代码中,searchExpression 变量包含用户输入的搜索表达式。当搜索过滤器的静态部分来自属性文件或配置文件时,HardcodedFilter 也非常方便。记住这个过滤器不对传入的文本进行编码是很重要的。所以请谨慎使用,尤其是直接处理用户输入的时候。

white space wild cardfilter

WhitespaceWildcardsFilter 是另一个方便的类,它使得创建子字符串搜索过滤器更加容易。像它的超类 EqualsFilter 一样,这个类接受一个属性名和值。然而,顾名思义,它将属性值中的所有空格都转换为通配符。考虑以下示例:

WhitespaceWildcardsFilter filter = new WhitespaceWildcardsFilter("cn", "John Will");

该过滤器产生以下表达式:(cn=JohnWill*)。在开发搜索和查找应用时,此过滤器会很有用。

创建自定义过滤器

尽管 Spring LDAP 提供的过滤器类在大多数情况下已经足够了,但是可能会出现当前设置不够的情况。谢天谢地,Spring LDAP 使得创建新的过滤器类变得很容易。在本节中,您将看到如何创建一个定制的近似过滤器。

近似过滤器用于检索属性值大约等于指定值的条目。近似表达式使用∾=运算符创建。因此,( given name∞= Adeli)过滤器将匹配名字为 Adel 或 Adele 的条目。当用户在搜索时不知道值的实际拼写时,近似筛选器在搜索应用中非常有用。查找发音相似值的算法的实现因 LDAP 服务器实现的不同而不同。

Spring LDAP 不提供任何现成的类来创建近似过滤器。在清单 6-12 中,你创建了这个过滤器的一个实现。注意,ApproximateFilter 类扩展了 AbstractFilter。构造函数被定义为接受属性类型和属性值。在 encode 方法中,通过连接属性类型、运算符和值来构造过滤器表达式。

清单 6-12。

import org.springframework.ldap.filter.AbstractFilter;

private class ApproximateFilter extends AbstractFilter {

   private static final String APPROXIMATE_SIGN = "∼=";
   private String attribute;
   private String value;

   public ApproximateFilter(String attribute, String value) {
      this.attribute = attribute;
      this.value = value;
   }

   @Override
   public StringBuffer encode(StringBuffer buff) {
      buff.append('(');
      buff.append(attribute).append(APPROXIMATE_SIGN).append(value);
      buff.append(')');

          return buff;
   }
}

清单 6-13 显示了使用 ApproximateFilter 类运行 searchAndPrintResults 方法的测试代码。

清单 6-13。

@Test
public void testApproximateFilter() {
   ApproximateFilter approx = new ApproximateFilter("givenName", "Adeli");
   searchFilterDemo.searchAndPrintResults(approx);
}

下面是运行测试用例的输出:

Results  found in  search:  6
Adel  Acker
Adela Acklin
Adele Acres
Adelia  Actionteam
Adella  Adamczyk
Adelle Adamkowski

处理特殊字符

有时候,您需要使用在 LDAP 中有特殊含义的字符(或 a *)来构建搜索过滤器。为了成功地执行这些过滤器,正确地对特殊字符进行转义是很重要的。转义使用格式\xx 完成,其中 xx 表示字符的十六进制表示。表 6-3 列出了所有特殊字符及其转义值。

表 6-3 。特殊字符和转义值

特殊字符逸出值
\28
)\29
*\2a
\\5c
/\2f

除了上述字符之外,如果在 DN 中使用了以下任何字符,也需要对它们进行适当的转义:逗号(,)、等号(=)、加号(+)、小于()、井号(#)和分号(;).

摘要

在本章中,您学习了如何使用搜索过滤器简化 LDAP 搜索。我以 LDAP 搜索概念的概述开始了这一章。然后,您查看了不同的搜索过滤器,您可以使用这些过滤器以各种方式检索数据。您还看到了 Spring LDAP 如何使创建定制搜索过滤器变得容易。

在下一章中,您将看到从 LDAP 服务器获得的结果的排序和分页。

七、排序和分页结果

在本章中,您将学习

  • LDAP 控件的基础。
  • 对 LDAP 结果进行排序。
  • 分页 LDAP 结果。

LDAP 控件

LDAP 控制提供了一种标准化的方法来修改 LDAP 操作的行为。控件可以简单地看作是客户端发送给 LDAP 服务器的消息(反之亦然)。作为客户端请求的一部分发送的控件可以向服务器提供附加信息,指示应该如何解释和执行操作。例如,可以在 LDAP 删除操作中指定删除子树控件。收到删除请求后,LDAP 服务器的默认行为是删除条目。但是,当 delete subtree 控件附加到 delete 请求时,服务器会自动删除该条目及其所有从属条目。这种控制被称为请求控制 。

LDAP 服务器也可以将控制作为其响应消息的一部分发送,以指示操作是如何处理的。例如,LDAP 服务器可能会在绑定操作期间返回密码策略控制,指示客户端的密码已经过期或即将过期。由服务器发送的这种控制被称为响应控制 。可以随操作一起发送任意数量的请求或响应控制。

LDAP 控制,包括请求和响应,由以下三部分组成:

  • 唯一标识控件的对象标识符(OID)。这些 oid 防止控件名称之间的冲突,通常由创建控件的供应商定义。这是控件的必需组件。
  • 指明控制对于操作是关键还是非关键。这也是一个必需组件,可以是真或假。
  • 特定于控件的可选信息。例如,用于分页搜索结果的分页控件需要页面大小来确定页面中要返回的条目数。

RFC 2251(www.ietf.org/rfc/rfc2251…)中规定的 LDAP 控件的正式定义如图 7-1 中的所示。然而,这个 LDAP 规范没有定义任何具体的控制。控制定义通常由 LDAP 供应商提供,它们的支持因服务器而异。

9781430263975_Fig07-01.jpg

图 7-1 。LDAP 控制规范

当 LDAP 服务器在操作中接收控件时,其行为取决于控件及其相关信息。图 7-2 中的流程图显示了接收请求控制时的服务器行为。

9781430263975_Fig07-02.jpg

图 7-2 。LDAP 服务器控制交互

一些通常支持的 LDAP 控件及其 OID 和描述在表 7-1 中显示。

表 7-1 。常用控件

控件名称似…的说明(RFC)
分类控制1.2.840.113556.1.4.473请求服务器在将搜索结果发送给客户端之前对它们进行排序。这是 RFC 2891 的一部分。
分页结果控制1.2.840.113556.1.4.319请求服务器在包含指定数量条目的页面中返回搜索结果。只允许搜索结果的顺序迭代。这被定义为 RFC 2696 的一部分。
子树删除控件1.2.840.113556.1.4.805请求服务器删除该条目及其所有后代条目。
虚拟列表视图控件2.16.840.1.113730.3.4.9这类似于页面搜索结果,但允许客户端请求任意条目子集。在因特网草案文件 VLV 04 中描述了这种控制。
密码策略控制1.3.6.1.4.1.42.2.27.8.5.1服务器发送的控件,保存有关由于密码策略问题(如密码需要重置、帐户已被锁定或密码已过期或即将过期)而导致的失败操作(如身份验证)的信息。
管理 DSA/IT 控制2.16.840.1.113730.3.4.2请求服务器将“ref”属性条目(引用)视为常规 LDAP 条目。
持续搜索控制2.16.840.1.113730.3.4.3此控件允许客户端接收 LDAP 服务器中与搜索条件匹配的条目的更改通知。

识别支持的控件

在使用特定控件之前,确保您使用的 LDAP 服务器支持该控件是很重要的。LDAP 规范要求每个符合 LDAP v3 的服务器在根 DSA 特定条目 (DSE)的 supportedControl 属性中发布所有支持的控件。因此,在根 DSE 条目中搜索 supportedControl 属性将列出所有控件。清单 7-1 显示了连接到运行在端口 11389 上的 OpenDJ 服务器并将控制列表打印到控制台的代码。

清单 7-1

package com.inflinx.book.ldap;

import java.util.Properties;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;

public class SupportedControlApplication {

   public void displayControls() {

      String ldapUrl = "ldap://localhost:11389";
      try {
         Properties environment = new Properties();
         environment.setProperty(DirContext.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory");
         environment.setProperty(DirContext.PROVIDER_URL,ldapUrl);
         DirContext context = new InitialDirContext(environment);
         Attributes attributes = context.getAttributes("", new String[]{"supportedcontrol"});
         Attribute supportedControlAttribute = attributes.get("supportedcontrol");
         NamingEnumeration controlOIDList = supportedControlAttribute.getAll();
         while(controlOIDList != null && controlOIDList.hasMore()) {
            System.out.println(controlOIDList.next());
         }
         context.close();
      }
      catch(NamingException e) {
         e.printStackTrace();
      }
   }

   public static void main(String[] args) throws NamingException {
      SupportedControlApplication supportedControlApplication = new SupportedControlApplication();
      supportedControlApplication.displayControls();
   }
}

下面是运行清单 7-1 中的代码后的输出:

1.2.826.0.1.3344810.2.3
1.2.840.113556.1.4.1413
1.2.840.113556.1.4.319
1.2.840.113556.1.4.473
1.2.840.113556.1.4.805
1.3.6.1.1.12
1.3.6.1.1.13.1
1.3.6.1.1.13.2
1.3.6.1.4.1.26027.1.5.2
1.3.6.1.4.1.42.2.27.8.5.1
1.3.6.1.4.1.42.2.27.9.5.2
1.3.6.1.4.1.42.2.27.9.5.8
1.3.6.1.4.1.4203.1.10.1
1.3.6.1.4.1.4203.1.10.2
2.16.840.1.113730.3.4.12
2.16.840.1.113730.3.4.16
2.16.840.1.113730.3.4.17
2.16.840.1.113730.3.4.18
2.16.840.1.113730.3.4.19
2.16.840.1.113730.3.4.2
2.16.840.1.113730.3.4.3
2.16.840.1.113730.3.4.4
2.16.840.1.113730.3.4.5
2.16.840.1.113730.3.4.9

OpenDJ 安装提供了一个命令行 ldapsearch 工具,也可以用来列出支持的控件。假设 OpenDJ 安装在 Windows 的 c:\practicalldap\opendj 下,下面是获取支持的控件列表的命令:

ldapsearch --baseDN "" --searchScope base --port 11389 "(objectclass=*)" supportedControl

图 7-3 显示了运行该命令的结果。请注意,为了搜索根 DSE,您使用了作用域 base,但没有提供基本 DN。此外,图中支持的控件 oid 与运行清单 7-1 中的 Java 代码后收到的 oid 相匹配。

9781430263975_Fig07-03.jpg

图 7-3 。OpenDJ ldapsearch 命令

JNDI 和控制和

JNDI API 中的 javax.naming.ldap 包包含对 LDAP V3 特定特性的支持,比如控件和扩展操作。当控制修改或增加现有操作的行为时,扩展操作允许定义额外的操作。图 7-4 中的 UML 图突出了 javax.naming.ldap 包中一些重要的控件类。

9781430263975_Fig07-04.jpg

图 7-4 。Java LDAP 控件类层次结构

javax.naming.ldap.Control 接口为请求和响应控件提供了抽象。此接口的几个实现(如 SortControl 和 PagedResultsControl)作为 JDK 的一部分提供。其他控件,如 Virtual- ListViewControl 和 PasswordExpiringResponseControl,可作为 LDAP booster pack 的一部分。

javax.naming.ldap 包中的核心组件是 LdapContext 接口。该接口扩展了 javax.naming.DirContext 接口,并提供了执行 LDAP V3 操作的其他方法。javax.naming.ldap 包中的 InitialLdapContext 类提供了该接口的具体实现。

在 JNDI API 中使用控件非常简单。清单 7-2 中的代码提供了使用控件的算法。

清单 7-2

   LdapContext context = new InitialLdapContext();
   Control[] requestControls = // Concrete control instance array
   context.setRequestControls(requestControls);
   /* Execute a search operation using the context*/
   context.search(parameters);
   Control[] responseControls = context.getResponseControls();
   // Analyze the response controls

在该算法中,首先创建希望包含在请求操作中的控件的实例。然后执行操作并处理操作的结果。最后,分析服务器发送的任何响应控制。在接下来的部分中,您将看到该算法与排序和分页控件的具体实现。

Spring LDAP 和控件

当使用 LdapTemplate 的搜索方法时,Spring LDAP 不提供对目录上下文的访问。因此,您无法将请求控件添加到上下文或流程响应控件中。为了解决这个问题,Spring LDAP 提供了一个目录上下文处理器,可以自动向上下文添加和分析 LDAP 控件。清单 7-3 显示了 DirContextProcessor API 代码。

清单 7-3

package org.springframework.ldap.core;

import javax.naming.NamingException;
import javax.naming.directory.DirContext;

public interface DirContextProcessor {
   void preProcess(DirContext ctx) throws NamingException;
   void postProcess(DirContext ctx) throws NamingException;
}

DirContextProcessor 接口的具体实现被传递给 LdapTemplate 的搜索方法。在执行搜索之前调用预处理方法。因此,具体的实现将在预处理方法中包含逻辑,以将请求控制添加到上下文中。执行搜索后将调用后处理方法。因此,具体的实现将在后处理方法中有逻辑来读取和分析 LDAP 服务器发送的任何响应控制。

图 7-5 显示了 DirContextProcessor 及其所有实现的 UML 表示。

9781430263975_Fig07-05.jpg

图 7-5 。DirContextProcessor 类层次结构

AbstractRequestControlDirContextProcessor 实现 DirContextProcessor 的预处理方法,并在 LdapContext 上应用单个 RequestControl。AbstractRequestDirContextProcessor 通过 createRequestControl 模板方法将请求控件的实际创建委托给子类。

AbstractFallbackRequestAndResponseControlDirContextProcessor 类扩展了 AbstractRequestControlDirContextProcessor,并大量使用反射来自动化 DirContext 处理。它执行加载控件类、创建它们的实例以及将它们应用到上下文的任务。它还负责响应控件的大部分后处理,将模板方法委托给执行实际值检索的子类。

PagedResultsDirContextProcessor 和 SortControlDirContextProcessor 用于管理分页和排序控件。在接下来的部分中,您将会看到它们。

分类控制

sort 控件提供了一种机制,请求 LDAP 服务器在将搜索结果发送给客户机之前对它们进行排序。RFC 2891(www.ietf.org/rfc/rfc2891…)中规定了这种控制。排序请求控件接受一个或多个 LDAP 属性名,并将其提供给服务器来执行实际的排序。

让我们看看如何在普通的 JNDI API 中使用排序控件。清单 7-4 显示了按照姓氏对所有搜索结果进行排序的代码。首先创建 javax.naming.ldap.SortControl 的一个新实例,并为它提供 sn 属性,表明您打算按姓氏排序。您还通过向同一个构造函数提供 critical 标志来表明这是一个关键控件。然后,使用 setRequestControls 方法将该请求控件添加到上下文中,并执行 LDAP 搜索操作。然后遍历返回的结果,并将它们打印到控制台。最后,你看看反应控制。排序响应控件保存排序操作的结果。如果服务器未能对结果进行排序,您可以通过抛出异常来表明这一点。

清单 7-4。

public void sortByLastName() {
   try {
      LdapContext context = getContext();
      Control lastNameSort = new SortControl("sn", Control.CRITICAL);
      context.setRequestControls(new Control[]{lastNameSort});
      SearchControls searchControls = new SearchControls();
      searchControls.setSearchScope( SearchControls.SUBTREE_SCOPE);
      NamingEnumeration results = context.search("dc=inflinx,dc=com", "(objectClass=inetOrgPerson)", searchControls);

          /* Iterate over search results and display
      * patron entries
      */
      while (results != null && results.hasMore()) {
         SearchResult entry = (SearchResult)results.next();
         System.out.println(entry.getAttributes().get("sn") + " ( " + (entry.getName()) + " )");
      }

      /* Now that we have looped, we need to look at the response controls*/
      Control[] responseControls = context.getResponseControls();
      if(null != responseControls) {
         for(Control control : responseControls) {
            if(control instanceof SortResponseControl) {
               SortResponseControl sortResponseControl = (SortResponseControl) control;
               if(!sortResponseControl.isSorted()) {
                  // Sort did not happen. Indicate this with an exception
                  throw sortResponseControl.getException();
               }
            }
        }
      }
      context.close();
   }
   catch(Exception e) {
      e.printStackTrace();
   }
}

The output should display the sorted patrons as shown below:
sn: Aalders ( uid=patron4,ou=patrons )
sn: Aasen ( uid=patron5,ou=patrons )
sn: Abadines ( uid=patron6,ou=patrons )
sn: Abazari ( uid=patron7,ou=patrons )
sn: Abbatantuono ( uid=patron8,ou=patrons )
sn: Abbate ( uid=patron9,ou=patrons )
sn: Abbie ( uid=patron10,ou=patrons )
sn: Abbott ( uid=patron11,ou=patrons )
sn: Abdalla ( uid=patron12,ou=patrons )
......................................

现在让我们看看使用 Spring LDAP 实现相同的排序行为。清单 7-5 显示了相关的代码。在这个实现中,首先创建一个新的 org . spring framework . LDAP . control . sortcontroldircontextprocessor 实例。SortControlDirContextProcessor 构造函数采用 LDAP 属性名称,该名称应在控件创建期间用作排序键。下一步是创建 SearchControls 和一个过滤器来限制搜索。最后,调用 search 方法,传递创建的实例和映射数据的映射器。

清单 7-5。

public List<String> sortByLastName() {
   DirContextProcessor scdcp = new SortControlDirContextProcessor("sn");
   SearchControls searchControls = new SearchControls();
   searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);

   EqualsFilter equalsFilter = new EqualsFilter("objectClass", "inetOrgPerson");

   @SuppressWarnings("unchecked")
   ParameterizedContextMapper<String> lastNameMapper = new AbstractParameterizedContextMapper<String>() {
      @Override
      protected String doMapFromContext(DirContextOperations context) {
         return context.getStringAttribute("sn");
      }
   };

   List<String> lastNames = ldapTemplate.search("", equalsFilter.encode(), searchControls, lastNameMapper, scdcp);
   for (String ln : lastNames){
      System. out .println(ln);
   }
   return lastNames;
}

调用此方法后,您应该会在控制台中看到以下输出:

Aalders
Aasen
Abadines
Abazari
Abbatantuono
Abbate
Abbie
Abbott
Abdalla
Abdo
Abdollahi
Abdou
Abdul-Nour
................

实现自定义 DirContextProcessor

从 Spring LDAP 1.3.2 开始,SortControlDirContextProcessor 只能用于对一个 LDAP 属性进行排序。然而,JNDI API 允许你对多个属性进行排序。因为在某些情况下,您可能希望根据多个属性对搜索结果进行排序,所以让我们实现一个新的 DirContextProcessor,它将允许您在 Spring LDAP 中实现这一点。

到目前为止,您已经看到,排序操作需要一个请求控件,并将发送一个响应控件。所以实现这个功能最简单的方法就是扩展 AbstractFallbackRequestAndResponseControlDirContextProcessor。清单 7-6 显示了用空抽象方法实现的初始代码。正如您将看到的,您使用了三个实例变量来保存控件的状态。顾名思义,sortKeys 将保存将要排序的属性名。sorted 和 resultCode 变量将保存从响应控件中提取的信息。

清单 7-6。

package com.inflinx.book.ldap.control;

import javax.naming.ldap.Control;
import org.springframework.ldap.control.AbstractFallbackRequestAndResponseControlDirContextProcessor;

public class SortMultipleControlDirContextProcessor extends AbstractFallbackRequestAndResponseControlDirContextProcessor {

   //The keys to sort on
   private String[] sortKeys;

   //Did the results actually get sorted?
   private boolean sorted;

   //The result code of the sort operation
   private int resultCode;

   @Override
   public Control createRequestControl() {
      return null;
   }

   @Override
   protected void handleResponse(Object control) {
   }

   public String[] getSortKeys() {
      return sortKeys;
   }

   public boolean isSorted() {
      return sorted;
   }

   public int getResultCode() {
      return resultCode;
   }
}

下一步是向 AbstractFallbackRequestAndResponseControlDirContextProcessor 提供加载控件所需的信息。abstractfallbackrequestandresponsecontroldicontextprocessor 需要来自子类的两条信息:要使用的请求和响应控件的完全限定类名,以及应该用作后备的控件的完全限定类名。清单 7-7 显示了完成这项工作的构造器代码。

清单 7-7

public SortMultipleControlDirContextProcessor(String ... sortKeys) {

   if(sortKeys.length == 0) {
      throw new IllegalArgumentException("You must provide " + "atlease one key to sort on");
   }

   this.sortKeys = sortKeys;
   this.sorted = false;
   this.resultCode = -1;
   this.defaultRequestControl = "javax.naming.ldap.SortControl";
   this.defaultResponseControl = "javax.naming.ldap.SortResponseControl";
   this.fallbackRequestControl = "com.sun.jndi.ldap.ctl.SortControl";
   this.fallbackResponseControl = "com.sun.jndi.ldap.ctl.SortResponseControl";

   loadControlClasses();
}

请注意,您已经提供了 JDK 附带的控件类作为要使用的默认控件,以及 LDAP booster pack 附带的控件作为后备控件。在构造函数的最后一行,您指示 AbstractFallbackRequestAndResponseControlDirContextProcessor 类将这些类加载到 JVM 中以供使用。

流程的下一步是提供 createRequestControl 方法的实现。由于超类 abstractfallbackrequestandresponsecontroldicontextprocessor 将负责控件的实际创建,所以您只需提供创建控件所需的信息。以下代码说明了这一点:

@Override
public Control createRequestControl() {
   return super.createRequestControl(new Class[] {String[].class, boolean.class }, new Object[] { sortKeys, critical });
}

实施的最后一步是分析响应控制并检索关于已完成操作的信息。清单 7-8 显示了相关的代码。请注意,您正在使用反射从响应控件中检索排序和结果代码信息。

清单 7-8。

@Override
protected void handleResponse(Object control) {

   Boolean result = (Boolean) invokeMethod("isSorted", responseControlClass, control);
   this.sorted = result;

   Integer code = (Integer) invokeMethod("getResultCode", responseControlClass, control);
   this.resultCode = code;
}

现在您已经创建了一个新的 DirContextProcessor 实例,它允许您对多个属性进行排序,让我们来试一试。清单 7-9 显示了一个排序方法,它使用了 SortMultipleControlDirContextProcessor。该方法使用属性 st 和 l 对结果进行排序。

清单 7-9。

public void sortByLocation() {

   String[] locationAttributes = {"st", "l"};
   SortMultipleControlDirContextProcessor smcdcp = new SortMultipleControlDirContextProcessor(locationAttributes);
   SearchControls searchControls = new SearchControls();
   searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);

   EqualsFilter equalsFilter = new EqualsFilter("objectClass","inetOrgPerson");

   @SuppressWarnings("unchecked")
   ParameterizedContextMapper<String> locationMapper = new AbstractParameterizedContextMapper<String>() {

      @Override
      protected String doMapFromContext(DirContextOperations context) {
         return context.getStringAttribute("st") + "," + context.getStringAttribute("l");
      }
   };

   List<String> results = ldapTemplate.search("", equalsFilter.encode(), searchControls, locationMapper, smcdcp);
   for(String r : results) {
      System.out.println(r);
   }
}

调用该方法后,排序后的位置将显示在控制台上,如图所示:

AK,Abilene
AK,Florence
AK,Sioux Falls
AK,Wilmington
AL,Glendive
AR,Gainesville
AR,Green Bay
AZ,Gainesville
AZ,Moline
AZ,Reno
AZ,Saint Joseph
AZ,Wilmington
CA,Buffalo
CA,Ottumwa
CO,Charlottesville
CO,Lake Charles
CT,Quincy
CT,Youngstown
...............

分页搜索控件

分页结果控件允许 LDAP 客户端控制 LDAP 搜索操作结果的返回速率。LDAP 客户端创建具有指定页面大小的页面控件,并将其与搜索请求相关联。收到请求后,LDAP 服务器将分块返回结果,每个块包含指定数量的结果。在处理大型目录或构建具有分页功能的搜索应用时,分页结果控件非常有用。这种控制在 RFC 2696(www.ietf.org/rfc/rfc2696…)中有描述。

图 7-6 描述了使用页面控件的 LDAP 客户端和服务器之间的交互。

9781430263975_Fig07-06.jpg

图 7-6 。页面控制交互

image 注意 LDAP 服务器经常使用 sizeLimit 指令来限制搜索操作返回的结果数量。如果搜索产生的结果多于指定的大小限制,则会引发大小限制超出异常 javax . naming . sizelimitexceededededexception。分页方法不会让您超过这个限制。

第一步,LDAP 客户端发送搜索请求和页面控件。收到请求后,LDAP 服务器执行搜索操作并返回第一页结果。此外,它发送一个 cookie ,需要用它来请求下一个分页的结果集。这个 cookie 使 LDAP 服务器能够维护搜索状态。客户端不得对 cookie 的内部结构做出任何假设。当客户端请求下一批结果时,它会发送相同的搜索请求和页面控件以及 cookie。服务器用新的结果集和新的 cookie 进行响应。当没有更多的搜索结果返回时,服务器发送一个空的 cookie。

使用分页搜索控件的分页是单向和顺序的。客户端不可能在页面之间跳转或返回。现在你已经知道了分页控制的基本知识,清单 7-10 显示了使用普通 JNDI API 的实现。

清单 7-10。

public void pageAll() {

   try {
      LdapContext context = getContext();
      PagedResultsControl prc = new PagedResultsControl(20, Control.CRITICAL);
      context.setRequestControls(new Control[]{prc});
      byte[] cookie = null;
      SearchControls searchControls = new SearchControls();
      searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
      do {
         NamingEnumeration results = context.search("dc=inflinx,dc=com","(objectClass=inetOrgPerson)",searchControls);
         // Iterate over search results
         while(results != null && results.hasMore()) {
           // Display an entry
           SearchResult entry = (SearchResult)results.next();
           System.out.println(entry.getAttributes().get("sn") + " ( " + (entry.getName())+ " )");
         }
         // Examine the paged results control response
         Control[] controls = context.getResponseControls();
         if (controls != null) {
           for(int i = 0; i < controls.length; i++) {
             if(controls[i] instanceof PagedResultsResponseControl) {
               PagedResultsResponseControl prrc =(PagedResultsResponseControl)controls[i];
               int resultCount = prrc.getResultSize();
               cookie = prrc.getCookie();
             }
           }
        }
        // Re-activate paged results
        context.setRequestControls(new Control[]{
        new PagedResultsControl(20, cookie, Control.CRITICAL)});
      } while(cookie != null);

      context.close();
   }
   catch(Exception e) {
      e.printStackTrace();
   }
}

在清单 7-10 中,您通过获取 LDAP 服务器上的上下文来开始实现。然后创建 PagedResultsControl ,并将页面大小指定为其构造函数参数。您将控件添加到上下文中,并执行了搜索操作。然后循环搜索结果,并在控制台上显示信息。下一步,您将检查响应控件以识别服务器发送的 PagedResultsResponseControl。从该控件中,您提取 cookie 和该搜索的估计结果总数。结果计数是可选的信息,并且服务器可以简单地返回零来指示未知的计数。最后,创建一个新的 PagedResultsControl,将页面大小和 cookie 作为其构造函数参数。这个过程一直重复,直到服务器发送一个空的(null) cookie,表示不再有要处理的结果。

Spring LDAP 抽象了清单 7-10 中的大部分代码,并使用 PagedResultsDirContextProcessor 简化了页面控件的处理。清单 7-11 显示了 Spring LDAP 代码。

清单 7-11。

public void pagedResults() {

   PagedResultsCookie cookie = null;
   SearchControls searchControls = new SearchControls();
   searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
   int page = 1;
   do {
      System.out.println("Starting Page: " + page);
      PagedResultsDirContextProcessor processor = new PagedResultsDirContextProcessor(20,cookie);
      EqualsFilter equalsFilter = new EqualsFilter("objectClass","inetOrgPerson");
      List<String> lastNames = ldapTemplate.search("", equalsFilter.encode(), searchControls, new LastNameMapper(), processor);
      for(String l : lastNames) {
         System.out.println(l);
      }
      cookie = processor.getCookie();
      page = page + 1;
   } while(null != cookie.getCookie());
}

在这个实现中,使用页面大小和一个 cookie 创建 PagedResultsDirContextProcessor。请注意,您使用 org . spring framework . LDAP . control . pagedresultsCookie 类来抽象服务器发送的 cookie。cookie 值最初以空值开始。然后执行搜索并遍历结果。服务器发送的 cookie 是从 DirContextProcessor 中提取的,用于检查未来的搜索请求。您还使用 LastNameMapper 类从结果上下文中提取姓氏。清单 7-12 给出了 LastNameMapper 类的实现。

清单 7-12。

private class LastNameMapper extends AbstractParameterizedContextMapper<String> {

   @Override
   protected String doMapFromContext(DirContextOperations context) {
      return context.getStringAttribute("sn");
   }
}

摘要

在本章中,您学习了与 LDAP 控件相关的基本概念。然后查看了排序控件,该控件可用于对结果执行服务器端排序。您看到了 Spring LDAP 如何显著简化排序控件的使用。分页控件可用于分页 LDAP 结果,这在流量很大的情况下非常有用。

在下一章中,您将看到使用 Spring LDAP ODM 技术来实现数据访问层。

八、对象目录映射

在本章中,您将学习

  • ODM 的基础。
  • Spring LDAP ODM 实现。

企业 Java 开发人员采用面向对象(OO)技术来创建模块化的复杂应用。在 OO 范式中,对象是系统的核心,代表现实世界中的实体。每个对象都有一个身份、状态和行为。对象可以通过继承或组合与其他对象相关联。另一方面,LDAP 目录以分层树结构表示数据和关系。这种差异导致了对象-目录范式的不匹配,并可能导致面向对象和目录环境之间的通信出现问题。

Spring LDAP 提供了一个对象-目录映射(ODM) 框架,在对象和目录模型之间架起了一座桥梁。ODM 框架允许我们在两个模型之间映射概念,并编排自动将 LDAP 目录条目转换成 Java 对象的过程。ODM 类似于人们更熟悉的对象关系映射(ORM)方法,它在对象和关系数据库世界之间架起了一座桥梁。Hibernate 和 Toplink 之类的框架使得 ORM 变得流行,并且成为开发人员工具集的重要组成部分。

尽管 Spring LDAP ODM 与 ORM 共享相同的概念,但它确实有以下不同之处:

  • 不可能缓存 LDAP 条目。
  • ODM 元数据是通过类级注释来表达的。
  • 没有可用的 XML 配置。
  • 条目的惰性加载是不可能的。
  • 像 HQL 这样的查询语言并不存在。对象的加载是通过 DN 查找和标准 LDAP 搜索查询来完成的。

Spring ODM 基础

Spring LDAP ODM 作为一个独立于核心 LDAP 项目的模块分发。为了在项目中包含 Spring LDAP ODM,需要将下面的依赖项添加到项目的 pom.xml 文件中:

<dependency>
    <groupId>org.springframework.ldap</groupId>
    <artifactId>spring-ldap-odm</artifactId>
    <version>${org.springframework.ldap.version}</version>
    <exclusions>
        <exclusion>
            <artifactId>commons-logging</artifactId>
            <groupId>commons-logging</groupId>
        </exclusion>
    </exclusions>
</dependency>

Spring LDAP ODM 可以在 org.springframework.ldap.odm 包及其子包中找到。Spring LDAP ODM 的核心类如图 8-1 中的所示。在这一章中,你将会详细地看到每一个类。

9781430263975_Fig08-01.jpg

图 8-1 。Spring LAP ODM 核心类

LDAP ODM 的核心是提供通用搜索和 CRUD 操作的 OdmManager。它充当中介,在 LDAP 条目和 Java 对象之间转换数据。Java 对象被注释以提供转换元数据。清单 8-1 展示了 OdmManager API 。

清单 8-1。

Package org.springframeworkldap.odm.core;

import java.util.List;
import javax.naming.Name;
import javax.naming.directory.SearchControls;

public interface OdmManager {

   void create(Object entry);
   <T> T read(Class<T> clazz, Name dn);
   void update(Object entry);
   void delete(Object entry);
   <T> List<T> findAll(Class<T> clazz, Name base, SearchControls searchControls);
   <T> List<T> search(Class<T> clazz, Name base, String filter, SearchControls searchControls);
}

OdmManager 的 create、update 和 delete 方法接受一个 Java 对象,并使用其中的信息来执行相应的 LDAP 操作。read 方法有两个参数,一个确定返回类型的 Java 类和一个用于查找 LDAP 条目的全限定 DN。OdmManager 可以看作是你在第五章中看到的通用 DAO 模式的一个微小变化。

Spring LDAP ODM 提供了 OdmManager 的现成实现,名为 OdmManagerImpl。为了正常运行,OdmManagerImpl 使用以下三个对象:

  • 用于与 LDAP 服务器通信的 ContextSource 实现。
  • 一个 ConverterManager 实现,用于将 LDAP 数据类型转换为 Java 数据类型,反之亦然。
  • 需要由 ODM 实现管理的一组域类。

为了简化 OdmManagerImpl 实例的创建,框架提供了一个工厂 bean OdmManagerImplFactoryBean。下面是创建 OdmManager 实例的必要配置:

<bean  id="odmManager" class="org.springframework.ldap.odm. core.impl.OdmManagerImplFactoryBean">
    <property  name="converterManager" ref="converterManager"  />
    <property  name="contextSource" ref="contextSource" />
    <property  name="managedClasses">
        <set>
            <value>FULLY_QUALIFIED_CLASS_NAME</value>
        </set>
    </property>
</bean>

OdmManager 将 LDAP 属性到 Java 字段的转换管理(反之亦然)委托给 ConverterManager。ConverterManager 本身依赖于一组用于实际转换目的的转换器实例。清单 8-2 显示了转换器接口 API 。convert 方法接受一个对象作为其第一个参数,并将其转换为由 toClass 参数指定的类型的实例。

清单 8-2。

package org.springframework.ldap.odm.typeconversion.impl;

public interface Converter {
   <T> T convert(Object source, Class<T> toClass) throws Exception;
}

转换器的通用特性使得创建特定的实现变得容易。Spring LDAP ODM 提供了转换器接口的 ToStringConverter 实现,它将给定的源对象转换为字符串。清单 8-3 提供了 ToStringConverter API 实现。正如您所看到的,只需在源对象上调用 toString 方法就可以进行转换。

清单 8-3。

package org.springframework.ldap.odm.typeconversion.impl.converters;

import org.springframework.ldap.odm.typeconversion.impl.Converter;

public final class ToStringConverter implements Converter {

   public <T> T convert(Object source, Class<T> toClass) {
      return toClass.cast(source.toString());
   }
}

这个实现的逆过程是 FromStringConverter,它将 java.lang.String 对象转换为任何指定的 toClass 类型。清单 8-4 提供了 FromStringConverter API 实现。转换器实现通过调用 toClass 参数的构造函数并传入 String 对象来创建新的实例。toClass 类型参数必须有一个接受单个 java.lang.String 类型参数的公共构造函数。例如,FromStringConverter 可以将字符串数据转换为整数或长数据类型。

清单 8-4。

package org.springframework.ldap.odm.typeconversion.impl.converters;

import java.lang.reflect.Constructor;
import org.springframework.ldap.odm.typeconversion.impl.Converter;

public final class FromStringConverter implements Converter {

   public <T> T convert(Object source, Class<T> toClass) throws Exception {
      Constructor<T> constructor = toClass.getConstructor(java.lang.String.class);
      return constructor.newInstance(source);
   }
}

这两个转换器类应该足以将大多数 LDAP 数据类型转换为常见的 Java 字段类型,如 java.lang.Integer、java.lang.Byte 等。反之亦然。清单 8-5 显示了创建 FromStringConverter 和 ToStringConverter 实例所涉及的 XML 配置。

清单 8-5。

<bean id="fromStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.converters.FromStringConverter" />
<bean id="toStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.converters.ToStringConverter" />

现在您已经准备好创建 ConverterManager 的一个实例,并向它注册上述两个转换器。注册转换器包括指定转换器本身、指示转换器预期的源对象类型的 fromClass 和指示转换器将返回的类型的 toClass。为了简化转换器注册过程,Spring ODM 提供了一个 ConverterConfig 类。清单 8-6 显示了注册 toStringConverter 实例的 XML 配置。

清单 8-6。

<bean id="toStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
   <property name="converter" ref="toStringConverter"/>
   <property name="fromClasses">
      <set>
         <value>java.lang.Integer</value>
      </set>
   </property>
   <property name="toClasses">
      <set>
         <value>java.lang.String</value>
      </set>
   </property>
</bean>

如您所见,ConverterConfig 是 org . spring framework . LDAP . ODM . type conversion . impl . convertermanagerfactorybean 类的内部类。此配置告诉 ConverterManager 使用 toStringConverter bean 将 java.lang.Integer 类型转换为 String 类型。在内部,转换器注册在使用以下算法计算的密钥下:

key = fromClass.getName() + ":" + syntax + ":" + toClass. getName();

有时,您可能希望使用同一个转换器实例来转换各种数据类型。例如,ToStringConverter 可用于转换其他类型,如 java.lang.Long、java.lang.Byte、java.lang.Boolean 等。为了处理这种情况,ConverterConfig 接受一组转换器可以处理的 from 和 To 类。清单 8-7 显示了修改后的 ConverterConfig ,它接受几个 fromClasses。

清单 8-7。

<bean id="toStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
   <property name="converter" ref="toStringConverter" />
   <property name="fromClasses">
      <set>
         <value>java.lang.Byte</value>
         <value>java.lang.Integer</value>
         <value>java.lang.Boolean</value>
      </set>
   </property>
   <property name="toClasses">
      <set>
         <value>java.lang.String</value>
      </set>
   </property>
</bean>

上述 fromClasses 集合中指定的每个类都将与 toClasses 集合中的一个类成对出现,以便进行转换器注册。因此,如果指定 n 个 fromClasses 和 m 个 toClasses,将导致转换器有 n*m 个注册。清单 8-8 显示了 fromStringConverterConfig ,它与之前的配置非常相似。

清单 8-8。

<bean id="fromStringConverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
   <property name="converter" ref="fromStringConverter" />
   <property name="fromClasses">
      <set>
         <value>java.lang.String</value>
      </set>
   </property>
   <property name="toClasses">
      <set>
         <value>java.lang.Byte</value>
         <value>java.lang.Integer</value>
         <value>java.lang.Boolean</value>
      </set>
   </property>
</bean>

拥有必要的转换器配置后,可以使用 ConverterManagerFactoryBean 创建新的 ConverterManager 实例。清单 8-9 显示了所需的 XML 声明。

清单 8-9。

<bean id="converterManager" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean">
   <property name="converterConfig">
      <set>
         <ref bean="fromStringConverterConfig"/>
         <ref bean="toStringConverterConfig"/>
      </set>
   </property>
</bean>

使用 ODM 框架所需的设置到此结束。在接下来的小节中,您将看到如何注释域类,以及如何使用这个配置进行 LDAP 读写。在此之前,让我们回顾一下到目前为止你做了什么(见图 8-2 )。

9781430263975_Fig08-02.jpg

图 8-2 。OdmManager 内部工作方式

  1. OdmManager 实例是由 OdmManagerImplFactoryBean 创建的。
  2. OdmManager 使用 ConverterManager 实例在 LDAP 和 Java 类型之间进行转换。
  3. 对于从一种特定类型到另一种特定类型的转换,ConverterManager 使用转换器。
  4. ConverterManager 实例由 ConverterManagerFactoryBean 创建。
  5. ConverterManagerFactoryBean 使用 ConverterConfig 实例来简化转换器注册。ConverterConfig 类接受 fromClasses、toClasses 和伴随关系的转换器。

ODM 元数据

org . spring framework . LDAP . odm . annotations 包包含可用于将简单的 Java POJOs 转换成 ODM 可管理实体的注释。清单 8-10 展示了 Patron Java 类,您将把它转换成一个 ODM 实体。

清单 8-10。

public class Patron {

   private String lastName;
   private String firstName;
   private String telephoneNumber;
   private String fullName;
   private String mail;
   private int employeeNumber;

   // Getters and setters

   @Override
   public String toString() {
      return "Dn: " + dn + ", firstName: " + firstName + ", fullName: " +       fullName + ", Telephone Number: " + telephoneNumber;
   }
}

您将通过用@Entry 注释该类来开始转换。这个标记注释告诉 ODM 管理器这个类是一个实体。它还用于提供实体映射到的 LDAP 中的对象类定义。清单 8-11 显示了带注释的 Patron 类。

清单 8-11。

@Entry(objectClasses= { "inetorgperson", "organizationalperson", "person", "top" })
public class Patron {
   // Fields and getters and setters
}

您需要添加的下一个注释是@Id 。该注释指定条目的 DN,并且只能放在 javax.naming.Name 类的派生字段上。为了解决这个问题,您将在 Patron 类中创建一个名为 dn 的新字段。清单 8-12 显示了修改后的顾客类。

清单 8-12。

@Entry(objectClasses= { "inetorgperson", "organizationalperson", "person", "top" })
public class Patron {

   @Id
   private Name dn;
   // Fields and getters and setters
}

Java 持久性 API 中的@Id 注释指定了实体 bean 的标识符属性。此外,它的位置决定了 JPA 提供者将用于映射的默认访问策略。如果将@Id 放在字段上,则使用字段访问。如果将它放在 getter 方法上,将使用属性访问。然而,Spring LDAP ODM 只允许字段访问。

@Entry 和@Id 是使 Patron 类成为 ODM 实体的唯一两个必需的注释。默认情况下,Patron 实体类中的所有字段都将自动变为可持久的。默认策略是在持久化或读取时使用实体字段的名称作为 LDAP 属性名称。在 Patron 类中,这适用于 telephoneNumber 或 mail 等属性,因为字段名和 LDAP 属性名是相同的。但是这会导致 firstName 和 fullName 等字段出现问题,因为它们的名称不同于 LDAP 属性名称。为了解决这个问题,ODM 提供了@Attribute 注释,将实体字段映射到对象类字段。该注释允许您指定 LDAP 属性的名称、可选的语法 OID 和可选的类型声明。清单 8-13 显示了完全注释的顾客实体类。

清单 8-13。

@Entry(objectClasses = { "inetorgperson", "organizationalperson", "person", "top" })
public class Patron {

   @Id
   private Name dn;

   @Attribute(name = "sn")
   private String lastName;

   @Attribute(name = "givenName")
   private String firstName;
   private String telephoneNumber;

   @Attribute(name = "cn")
   private String fullName;
   private String mail;

   @Attribute(name = "objectClass")
   private List<String> objectClasses;

   @Attribute(name = "employeeNumber", syntax = "2.16.840.1.113730.3.1.3")
   private int employeeNumber;

   // Getters and setters

   @Override
   public String toString() {
      return "Dn: " + dn + ", firstName: " + firstName + "," + " fullName: " + fullName + ", Telephone Number: " + telephoneNumber;
   }
}

有些时候你不希望保存实体类的某些字段。通常,这些涉及到计算的字段。这样的字段可以用@Transient annotation 进行注释,表示该字段应该被 OdmManager 忽略。

ODM 服务类别

基于 Spring 的企业应用通常有一个保存应用业务逻辑的服务层。服务层中的类将持久性细节委托给 DAO 或存储库层。在第五章中,你用 LdapTemplate 实现了一个 DAO。在本节中,您将创建一个新的服务类,它使用 OdmManager 作为 DAO 的替代。清单 8-14 显示了您将要实现的服务类的接口。

清单 8-14。

package com.inflinx.book.ldap.service;

import com.inflinx.book.ldap.domain.Patron;

public interface PatronService {

   public void create(Patron patron);
   public void delete(String id);
   public void update(Patron patron);
   public Patron find(String id);
}

服务类实现在清单 8-15 中给出。在实现中,您注入一个 OdmManager 实例。create 和 update 方法实现只是将调用委托给 OdmManager。find 方法将传入的 id 参数转换为完全限定的 DN,并将实际的检索委托给 OdmManager 的 read 方法。最后,delete 方法使用 find 方法读取 patron,并使用 OdmManager 的 delete 方法删除它。

清单 8-15。

package com.inflinx.book.ldap.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.odm.core.OdmManager;
import org.springframework.stereotype.Service;
import com.inflinx.book.ldap.domain.Patron;

@Service("patronService" )
public class PatronServiceImpl implements PatronService {

   private static final String PATRON_BASE = "ou=patrons,dc=inflinx,dc=com";

   @Autowired
   @Qualifier("odmManager" )
   private OdmManager odmManager;

   @Override
   public void create(Patron patron) {
      odmManager.create(patron);
   }
   @Override
   public void update(Patron patron) {
      odmManager.update(patron);
   }
   @Override
   public Patron find(String id) {
      DistinguishedName dn = new DistinguishedName(PATRON_BASE);
      dn.add("uid", id);
      return odmManager.read(Patron.class, dn);
   }
   @Override
   public void delete(String id) {
      odmManager.delete(find(id));
   }
}

验证 PatronService 实现的 JUnit 测试如清单 8-16 所示。

清单 8-16。

package com.inflinx.book.ldap.service;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.ldapunit.util.LdapUnitUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.ldap.NameNotFoundException;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.inflinx.book.ldap.domain.Patron;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;

@RunWith(SpringJUnit4ClassRunner.class )
@ContextConfiguration("classpath:repositoryContext-test.xml" )
public class PatronServiceImplTest {

   @Autowired
   private PatronService patronService;
   private static final String PORT = "12389";
   private static final String ROOT_DN = "dc=inflinx,dc=com";

   @Before
   public void setup() throws Exception {
      System.out.println("Inside the setup");
      LdapUnitUtils.loadData(new ClassPathResource("patrons.ldif"), PORT);
   }

   @After
   public void teardown() throws Exception {
      System.out.println("Inside the teardown");
      LdapUnitUtils.clearSubContexts(new DistinguishedName(ROOT_DN), PORT);
   }

   @Test
   public void testService() {
      Patron patron = new Patron();

      patron.setDn(new DistinguishedName("uid=patron10001," + "ou=patrons,dc=inflinx,dc=com"));
      patron.setFirstName("Patron");
      patron.setLastName("Test 1");
      patron.setFullName("Patron Test 1");
      patron.setMail("balaji@inflinx.com" );
      patron.setEmployeeNumber(1234);
      patron.setTelephoneNumber("8018640759");
      patronService.create(patron);

      // Lets read the patron
      patron = patronService.find("patron10001");
      assertNotNull(patron);

      patron.setTelephoneNumber("8018640850");
      patronService.update(patron);
      patron = patronService.find("patron10001");
      assertEquals(patron.getTelephoneNumber(), "8018640850");
      patronService.delete("patron10001");

      try {
         patron = patronService.find("patron10001");
         assertNull(patron);
      }
      catch(NameNotFoundException e) {
      }
   }
}

repositoryContext-test.xml 文件包含到目前为止您所看到的配置片段。清单 8-17 给出了 XML 文件的完整内容。

清单 8-17。

<?xml version="1.0" encoding="UTF-8"?>
<beans FontName3">http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

   <context:component-scan base-package="com.inflinx.book.ldap" />
   <bean id="contextSource" class="org.ldapunit.context.EmbeddedContextSourceFactory">
      <property name="port" value="12389" />
      <property name="rootDn" value="dc=inflinx,dc=com" />
      <property name="serverType" value="OPENDJ" />
   </bean>
   <bean id="odmManager" class="org.springframework.ldap.odm.core.impl.OdmManagerImpl">
      <constructor-arg name="converterManager" ref="converterManager" />
      <constructor-arg name="contextSource" ref="contextSource" />
      <constructor-arg name="managedClasses">
         <set>
            <value>com.inflinx.book.ldap.domain.Patron</value>
         </set>
      </constructor-arg>
   </bean>
   <bean id="fromStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.converters.FromStringConverter" />
   <bean id="toStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.converters.ToStringConverter" />

   <!-- Configuration information for a single instance of FromString -->
   <bean id="fromStringConverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
      <property name="converter" ref="fromStringConverter" />
      <property name="fromClasses">
         <set>
            <value>java.lang.String</value>
         </set>
      </property>
      <property name="toClasses">
         <set>
            <value>java.lang.Byte</value>
            <value>java.lang.Integer</value>
            <value>java.lang.Boolean</value>
         </set>
      </property>
   </bean>
   <bean id="toStringCoverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
      <property name="converter" ref="toStringConverter" />
      <property name="fromClasses">
         <set>
            <value>java.lang.Byte</value>
            <value>java.lang.Integer</value>
            <value>java.lang.Boolean</value>
         </set>
      </property>
      <property name="toClasses">
         <set>
            <value>java.lang.String</value>
         </set>
      </property>
   </bean>
   <bean id="converterManager" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean">
      <property name="converterConfig">
         <set>
            <ref bean="fromStringConverterConfig"/>
            <ref bean="toStringCoverterConfig"/>
         </set>
      </property>
   </bean>
</beans>

配置简化

清单 8-17 中的配置乍一看可能令人望而生畏。因此,为了解决这个问题,让我们创建一个新的 ConverterManager 实现来简化配置过程。清单 8-18 显示了 DefaultConverterManagerImpl 类。如您所见,它使用了其实现内部的 ConverterManagerImpl 类。

清单 8-18。

package com.inflinx.book.ldap.converter;

import org.springframework.ldap.odm.typeconversion.ConverterManager;
import org.springframework.ldap.odm.typeconversion.impl.Converter;
import org.springframework.ldap.odm.typeconversion.impl.ConverterManagerImpl;
import org.springframework.ldap.odm.typeconversion.impl.converters.FromStringConverter;
import org.springframework.ldap.odm.typeconversion.impl.converters.ToStringConverter;

public class DefaultConverterManagerImpl implements ConverterManager {

   private static final Class[] classSet = { java.lang.Byte.class, java.lang.Integer.class, java.lang.Long.class, java.lang.Double.class, java.lang.Boolean.class };
   private ConverterManagerImpl converterManager;

   public DefaultConverterManagerImpl() {
      converterManager = new ConverterManagerImpl();
      Converter fromStringConverter = new FromStringConverter();
      Converter toStringConverter = new ToStringConverter();
      for(Class clazz : classSet) {
         converterManager.addConverter(String.class, null, clazz, fromStringConverter);
         converterManager.addConverter(clazz, null, String.class, toStringConverter);
      }
   }

   @Override
   public boolean canConvert(Class<?> fromClass, String syntax, Class<?> toClass) {
      return converterManager.canConvert(fromClass, syntax, toClass);
   }

   @Override
   public <T> T convert(Object source, String syntax, Class<T> toClass) {
      return converterManager.convert(source,syntax,toClass);
   }
}

使用这个类可以大大减少所需的配置,如清单 8-19 所示。

清单 8-19。

<?xml version="1.0" encoding="UTF-8"?>
<beans http://www.springframework.org/schema/beans">http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

   <context:component-scan base-package="com.inflinx.book.ldap" />
   <bean id="contextSource" class="org.ldapunit.context.EmbeddedContextSourceFactory">
      <property name="port" value="12389" />
      <property name="rootDn" value="dc=inflinx,dc=com" />
      <property name="serverType" value="OPENDJ" />
   </bean>
   <bean id="odmManager" class="org.springframework.ldap.odm.core.impl.OdmManagerImplFactoryBean">
      <property name="converterManager" ref="converterManager" />
      <property name="contextSource" ref="contextSource" />
      <property name="managedClasses">
         <set>
            <value>com.inflinx.book.ldap.domain.Patron</value>
         </set>
      </property>
   </bean>
   <bean id="converterManager" class="com.inflinx.book.ldap.converter.DefaultConverterManagerImpl" />
</beans>

创建自定义转换器

考虑这样一个场景,您的顾客类使用一个定制的 PhoneNumber 类来存储顾客的电话号码。现在,当需要持久化一个 Patron 类时,您需要将 PhoneNumber 类转换为 String 类型。类似地,当从 LDAP 中读取 Patron 类时,需要将电话属性中的数据转换成 PhoneNumber 类。默认的 ToStringConverter 和 FromStringConverter 对此类转换没有用。清单 8-20 和清单 8-21 分别显示了电话号码和修改后的顾客类。

清单 8-20。

package com.inflinx.book.ldap.custom;

public class PhoneNumber {

   private int areaCode;
   private int exchange;
   private int extension;

   public PhoneNumber(int areaCode, int exchange, int extension) {
      this.areaCode = areaCode;
      this.exchange = exchange;
      this.extension = extension;
   }

   public boolean equals(Object obj) {
      if(obj == null || obj.getClass() != this.getClass())
      { return false; }

      PhoneNumber p = (PhoneNumber) obj;
         return (this.areaCode == p.areaCode) && (this.exchange == p.exchange) && (this.extension == p.extension);
   }

   public String toString() {
      return String.format("+1 %03d %03d %04d", areaCode, exchange, extension);
   }

   // satisfies the hashCode contract
   public int hashCode() {
      int result = 17;
      result = 37 * result + areaCode;
      result = 37 * result + exchange;
      result = 37 * result + extension;

          return result;
   }
}

清单 8-21

package com.inflinx.book.ldap.custom;

import java.util.List;
import javax.naming.Name;
import org.springframework.ldap.odm.annotations.Attribute;
import org.springframework.ldap.odm.annotations.Entry;
import org.springframework.ldap.odm.annotations.Id;

@Entry(objectClasses = { "inetorgperson", "organizationalperson", "person", "top" })
public class Patron {

   @Id
   private Name dn;

   @Attribute(name= "sn")
   private String lastName;

   @Attribute(name= "givenName")
   private String firstName;

   @Attribute(name= "telephoneNumber")
   private PhoneNumber phoneNumber;

   @Attribute(name= "cn")
   private String fullName;
   private String mail;

   @Attribute(name= "objectClass")
   private List<String> objectClasses;

   @Attribute(name= "employeeNumber", syntax = "2.16.840.1.113730.3.1.3")
    private int employeeNumber;

   // Getters and setters

   @Override
   public String toString() {
      return "Dn: " + dn + ", firstName: " + firstName + "," + " fullName: " + fullName + ", " + "Telephone Number: " + phoneNumber;
   }
}

若要将 PhoneNumber 转换为字符串,您需要创建一个新的 FromPhoneNumberConverter 转换器。清单 8-22 显示了实现。实现只需要调用 toString 方法来执行转换。

清单 8-22。

package com.inflinx.book.ldap.custom;

import org.springframework.ldap.odm.typeconversion.impl.Converter;

public class FromPhoneNumberConverter implements Converter {

   @Override
   public <T> T convert(Object source, Class<T> toClass) throws Exception {
      T result = null;
      if(PhoneNumber.class.isAssignableFrom(source.getClass()) && toClass.equals(String.class)) {
         result = toClass.cast(source.toString());
      }
      return result;
   }
}

接下来,您需要一个实现来将 LDAP 字符串属性转换为 Java PhoneNumber 类型。为此,您创建了 ToPhoneNumberConverter ,如清单 8-23 中的所示。

清单 8-23。

package com.inflinx.book.ldap.custom;

import org.springframework.ldap.odm.typeconversion.impl.Converter;

public class ToPhoneNumberConverter implements  Converter {

   @Override
   public <T> T convert(Object source, Class<T> toClass) throws Exception {
      T result = null;
      if(String.class.isAssignableFrom(source.getClass()) && toClass == PhoneNumber.class) {
      // Simple implementation
      String[] tokens = ((String)source).split(" ");
      int i = 0;
      if(tokens.length == 4) {
         i = 1;
      }
      result = toClass.cast(new PhoneNumber(
         Integer.parseInt(tokens[i]),
         Integer.parseInt(tokens[i+1]),
         Integer.parseInt(tokens[i+2])));
      }
      return result;
   }
}

最后,你在配置中绑定所有东西,如清单 8-24 所示。

清单 8-24。

<?xml version="1.0" encoding="UTF-8"?>
<beans http://www.springframework.org/schema/beans">http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">

   <context:component-scan base-package="com.inflinx.book.ldap" />
   <bean id="contextSource" class="org.ldapunit.context.EmbeddedContextSourceFactory">
      <property name="port" value="12389" />
      <property name="rootDn" value="dc=inflinx,dc=com" />
      <property name="serverType" value="OPENDJ" />
   </bean>
   <bean id="odmManager" class="org.springframework.ldap.odm.core.impl.OdmManagerImpl">
      <constructor-arg name="converterManager" ref="converterManager" />
      <constructor-arg name="contextSource" ref="contextSource" />
      <constructor-arg name="managedClasses">
         <set>
            <value>com.inflinx.book.ldap.custom.Patron</value>
         </set>
      </constructor-arg>
   </bean>
   <bean id="fromStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.converters.FromStringConverter" />
   <bean id="toStringConverter" class="org.springframework.ldap.odm.typeconversion.impl.converters.ToStringConverter" />
   <bean id="fromPhoneNumberConverter" class="com.inflinx.book.ldap.custom.FromPhoneNumberConverter" />
   <bean id="toPhoneNumberConverter" class="com.inflinx.book.ldap.custom.ToPhoneNumberConverter" />

   <!-- Configuration information for a single instance of FromString -->
   <bean id="fromStringConverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
      <property name="converter" ref="fromStringConverter" />
      <property name="fromClasses">
         <set>
            <value>java.lang.String</value>
         </set>
      </property>
      <property name="toClasses">
         <set>
            <value>java.lang.Byte</value>
            <value>java.lang.Integer</value>
            <value>java.lang.Boolean</value>
         </set>
      </property>
   </bean>
   <bean id="fromPhoneNumberConverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
      <property name="converter" ref="fromPhoneNumberConverter" />
      <property name="fromClasses">
         <set>
            <value>com.inflinx.book.ldap.custom.PhoneNumber</value>
         </set>
      </property>
      <property name="toClasses">
         <set>
            <value>java.lang.String</value>
         </set>
      </property>
   </bean>
   <bean id="toPhoneNumberConverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
      <property name="converter" ref="toPhoneNumberConverter" />
      <property name="fromClasses">
         <set>
            <value>java.lang.String</value>
         </set>
      </property>
      <property name="toClasses">
         <set>
            <value>com.inflinx.book.ldap.custom.PhoneNumber</value>
         </set>
      </property>
   </bean>
   <bean id="toStringConverterConfig" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean$ConverterConfig">
      <property name="converter" ref="toStringConverter"/>
      <property name="fromClasses">
         <set>
            <value>java.lang.Byte</value>
            <value>java.lang.Integer</value>
            <value>java.lang.Boolean</value>
         </set>
      </property>
      <property name="toClasses">
         <set>
            <value>java.lang.String</value>
         </set>
      </property>
   </bean>
   <bean id="converterManager" class="org.springframework.ldap.odm.typeconversion.impl.ConverterManagerFactoryBean">
      <property name="converterConfig">
         <set>
            <ref bean="fromPhoneNumberConverterConfig"/>
            <ref bean="toPhoneNumberConverterConfig"/>
            <ref bean="fromStringConverterConfig"/>
            <ref bean="toStringConverterConfig"/>
         </set>
      </property>
   </bean>
</beans>

用于测试新增转换器的修改后的测试用例如清单 8-25 所示。

清单 8-25。

@RunWith(SpringJUnit4ClassRunner.class )
@ContextConfiguration("classpath:repositoryContext-test3.xml")
public class PatronServiceImplCustomTest {

   @Autowired
   private PatronService patronService;
   private static final String PORT = "12389";
   private static final String ROOT_DN = "dc=inflinx,dc=com";

   @Before
   public void setup() throws Exception {
      System.out.println("Inside the setup");
      LdapUnitUtils.loadData(new ClassPathResource("patrons.ldif"), PORT);
   }

   @After
   public void teardown() throws Exception {
      System.out.println("Inside the teardown");
      LdapUnitUtils.clearSubContexts(new DistinguishedName(ROOT_DN), PORT);
   }

   @Test
   public void testService() {
      Patron patron = new Patron();
      patron.setDn(new DistinguishedName("uid=patron10001," + "ou=patrons,      dc=inflinx,dc=com"));
      patron.setFirstName("Patron"); patron.setLastName("Test 1");
      patron.setFullName("Patron Test 1");
      patron.setMail("balaji@inflinx.com" );
      patron.setEmployeeNumber(1234);
      patron.setPhoneNumber(new PhoneNumber(801, 864, 8050));
      patronService.create(patron);

      // Lets read the patron
      patron = patronService.find("patron10001");
      assertNotNull(patron);

          System.out.println(patron.getPhoneNumber());
      patron.setPhoneNumber(new PhoneNumber(435, 757, 9369));
      patronService.update(patron);

          System.out.println("updated phone: " + patron.getPhoneNumber());
      patron = patronService.find("patron10001");

          System.out.println("Read the phone number: " + patron.getPhoneNumber());
      assertEquals(patron.getPhoneNumber(), new PhoneNumber(435, 757, 9369));

          patronService.delete("patron10001");
      try {
         patron = patronService.find("patron10001");
         assertNull(patron);
      }
      catch(NameNotFoundException e) {
      }
   }
}

摘要

Spring LDAP 的对象-目录映射(ODM)在对象和目录模型之间架起了一座桥梁。在这一章中,你学习了 ODM 的基础知识,并且看了定义 ODM 映射的注释。然后,您深入研究了 ODM 框架,构建了顾客服务和定制转换器。

到目前为止,您已经创建了几种不同的服务和 DAO 实现。在下一章中,您将探索 Spring LDAP 对事务的支持。