Sunday, January 12, 2014

UnboundID SDK には Persistence Framework というものが

UnboundID LDAP SDK for Java には, ディレクトリサービスにおけるエントリと Java におけるオブジェクトとを橋渡しする試みとして, Persistence Framework というものがあります。 リレーショナルデータベースとオブジェクト指向言語における JPA や Hibernate などの ORM に比べたらまだまだかもしれませんが, なかなかおもしろそうな気がしてはいます。

少しだけ触ってみましたので, ここに書きとめておきたいと思います。

ちなみに, 手元の環境の LDAP サーバーは OpenLDAP 2.4.38 でして, そのディレクトリにはすでに次のようなエントリが存在しているものとします。

dn: ou=Prog Rock,dc=localdomain
objectClass: organizationalUnit
ou: Prog Rock

今回は, (1) このエントリの直下に inetOrgPerson クラスのエントリをひとつ追加し, (2) 追加したエントリの属性を一部変更し, (3) そしてそのエントリを削除する, ということをやってみました。 利用した API は, ここ最近ちょっと気に入っている UnboundID LDAP SDK for Java の 2.3.5 です。 Java 実行環境は JDK 7u45 です。

エントリに対応するクラスを用意します

ディレクトリサービスにおけるエントリに対応するオブジェクトのクラスを, まずは用意します。 そのクラスには @LDAPObject というアノテーションを付与し, エントリの属性に対応するフィールドには @LDAPField というアノテーションを付与します。

import com.unboundid.ldap.sdk.ReadOnlyEntry;
import com.unboundid.ldap.sdk.persist.LDAPEntryField;
import com.unboundid.ldap.sdk.persist.LDAPField;
import com.unboundid.ldap.sdk.persist.LDAPObject;
import java.util.LinkedList;
import java.util.List;

@LDAPObject(structuralClass="inetOrgPerson")
public class Musician {
    
    @LDAPEntryField
    private ReadOnlyEntry entry;
    
    @LDAPField(inRDN=true)
    private List<String> cn;
    
    @LDAPField
    private List<String> sn;
    
    @LDAPField
    private List<String> description;
    
    @LDAPField
    private List<String> givenName;
    
    private Musician() {}
    
    public Musician(String commonName, String surname) {
        if (commonName == null || surname == null
                || commonName.isEmpty() || surname.isEmpty())
            throw new IllegalArgumentException();
        cn = new LinkedList<>();
        cn.add(commonName);
        sn = new LinkedList<>();
        sn.add(surname);
    }
    
    public String getDN() {
        if (entry == null)
            return null;
        return entry.getDN();
    }
    
    public void addDescriptions(String... descriptions) {
        if (description == null)
            description = new LinkedList<>();
        for (String s : descriptions)
            if (s != null && !s.isEmpty() && !description.contains(s))
                description.add(s);
        if (description.isEmpty())
            description = null;
    }
    
}

@LDAPField を付与したフィールドのうち, どれかひとつは DN 文字列を構成する RDN とならなければなりませんので, どれかひとつに isRDN=true を指定することが必要です。 各フィールドを List としたのは, LDAP においてはひとつの属性が複数の値を持つことが一般的だからです。 (属性の定義によってはそうでないものもあるようですが。) List の代わりに配列にしなかったのは, 属性の数は可変長だからです。 本当は, ある属性が複数の値を持ちうる場合には重複が許されないらしいので Set を使うことも考えましたが, 追加された順番が意味を持つ局面もありうるので, List を採用し, そのうえで追加の際には重複をはじくことにするという書き方にしました。 なお, List の実装は, 基本的にランダムアクセスが必要となる機会はおそらくあまりなくて, 追加や削除をおこなう機会のほうが多いんじゃないかと思ったので, ArrayList ではなく LinkedList にしました。

ところどころ isEmpty かどうかといった確認を入れていますが, それは LDAP においては空文字列のような長さ 0 の値は原則として禁止されているからです。 長さ 0 の値をあてがって LDAP サーバーに叱られるくらいなら, そもそもその属性を持たせなければいいわけでして, null にしておけば 「その属性を持たない」 を意味することになります。 (ただし, cnsn という二つの属性は, inetOrgPerson における MUST 属性ですので, null を許容せず IAE を投げることにしました。)

