LECTURE 4 ディレクトリエントリの検索

○ はじめに

LDAP クライアントで最も使用される機能である(と自分では思ってるのですが。。。)検索処理について今回は説明していきます。前回登録したエントリの検索もできますし、身近に公開されている LDAP サーバなどがあればそのディレクトリエントリの検索も思うがままです! ぜひ活用してみて下さい。

○ サンプルプログラム

LDAP サーバに接続して、指定条件に該当するエントリの全属性を表示するサンプルプログラム LDAPSearch.java です。RFC 2254 で指定されている検索フィルタを使用することができます(というかこれしか使えない。。。)。

コンパイルが終了したら

java LDAPSearch [LDAPサーバ名/IPアドレス] \
                [bindユーザのDN] \
                [bindユーザのパスワード] \
                [検索ベース] \
                [検索フィルタ]

で実行です。正常に処理が終了した場合は、指定した条件に該当するエントリの全属性が表示されます(該当エントリが無い場合は何も表示されません)。例外が発生したら、その内容が表示されます。

なお、はじめに のページから全てのサンプルプログラムを固めた tar ボールをダウンロードすることもできます。

--- LDAPSearch.java ---

    1	import java.util.Enumeration;
    2	import org.ietf.ldap.LDAPAttribute;
    3	import org.ietf.ldap.LDAPAttributeSet;
    4	import org.ietf.ldap.LDAPConnection;
    5	import org.ietf.ldap.LDAPEntry;
    6	import org.ietf.ldap.LDAPException;
    7	import org.ietf.ldap.LDAPSearchResults;
    8	
    9	
   10	/**
   11	 * 指定された LDAP サーバに接続し、条件に応じたディレクトリエントリを検索し、
   12	 * 指定された属性を表示する。
   13	 *
   14	 * @version 19 May 2002
   15	 * @author  Ryo Ichinose
   16	 */
   17	public class LDAPSearch
   18	{
   19		/**
   20		 * LDAP Protocol Version 3
   21		 */
   22		public static final int LDAP_V3 = 3; // LDAPv3
   23	
   24	
   25		/**
   26		 * 指定された LDAP サーバに接続し、条件に応じたディレクトリエントリ
   27		 * を検索し、そのエントリの属性を表示する。指定検索ベース以下全ての
   28		 * エントリを検索する。
   29		 *
   30		 * @param host    ホスト名もしくは IPアドレス
   31		 * @param binddn  bind ユーザの DN
   32		 * @param bindpw  bind ユーザのパスワード
   33		 * @param base    検索ベース
   34		 * @param filter  検索フィルタ
   35		 */
   36		public void search( String host, String binddn, String bindpw,
   37				    String base, String filter )
   38		{
   39			// 検索属性リスト(全属性指定)
   40			String attrs[] = { LDAPConnection.ALL_USER_ATTRS };
   41	
   42			LDAPConnection lc = new LDAPConnection();
   43	
   44			try {
   45				// LDAP サーバに接続する。ポートはデフォルト(389番)を使う。
   46				lc.connect( host, LDAPConnection.DEFAULT_PORT );
   47				System.out.println( "Connected to " + host );
   48				System.out.println();
   49	
   50	
   51				// 指定された DN およびパスワードでバインド(認証)する。
   52				// これを実行しない場合、anonymous 状態で操作することになる。
   53				lc.bind( LDAPSearch.LDAP_V3, binddn, bindpw );
   54	
   55	
   56				// 指定条件でのディレクトリエントリの検索を行う。
   57				LDAPSearchResults results =	lc.search( 
   58					base,                     // 検索ベース
   59					LDAPConnection.SCOPE_SUB, // 検索スコープ
   60					filter,                   // 検索フィルタ
   61					attrs,                    // 検索属性リスト
   62					false                     // 属性名および属性値の取得
   63				);
   64	
   65				// 得られたエントリを全て表示する。
   66				while( results.hasMoreElements() ) {
   67					showLDAPEntry( results.next() );
   68				}
   69	
   70				// 接続を切断する。
   71				lc.disconnect();
   72				System.out.println();		
   73				System.out.println( "Disconnected" );
   74			}
   75			catch( LDAPException e ) {
   76				System.out.println( "LDAPException occured : " + 
   77						    e.toString() );
   78			}
   79		}
   80	
   81	
   82		private void showLDAPEntry( LDAPEntry entry )
   83		{
   84			System.out.println( "Listing attributes of " + entry.getDN() + " :" );
   85			Enumeration items = entry.getAttributeSet().getAttributes();
   86	
   87			while( items.hasMoreElements() ) {
   88				// 各属性に対して複数の属性値が設定されている可能性があるが、
   89				// ここでは最初の属性値のみ表示することにする。
   90				LDAPAttribute attr = ( LDAPAttribute )items.nextElement();
   91				Enumeration vals = attr.getStringValues();
   92				String val  = ( String )vals.nextElement();
   93				System.out.println( "  " + attr.getName() + ": " + val );
   94			}
   95	
   96			System.out.println();		
   97		}
   98	
   99	
  100		/**
  101		 * エントリポイント
  102		 *
  103		 * @param args 
  104		 *    <ul>
  105		 *      <li>args[0] --- ホスト名もしくは IP アドレス
  106		 *      <li>args[1] --- bind ユーザの DN
  107		 *      <li>args[2] --- bind ユーザのパスワード
  108		 *      <li>args[3] --- 検索ベース
  109		 *      <li>args[4] --- 検索フィルタ
  110		 *    </ul>
  111		 */
  112		public static void main( String[] args )
  113		{
  114			LDAPSearch obj = new LDAPSearch();
  115			obj.search( args[0], args[1], args[2], args[3], args[4] );
  116		}
  117	}


