DirContextAdapter による属性アクセスと操作の簡素化

あまり知られていない — そしておそらく過小評価されている — Java LDAP API の機能の 1 つは、DirObjectFactory を登録して、見つかった LDAP エントリからオブジェクトを自動的に作成する機能です。Spring LDAP は、この機能を利用して、特定の検索およびルックアップ操作で DirContextAdapter (Javadoc) インスタンスを返します。

DirContextAdapter は、特にデータを追加または変更する場合に、LDAP 属性を操作するための便利なツールです。

ContextMapper を使用した検索とルックアップ

LDAP ツリーでエントリが見つかると、その属性と識別名 (DN) が Spring LDAP によって使用され、DirContextAdapter が構築されます。これにより、次のように、AttributesMapper の代わりに ContextMapper (Javadoc) を使用して、見つかった値を変換できます。

例 1: ContextMapper を使用した検索
public class PersonRepoImpl implements PersonRepo {
   ...
   private static class PersonContextMapper implements ContextMapper {
      public Object mapFromContext(Object ctx) {
         DirContextAdapter context = (DirContextAdapter)ctx;
         Person p = new Person();
         p.setFullName(context.getStringAttribute("cn"));
         p.setLastName(context.getStringAttribute("sn"));
         p.setDescription(context.getStringAttribute("description"));
         return p;
      }
   }

   public Person findByPrimaryKey(
      String name, String company, String country) {
      Name dn = buildDn(name, company, country);
      return ldapClient.search().name(dn).toObject(new PersonContextMapper());
   }
}

前の例で示したように、Attributes および Attribute クラスを経由せずに、属性値を名前で直接取得できます。これは、複数値の属性を操作する場合に特に便利です。複数値属性から値を抽出するには、通常、Attributes 実装から返された属性値の NamingEnumeration をループする必要があります。DirContextAdapter は、getStringAttributes() (Javadoc) または getObjectAttributes() (Javadoc) メソッドでこれを行います。次の例では、getStringAttributes メソッドを使用しています。

例 2: getStringAttributes() を使用した複数値属性値の取得
private static class PersonContextMapper implements ContextMapper {
   public Object mapFromContext(Object ctx) {
      DirContextAdapter context = (DirContextAdapter)ctx;
      Person p = new Person();
      p.setFullName(context.getStringAttribute("cn"));
      p.setLastName(context.getStringAttribute("sn"));
      p.setDescription(context.getStringAttribute("description"));
      // The roleNames property of Person is an String array
      p.setRoleNames(context.getStringAttributes("roleNames"));
      return p;
   }
}

AbstractContextMapper を使用する

Spring LDAP は、AbstractContextMapper (Javadoc) と呼ばれる ContextMapper の抽象基本実装を提供します。この実装は、指定された Object パラメーターの DirContexOperations へのキャストを自動的に処理します。AbstractContextMapper を使用すると、前に示した PersonContextMapper を次のように書き直すことができます。

例 3: AbstractContextMapper を使用する
private static class PersonContextMapper extends AbstractContextMapper {
  public Object doMapFromContext(DirContextOperations ctx) {
     Person p = new Person();
     p.setFullName(ctx.getStringAttribute("cn"));
     p.setLastName(ctx.getStringAttribute("sn"));
     p.setDescription(ctx.getStringAttribute("description"));
     return p;
  }
}

DirContextAdapter を使用したデータの追加と更新