なお, 引数を取らないコンストラクターが必要らしいので, それも用意しました。 アクセスレベルは private でもかまわないようです。

少しためしてみます

さっそく, ためしてみます。 以下のクライアントコードは, 新たに作成した Musician オブジェクトを, LDAP サーバーに新規エントリとして追加しようとするものです。

        // LDAPConnection connection = ...
        
        Musician musician = new Musician("Mike Oldfield", "Oldfield");
        musician.addDescriptions("A British musician born in 1953");
        
        String parentDN = "ou=Prog Rock,dc=localdomain";
        
        LDAPPersister persister
                = LDAPPersister.getInstance(Musician.class);
        persister.add(musician, connection, parentDN);

成功すれば, LDAP サーバーには, 次のようなエントリが作成されるはずです。

dn: cn=Mike Oldfield,ou=Prog Rock,dc=localdomain
objectClass: inetOrgPerson
cn: Mike Oldfield
description: A British musician born in 1953
sn: Oldfield

内容を一部変更したいと思います

Musician クラスに, 次のようなメソッドを追加してみます。

    public void deleteDescriptions(String... descriptions) {
        if (description == null)
            return;
        for (String s : descriptions)
            if (s != null && !s.isEmpty())
                description.remove(s);
        if (description.isEmpty())
            description = null;
    }
    
    public void addGivenNames(String... givenNames) {
        if (givenName == null)
            givenName = new LinkedList<>();
        for (String s : givenNames)
            if (s != null && !s.isEmpty() && !givenName.contains(s))
                givenName.add(s);
        if (givenName.isEmpty())
            givenName = null;
    }

追加したメソッドを使ってみたいと思います。 次のようなクライアントコードを実行します。

        // LDAPConnection connection = ...
        
        String dn = "cn=Mike Oldfield,ou=Prog Rock,dc=localdomain";
        SearchScope scope = SearchScope.BASE;
        SearchResult result
                = connection.search(dn, scope, "(objectClass=*)");
        SearchResultEntry entry = result.getSearchEntry(dn);
        
        LDAPPersister persister
                = LDAPPersister.getInstance(Musician.class);
        Musician musician = persister.decode(entry);
        
        musician.addDescriptions("Tubular Bells (1973) is one of his best.");
        musician.deleteDescriptions("A British musician born in 1953");
        musician.addGivenNames("Mike", "Michael", "Michael Gordon");
        
        persister.modify(musician, connection, dn, true);

新規エントリを追加するわけではなく, 既存エントリを変更するわけですから, Musician オブジェクトは新たに作成するのではなく検索オペレーションの結果から取得しています。 そして, description 属性と givenName 属性に対応する各フィールドに変更を加えたら, それをディレクトリ上の実際のエントリのほうにも反映させます。

成功すれば, このエントリは次のように変化しているはずです。

dn: cn=Mike Oldfield,ou=Prog Rock,dc=localdomain
objectClass: inetOrgPerson
cn: Mike Oldfield
sn: Oldfield
givenName: Mike
givenName: Michael
givenName: Michael Gordon
description: Tubular Bells (1973) is one of his best.

用が済んだので削除してもよろしいでしょうか

削除します。

        // LDAPConnection connection = ...
        
        String dn = "cn=Mike Oldfield,ou=Prog Rock,dc=localdomain";
        SearchScope scope = SearchScope.BASE;
        SearchResult result
                = connection.search(dn, scope, "(objectClass=*)");
        SearchResultEntry entry = result.getSearchEntry(dn);
        
        LDAPPersister persister
                = LDAPPersister.getInstance(Musician.class);
        Musician musician = persister.decode(entry);
        
        persister.delete(musician, connection);

はい, これでこのエントリは, 削除されました。

result: 32 No such object

まとめ

うまくいくと, ちょっとうれしいです。

No comments:

Post a Comment