○ サンプルプログラムの説明

では、サンプルプログラムについて説明していきます。

  • 1 〜 7 行目

    使用するクラスライブラリのパッケージ指定を行っています。LDAPSearch.java では新たに LDAPSearchResults クラスを使用しています。

  • 25 〜 79 行目 ( LDAPSearch#search() )

    LDAPSearch#search() メソッドで、ディレクトリエントリの検索処理を行います。各ヶ所に ついて以降で説明します。

    • 39 〜 40 行目

      LDAPConnection#search() メソッドに指定する検索属性リストを配列として作成しています。 この配列に検索したい属性名を列挙しておくと、それらの値のみを取得することができます (ex. String attrs[] = { "objectclass", "cn", "uid" }; )。

      ここでは LDAPConnection.ALL_USER_ATTRS を指定していますが、これは全ての属性情報を取得 することを指定するためのものです。ちなみに LDAPConnection.NO_ATTRS を指定すると、各 属性についての情報は取得せず条件に該当するエントリの名称のみを取得することができます。

    • 56 〜 63 行目

      LDAPConnection#search() メソッドを使ってディレクトリエントリの検索を実行しています。

          public LDAPSearchResults LDAPConnection#search( 
                                           String   base,
                                           int      scope,
                                           String   filter,
                                           String[] attrs,
                                           boolean  typesOnly )
             throws LDAPException
      
          base      : 検索ベース
          scope     : 検索スコープ 
                       ( LDAPConnection.SCOPE_BASE /
                         LDAPConnection.SCOPE_ONE  /
                         LDAPConnection.SCOPE_SUB )
      
          filter    : 検索フィルタ
          attrs     : 情報を取得する属性名を列挙した配列
          typesOnly : 属性名のみを取得するか(true)、値との組で取得するか(false)を指定
      
      

      検索ベースには検索対象ツリーのトップとなる DN を指定します。

      検索スコープには

      • LDAPConnection.SCOPE_BASE
      • LDAPConnection.SCOPE_ONE
      • LDAPConnection.SCOPE_SUB

      のいずれかを指定します。SCOPE_BASE は指定 DN のディレクトリエントリのみを検索、SCOPE_ONE は更に子のディレクトリエントリも検索、SCOPE_SUB は更に孫以下のエントリも検索 ( つまり検索ベース以下全てのエントリの検索 )します。

      検索フィルタには、RFC 2254 で定義されている書式でフィルタを指定します。具体的には以下の 様なフィルタを指定できます(一例)。

      書式内容
      uid=abc 左辺の属性名に対応する値が右辺であるディレクトリエントリを検索する。属性値の一部を * 指定すると、その部分は何でも良いが明示的に指定した文字列の部分を含むディレクトリ エントリを検索する。属性値を完全に * とすると、単に左辺の属性を持っている ディレクトリエントリの検索になる。
      objectclass=* 属性名を objectclass とすると、全てのエントリがこの属性を持つため全てのエントリの 検索になる。
      uid~=abc 右辺に似ている文字列を左辺の属性の値に含むディレクトリエントリを検索する。
      uidNumber>=500 左辺の属性名の属性値と右辺の比較式が成り立つエントリの検索を行う。>=、<= は使えるが、 >、< は使えない(らしい)。未満、より大きいという表現は、これと否定( ! )を組み合わせる 必要がある(ようです)。
      (!(検索フィルタ)) 検索フィルタに該当しない(検索フィルタの否定)エントリの検索を意味する。
      (&(検索フィルタ1)(検索フィルタ2)) 検索フィルタ 1 および検索フィルタ 2 の両方に該当するエントリを検索する。検索フィルタ は 2 つだけでなくどんどんつなげていくことができる。
      (|(検索フィルタ1)(検索フィルタ2)) 検索フィルタ 1 もしくは検索フィルタ 2 のいずれかに該当するエントリを検索する。& 同様 検索フィルタはどんどんつなげていくことができる。

      何らかの理由で検索に失敗(見付けられなかったのではなく、異常発生)した場合は、LDAPException が throw されます。例外の内容については後述します。

      search() メソッドは上の形式以外の形でいくつかオーバーロードされています。それぞれの詳細につ いては JLDAP の API ドキュメントを参照して下さい。

    • 65 〜 68 行目

      LDAPConnection#search() によって得られた検索結果の表示処理を行っています。LDAPSearch.java では、LDAPSearchResults クラスのインスタンスを検索結果として返す LDAPConnection#search() メソッドを使用しています。この LDAPSearchResults は java.util.Enumeration インタフェース を実装しているので、通常の Enumeration を扱うのと同様に処理していきます。

      検索の結果得られた各エントリは、それぞれ LDAPEntry クラスのインスタンスとして LDAPSearchResults に格納されています。各 LDAPEntry の処理は、後述する showLDAPEntry() で行います。

    • 75 〜 78 行目

      ここまでの処理で発生する可能性のある LDAPException を catch しています。今回出て来た 新たな処理によって発生する可能性のある例外内容について、自分が確認できた範囲で以下に まとめておきます。これで全てという意味ではありません。

      番号 エラーコードの定数名(LDAPException.XXX の形で参照可) 内容
      2 PROTOCOL_ERROR LDAP サーバがクライアントから不正な要求を受け取った。LDAPSearch.java の場合、 検索スコープに不正な値を指定すると発生する。
      32 NO_SUCH_OBJECT LDAPConnection#search() に、不正な(構文は正しい)検索ベースを指定した場合に発生する。
      34 INVALID_DN_SYNTAX 指定した DN の構文が正しくない。
      87 FILTER_ERROR LDAPConnection#search() に指定した検索フィルタが不正である場合に発生する。

    • 82 〜 97 行目( LDAPSearch#showLDAPEntry() )

      LDAPEntry の内容を表示します。

      各エントリが保持している属性情報については、前回のエントリの登録/削除 の所で説明したように、1 つの属性名と属性値の組が LDAPAttribute クラスのインスタンスに設定 されています。そして LDAPAttributeSet クラスのインスタンスに複数の LDAPAttribute クラスの インスタンスが格納されることで複数属性の情報が保持されています。

      処理順序を簡単に書くと以下の通りになってます(記述も簡略化)

      1. LDAPEntry に設定されている DN を表示する
      2. LDAPEntry に設定されている LDAPAttributeSet から、設定されている複数の LDAPAttribute を Enumeration として取りだす
      3. ↑で取りだした Enumeration から順次 LDAPAttribute を取りだす
      4. LDAPAttribute に設定されている属性の属性名と値を表示する

      2 において、LDAPAttributeSet#getAttributes() を使って複数属性を Enumeration として処理して いますが、LDAPAttributeSet#size() と LDAPAttributeSet#elementAt() を使って処理することも 可能です。お好きな方法でどうぞ。

      4 では、LDAPAttribute#getStringValues() を使って各属性に設定されている属性値を Enumeration として取りだしています。LDAP では各属性に複数の属性値が設定されている場合がありますので、 この様に Enumeration として扱うか、 LDAPAttribute#getStringValueArray() を使って配列として 処理するかする必要があります。

      ここで使用している LDAPEntry、LDAPAttributeSet、LDAPAttribute クラスの 様々なメソッドの詳細については、JLDAP の API ドキュメントを参照して下さい。

以上で LDAPSearch.java についての説明は終わりです。

○ 1000 件以上の検索結果の取得

実は、このままだと LDAP サーバ側に該当エントリが何件あろうとも、1000 件以上の検索結果を得ることができません。これは、不要な検索処理を行わないなどの枝刈り条件のようなものだと思いますが、もしそれ以上の数の検索結果を取得したい場合は、LDAPSearchConstraints クラスを使って最大取得結果数を設定する必要があります。

具体的には、まず LDAPSearchConstraints クラスのインスタンスを作成して LDAPSearchConstraints#setMaxResults() で最大取得数を設定します。ちなみに setMaxResults() に 0 を設定すると、上限無しになります。

LDAPSearchConstraints cs = new LDAPSearchConstraints();
cs.setMaxResults( 0 ); // 検索件数制限解除

そして、検索を実行する前にこの LDAPSearchConstraints クラスのインスタンスを LDAPConnection#setConstraints() を使って LDAPConnection クラスのインスタンスに設定するか、LDAPSearchConstraints クラスのインスタンスを引数に取る LDAPConnection#search() を使って検索を実行するかのいずれかで、1000 件以上の検索結果を取得することができます。

public LDAPSearchResults LDAPConnection#search( 
                                 String                base,
                                 int                   scope,
                                 String                filter,
                                 String[]              attrs,
                                 boolean               typesOnly,
                                 LDAPSearchConstraints cons )
   throws LDAPException

# LDAPSearchConstraints を引数とする search() メソッドは、↑の他に
# もあるので、JLDAP の API ドキュメントを参照してください。

public void LDAPConnection#setConstraints( LDAPConstraints cons )

# LDAPSearchConstraints は LDAPConstraints のサブクラスなので、問題無し