` 属性値を抽出するときに便利ですが、DirContextAdapter はデータの追加と更新に関連する詳細を管理するためにさらに強力です。

DirContextAdapter を使用したデータの追加

次の例では、DirContextAdapter を使用して、データの追加で示されている create リポジトリメソッドの改善された実装を実装しています。

例 4: DirContextAdapter を使用したバインディング
public class PersonRepoImpl implements PersonRepo {
   ...
   public void create(Person p) {
      Name dn = buildDn(p);
      DirContextAdapter context = new DirContextAdapter(dn);

      context.setAttributeValues("objectclass", new String[] {"top", "person"});
      context.setAttributeValue("cn", p.getFullname());
      context.setAttributeValue("sn", p.getLastname());
      context.setAttributeValue("description", p.getDescription());

      ldapClient.bind(dn).object(context).execute();
   }
}

バインドする 2 番目のパラメーターとして DirContextAdapter インスタンスを使用することに注意してください。これは Context である必要があります。属性を明示的に指定しないため、3 番目のパラメーターは null です。

また、objectclass 属性値を設定するときに setAttributeValues() メソッドを使用することにも注意してください。objectclass 属性は複数値です。多値属性データを抽出する際の問題と同様に、多値属性の構築は退屈で冗長な作業です。setAttributeValues() メソッドを使用することで、自分に合った DirContextAdapter ハンドルを使用できます。

DirContextAdapter を使用したデータの更新

modifyAttributes を使用した更新が推奨される方法であることは以前に説明しましたが、そのためには、属性の変更を計算し、それに応じて ModificationItem 配列を構築するタスクを実行する必要があります。DirContextAdapter は、次のように、これらすべてを実行できます。

例 5: DirContextAdapter を使用した更新
public class PersonRepoImpl implements PersonRepo {
   ...
   public void update(Person p) {
      Name dn = buildDn(p);
      DirContextOperations context = ldapClient.search().name(dn).toEntry();

      context.setAttributeValue("cn", p.getFullname());
      context.setAttributeValue("sn", p.getLastname());
      context.setAttributeValue("description", p.getDescription());

      ldapClient.modify(dn).attributes(context.getModificationItems()).execute();
   }
}

SearchSpec#toEntry を呼び出すと、結果はデフォルトで DirContextAdapter インスタンスになります。lookup メソッドは Object を返しますが、toEntry は戻り値を DirContextOperations (DirContextAdapter が実装するインターフェース) に自動的にキャストします。

LdapTemplate#create メソッドと LdapTemplate#update メソッドに重複したコードがあることに注意してください。このコードは、ドメインオブジェクトからコンテキストにマップされます。次のように、別のメソッドに抽出できます。

例 6: DirContextAdapter を使用した追加と変更
public class PersonRepoImpl implements PersonRepo {
   private LdapClient ldapClient;

   ...
   public void create(Person p) {
      Name dn = buildDn(p);
      DirContextAdapter context = new DirContextAdapter(dn);

      context.setAttributeValues("objectclass", new String[] {"top", "person"});
      mapToContext(p, context);
      ldapClient.bind(dn).object(context).execute();
   }

   public void update(Person p) {
      Name dn = buildDn(p);
      DirContextOperations context = ldapClient.search().name(dn).toEntry();
      mapToContext(person, context);
      ldapClient.modify(dn).attributes(context.getModificationItems()).execute();
   }

   protected void mapToContext (Person p, DirContextOperations context) {
      context.setAttributeValue("cn", p.getFullName());
      context.setAttributeValue("sn", p.getLastName());
      context.setAttributeValue("description", p.getDescription());
   }
}

属性値としての DirContextAdapter および識別名

LDAP でセキュリティグループを管理する場合、識別名を表す属性値を持つのが一般的です。識別名の等価性は文字列の等価性とは異なるため (たとえば、識別名の等価性では空白と大文字と小文字の違いは無視されます)、文字列の等価性を使用した属性変更の計算は期待どおりに機能しません。

たとえば、member 属性の値が cn=John Doe,ou=People で、ctx.addAttributeValue("member", "CN=John Doe, OU=People") を呼び出す場合、文字列が実際には同じ識別名を表している場合でも、属性は 2 つの値を持つと見なされます。

Spring LDAP 2.0 の時点で、属性変更メソッドに javax.naming.Name インスタンスを提供すると、DirContextAdapter は属性変更を計算するときに識別名の等価性を使用するようになります。前の例を ctx.addAttributeValue("member", LdapUtils.newLdapName("CN=John Doe, OU=People")) に変更すると、次の例に示すように、変更はレンダリングされません。

例 7: DirContextAdapter を使用したグループメンバーシップの変更
public class GroupRepo implements BaseLdapNameAware {
    private LdapClient ldapClient;
    private LdapName baseLdapPath;

    public void setLdapClient(LdapClient ldapClient) {
        this.ldapClient = ldapClient;
    }

    public void setBaseLdapPath(LdapName baseLdapPath) {
        this.setBaseLdapPath(baseLdapPath);
    }

    public void addMemberToGroup(String groupName, Person p) {
        Name groupDn = buildGroupDn(groupName);
        Name userDn = buildPersonDn(
            person.getFullname(),
            person.getCompany(),
            person.getCountry());

        DirContextOperation ctx = ldapClient.search().name(groupDn).toEntry();
        ctx.addAttributeValue("member", userDn);

        ldapClient.modify(groupDn).attributes(ctx.getModificationItems()).execute();
    }

    public void removeMemberFromGroup(String groupName, Person p) {
        Name groupDn = buildGroupDn(String groupName);
        Name userDn = buildPersonDn(
            person.getFullname(),
            person.getCompany(),
            person.getCountry());

        DirContextOperation ctx = ldapClient.search().name(groupDn).toEntry();
        ctx.removeAttributeValue("member", userDn);

        ldapClient.modify(groupDn).attributes(ctx.getModificationItems()).execute();
    }

    private Name buildGroupDn(String groupName) {
        return LdapNameBuilder.newInstance("ou=Groups")
            .add("cn", groupName).build();
    }

    private Name buildPersonDn(String fullname, String company, String country) {
        return LdapNameBuilder.newInstance(baseLdapPath)
            .add("c", country)
            .add("ou", company)
            .add("cn", fullname)
            .build();
   }
}

前の例では、ベース LDAP パスへの参照の取得に従って、ベース LDAP パスを取得するために BaseLdapNameAware を実装します。これが必要なのは、メンバー属性値としての識別名が常にディレクトリルートからの絶対値である必要があるためです。

完全な PersonRepository クラス

Spring LDAP と DirContextAdapter の有用性を説明するために、次の例は LDAP の完全な Person リポジトリ実装を示しています。

import java.util.List;

import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.ldap.LdapName;

import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.WhitespaceWildcardsFilter;

import static org.springframework.ldap.query.LdapQueryBuilder.query;

public class PersonRepoImpl implements PersonRepo {
   private LdapClient ldapClient;

   public void setLdapClient(LdapClient ldapClient) {
      this.ldapClient = ldapClient;
   }

   public void create(Person person) {
      DirContextAdapter context = new DirContextAdapter(buildDn(person));
      mapToContext(person, context);
      ldapClient.bind(context.getDn()).object(context).execute();
   }

   public void update(Person person) {
      Name dn = buildDn(person);
      DirContextOperations context = ldapClient.lookupContext(dn);
      mapToContext(person, context);
      ldapClient.modify(dn).attributes(context.getModificationItems()).execute();
   }

   public void delete(Person person) {
      ldapClient.unbind(buildDn(person)).execute();
   }

   public Person findByPrimaryKey(String name, String company, String country) {
      Name dn = buildDn(name, company, country);
      return ldapClient.search().name(dn).toObject(getContextMapper());
   }

   public List<Person> findByName(String name) {
      LdapQuery query = query()
         .where("objectclass").is("person")
         .and("cn").whitespaceWildcardsLike("name");

      return ldapClient.search().query(query).toList(getContextMapper());
   }

   public List<Person> findAll() {
      EqualsFilter filter = new EqualsFilter("objectclass", "person");
      return ldapClient.search().query((query) -> query.filter(filter)).toList(getContextMapper());
   }

   protected ContextMapper getContextMapper() {
      return new PersonContextMapper();
   }

   protected Name buildDn(Person person) {
      return buildDn(person.getFullname(), person.getCompany(), person.getCountry());
   }

   protected Name buildDn(String fullname, String company, String country) {
      return LdapNameBuilder.newInstance()
        .add("c", country)
        .add("ou", company)
        .add("cn", fullname)
        .build();
   }

   protected void mapToContext(Person person, DirContextOperations context) {
      context.setAttributeValues("objectclass", new String[] {"top", "person"});
      context.setAttributeValue("cn", person.getFullName());
      context.setAttributeValue("sn", person.getLastName());
      context.setAttributeValue("description", person.getDescription());
   }

   private static class PersonContextMapper extends AbstractContextMapper<Person> {
      public Person doMapFromContext(DirContextOperations context) {
         Person person = new Person();
         person.setFullName(context.getStringAttribute("cn"));
         person.setLastName(context.getStringAttribute("sn"));
         person.setDescription(context.getStringAttribute("description"));
         return person;
      }
   }
}
場合によっては、オブジェクトの識別名 (DN) は、オブジェクトのプロパティを使用して構築されます。前の例では、Person の国、会社、フルネームが DN で使用されています。つまり、これらのプロパティを更新するには、実際には、Attribute 値の更新に加えて、rename() 操作を使用して LDAP ツリー内のエントリを移動する必要があります。これは非常に実装固有であるため、ユーザーがこれらのプロパティを変更できないようにするか、必要に応じて update() メソッドで rename() 操作を実行することにより、自分自身を追跡する必要があります。オブジェクトディレクトリマッピング (ODM) を使用すると、ドメインクラスに適切にアノテーションを付ければ、ライブラリが自動的にこれを処理できることに注意してください。