    001/*
    002 * Copyright (c) 2009 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.hayabusa.resource;
    017
    018import org.opengion.hayabusa.common.HybsSystem;
    019import org.opengion.hayabusa.common.HybsSystemException;
    020import org.opengion.fukurou.util.StringUtil;
    021
    022import java.util.Hashtable;
    023import java.util.List;
    024import java.util.ArrayList;
    025import java.util.Arrays;
    026import java.util.Comparator ;
    027import java.io.Serializable;
    028
    029import javax.naming.Context;
    030import javax.naming.NamingEnumeration;
    031import javax.naming.NamingException;
    032import javax.naming.directory.DirContext;
    033import javax.naming.directory.InitialDirContext;
    034import javax.naming.directory.SearchControls;
    035import javax.naming.directory.SearchResult;
    036import javax.naming.directory.Attribute;
    037import javax.naming.directory.Attributes;
    038
    039/**
    040 * LDAPの内容を検索するための、ldapQueryタグです。
    041 *
    042 * 検索した結果は、配列で取得します。
    043 *
    044 * 下記の項目については、src/resource/システムパラメータ に、予め
    045 * 設定しておくことで、タグごとに指定する必要がなくなります。
    046 * ・LDAP_INITIAL_CONTEXT_FACTORY
    047 * ・LDAP_PROVIDER_URL
    048 * ・LDAP_ENTRYDN
    049 * ・LDAP_PASSWORD
    050 * ・LDAP_SEARCH_BASE
    051 * ・LDAP_SEARCH_SCOPE
    052 * ・LDAP_SEARCH_REFERRAL
    053 *
    054 * @og.rev 3.7.1.0 (2005/04/15) ＬＤＡＰにアクセスできる、LDAPSearch.java を新規に作成。
    055 * @og.group その他入力
    056 *
    057 * @version  4.0
    058 * @author       Kazuhiko Hasegawa
    059 * @since    JDK5.0,
    060 */
    061public class LDAPSearch {
    062
    063        private String                  initctx                         = HybsSystem.sys( "LDAP_INITIAL_CONTEXT_FACTORY" );
    064        private String                  providerURL             = HybsSystem.sys( "LDAP_PROVIDER_URL" );
    065        private String                  entrydn                         = HybsSystem.sys( "LDAP_ENTRYDN" );
    066        private String                  password                        = HybsSystem.sys( "LDAP_PASSWORD" );            // 4.2.2.0 (2008/05/10)
    067        private String                  searchbase                      = HybsSystem.sys( "LDAP_SEARCH_BASE" );
    068        private String                  referral                        = HybsSystem.sys( "LDAP_SEARCH_REFERRAL" ); // 5.6.7.0 (201/07/27)
    069
    070        // 検索範囲。OBJECT_SCOPE、ONELEVEL_SCOPE、SUBTREE_SCOPE のどれか 1 つ
    071        private String                  searchScope                     = HybsSystem.sys( "LDAP_SEARCH_SCOPE" );
    072        private static final long       COUNTLIMIT              = 0;                    // 返すエントリの最大数。0 の場合、フィルタを満たすエントリをすべて返す
    073        private int                             timeLimit                       = 0;                    // 結果が返されるまでのミリ秒数。0 の場合、無制限
    074        private String[]                attrs                           = null;                 // エントリと一緒に返される属性の識別子。null の場合、すべての属性を返す。空の場合、属性を返さない
    075        private boolean                 returningObjFlag        = false;                // true の場合、エントリの名前にバインドされたオブジェクトを返す。false 場合、オブジェクトを返さない
    076        private boolean                 derefLinkFlag           = false;                // true の場合、検索中にリンクを間接参照する
    077
    078        private int                             executeCount            = 0;                    // 検索/実行件数
    079        private int                     maxRowCount                     = 0;                    // 最大検索数(0は無制限)
    080        private SearchControls  constraints                     = null;
    081        private DirContext              ctx                                     = null;
    082        private String[]                orderBy                         = null;                 // ｿｰﾄ項目(csv)
    083        private boolean[]               desc                            = null;                 // 降順ﾌﾗｸﾞ
    084
    085        /**
    086         * LDAPパラメータを利用して、LDAP検索用オブジェクトを構築します。
    087         *
    088         * @og.rev 4.2.2.0 (2008/05/10) LDAP パスワード取得対応
    089         * @og.rev 5.6.7.0 (2013/07/27) LDAPのREFERRAL対応
    090         *
    091         * 通常、パラメータをセット後、search( String filter ) の実行前に、呼びます。
    092         */
    093        public void init() {
    094                Hashtable<String,String> env = new Hashtable<String,String>();
    095                env.put(Context.INITIAL_CONTEXT_FACTORY, initctx);
    096                env.put(Context.PROVIDER_URL, providerURL);
    097                if( ! StringUtil.isNull( referral ) ) { // 5.6.7.0 (2013/07/27)
    098                        env.put( Context.REFERRAL, referral );
    099                }
    100                // 3.7.1.1 (2005/05/31)
    101                if( ! StringUtil.isNull( password ) ) {
    102                        env.put( Context.SECURITY_CREDENTIALS, password.trim() );
    103                }
    104                // 4.2.2.0 (2008/05/10) entrydn 属性の追加
    105                if( ! StringUtil.isNull( entrydn ) ) {
    106                        env.put( Context.SECURITY_PRINCIPAL  , entrydn );
    107                }
    108
    109                try {
    110                        ctx = new InitialDirContext(env);
    111                        constraints = new SearchControls(
    112                                                                        changeScopeString( searchScope ),
    113                                                                        COUNTLIMIT                      ,
    114                                                                        timeLimit                       ,
    115                                                                        attrs                           ,
    116                                                                        returningObjFlag        ,
    117                                                                        derefLinkFlag
    118                                                                                );
    119                } catch ( NamingException ex ) {
    120                        String errMsg = "LDAP検索用オブジェクトの初期化に失敗しました。" ;
    121                        throw new HybsSystemException( errMsg,ex );             // 3.5.5.4 (2004/04/15) 引数の並び順変更
    122                }
    123        }
    124
    125        /**
    126         * LDPA から、値を取り出し、List オブジェクトを作成します。
    127         * 引数の headerAdd をtrueにする事により、１件目に、キー情報の配列を返します。
    128         *
    129         * @og.rev 4.2.2.0 (2008/05/10) LDAP パスワード取得対応
    130         *
    131         * @param       filter  フィルター文字列
    132         *
    133         * @return      検索結果の Listオブジェクト
    134         */
    135        public List<String[]> search( final String filter ) {
    136
    137                List<String[]> list = new ArrayList<String[]>();
    138                try {
    139                        NamingEnumeration<SearchResult> results = ctx.search(searchbase, filter, constraints);    // 4.3.3.6 (2008/11/15) Generics警告対応
    140
    141                        while (results != null && results.hasMore()) {
    142                                if( maxRowCount > 0 && maxRowCount <= executeCount ) { break ; }
    143                                SearchResult si = results.next();               // 4.3.3.6 (2008/11/15) Generics警告対応
    144                                Attributes at = si.getAttributes();
    145                                // attrs が null の場合は、キー情報を取得します。
    146                                if( attrs == null ) {
    147                                        NamingEnumeration<String> ne = at.getIDs();       // 4.3.3.6 (2008/11/15) Generics警告対応
    148                                        List<String> lst = new ArrayList<String>();
    149                                        while( ne.hasMore() ) {
    150                                         lst.add( ne.next() );  // 4.3.3.6 (2008/11/15) Generics警告対応
    151                                        }
    152                                        ne.close();
    153                                        attrs = lst.toArray( new String[lst.size()] );
    154                                }
    155
    156                                String[] values = new String[attrs.length];
    157                                boolean flag = false;           // 属性チェックフラグ
    158                                for( int i=0; i<attrs.length; i++ ) {
    159                                        if( maxRowCount > 0 && maxRowCount <= executeCount ) { break ; }
    160                                        Attribute attr = at.get(attrs[i]);
    161                                        if( attr != null ) {
    162                                                NamingEnumeration<?> vals = attr.getAll();        // 4.3.3.6 (2008/11/15) Generics警告対応
    163                                                StringBuilder buf = new StringBuilder();
    164//                                              if( vals.hasMore() ) { buf.append( vals.next() ) ;}
    165                                                if( vals.hasMore() ) { getDataChange( vals.next(),buf ) ;}      // 4.2.2.0 (2008/05/10)
    166                                                while ( vals.hasMore() ) {
    167                                                        buf.append( "," ) ;
    168//                                                      buf.append( vals.next() ) ;
    169                                                        getDataChange( vals.next(),buf ) ;      // 4.2.2.0 (2008/05/10)
    170                                                }
    171                                                values[i] = buf.toString();
    172                                                flag = true;
    173                                        }
    174                                }
    175                                if( flag ) {
    176                                        list.add( values );
    177                                        executeCount++ ;
    178                                }
    179                        }
    180                        if( results != null ) { results.close(); }
    181                } catch ( NamingException ex ) {
    182                        String errMsg = "List オブジェクトの検索に失敗しました。"
    183                                                + HybsSystem.CR
    184                                                + "searchbase や、entrydn の記述をご確認ください。"
    185                                                + HybsSystem.CR
    186                                                + "searchbase:" + searchbase
    187                                                + " , entrydn:" + entrydn ;
    188                        throw new HybsSystemException( errMsg,ex );             // 3.5.5.4 (2004/04/15) 引数の並び順変更
    189                }
    190                return sort( list,attrs ) ;
    191        }
    192
    193        /**
    194         * LDAPから取得したデータの変換を行います。
    195         *
    196         * 主に、バイト配列(byte[]) オブジェクトの場合、文字列に戻します。
    197         *
    198         * @og.rev 4.2.2.0 (2008/05/10) 新規追加
    199         *
    200         * @param       obj     主にバイト配列データ
    201         * @param       buf     元のStringBuilder
    202         *
    203         * @return      データを追加したStringBuilder
    204         */
    205        private StringBuilder getDataChange( final Object obj, final StringBuilder buf ) {
    206                if( obj == null ) { return buf; }
    207                else if( obj instanceof byte[] ) {
    208                        byte[] bb = (byte[])obj ;
    209                        char[] chs = new char[bb.length];
    210                        for( int i=0; i<bb.length; i++ ) {
    211                                chs[i] = (char)bb[i];
    212                        }
    213                        buf.append( chs );
    214                }
    215                else {
    216                        buf.append( obj ) ;
    217                }
    218
    219                return buf ;
    220        }
    221
    222        /**
    223         * 検索範囲(OBJECT/ONELEVEL/SUBTREE)を設定します(初期値:LDAP_SEARCH_SCOPE)。
    224         *
    225         * 検索範囲を OBJECT_SCOPE、ONELEVEL_SCOPE、SUBTREE_SCOPE のどれか 1 つです。
    226         * 指定文字列は、それぞれ『OBJECT』『ONELEVEL』『SUBTREE』です。
    227         *
    228         * @param       scope   SearchControlsの検索範囲
    229         */
    230        public void setSearchScope( final String scope ) {
    231                searchScope = StringUtil.nval( scope, searchScope );
    232                if( ! "OBJECT".equals( searchScope ) &&
    233                        ! "ONELEVEL".equals( searchScope ) &&
    234                        ! "SUBTREE".equals( searchScope ) ) {
    235                                String errMsg = "検索範囲は、『OBJECT』『ONELEVEL』『SUBTREE』の中から選択して下さい。"
    236                                                                + "[" + searchScope + "]" ;
    237                                throw new HybsSystemException( errMsg );
    238                }
    239        }
    240
    241        /**
    242         * 引数の searchScope 文字列(『OBJECT』『ONELEVEL』『SUBTREE』のどれか)を、
    243         * SearchControls クラス定数である、OBJECT_SCOPE、ONELEVEL_SCOPE、SUBTREE_SCOPE のどれか
    244         *  1 つに設定します。
    245         *
    246         * @param       scope   searchScope文字列
    247         *
    248         * @return      SearchControls定数
    249         */
    250        private int changeScopeString( final String scope ) {
    251                final int rtnScope;
    252                if( "OBJECT".equals( scope ) )        { rtnScope = SearchControls.OBJECT_SCOPE ; }
    253                else if( "ONELEVEL".equals( scope ) ) { rtnScope = SearchControls.ONELEVEL_SCOPE ; }
    254                else if( "SUBTREE".equals( scope ) )  { rtnScope = SearchControls.SUBTREE_SCOPE ; }
    255                else {
    256                        String errMsg = "Search Scope in 『OBJECT』『ONELEVEL』『SUBTREE』Selected"
    257                                                        + "[" + searchScope + "]" ;
    258                        throw new HybsSystemException( errMsg );
    259                }
    260                return rtnScope ;
    261        }
    262
    263        /**
    264         * これらの SearchControls の時間制限をミリ秒単位で設定します(初期値:0[無制限])。
    265         *
    266         * 値が 0 の場合、無制限に待つことを意味します。
    267         *
    268         * @param       limit   ミリ秒単位の時間制限(初期値:無制限)
    269         */
    270        public void setTimeLimit( final int limit ) {
    271                timeLimit = limit;
    272        }
    273
    274        /**
    275         * 検索中のリンクへの間接参照を有効または無効[true/false]にします(初期値:false)。
    276         *
    277         * 検索中のリンクへの間接参照を有効または無効にします。
    278         *
    279         * @param       deref   リンクを逆参照する場合は true、そうでない場合は false(初期値:false)
    280         */
    281        public void setDerefLinkFlag( final boolean deref ) {
    282                derefLinkFlag = deref;
    283        }
    284
    285        /**
    286         * 結果の一部としてオブジェクトを返すことを有効または無効[true/false]にします(初期値:false)。
    287         *
    288         * 無効にした場合、オブジェクトの名前およびクラスだけが返されます。
    289         * 有効にした場合、オブジェクトが返されます。
    290         *
    291         * @param       pbjflag オブジェクトが返される場合は true、そうでない場合は false(初期値:false)
    292         */
    293        public void setReturningObjFlag( final boolean pbjflag ) {
    294                returningObjFlag = pbjflag;
    295        }
    296
    297        /**
    298         * レジストリの最大検索件数をセットします(初期値:0[無制限])。
    299         *
    300         * DBTableModelのデータとして登録する最大件数をこの値に設定します。
    301         * サーバーのメモリ資源と応答時間の確保の為です。
    302         * 0 は、無制限です。(初期値は、無制限です。)
    303         *
    304         * @param       count   レジストリの最大検索件数
    305         */
    306        public void setMaxRowCount( final int count ) {
    307                maxRowCount = count;
    308        }
    309
    310        /**
    311         * 検索の一部として返される属性を文字列配列でセットします。
    312         *
    313         * null は属性が何も返されないことを示します。
    314         * このメソッドからは、空の配列をセットすることは出来ません。
    315         *
    316         * @param       atr     返される属性を識別する属性 ID の配列
    317         */
    318        public void setAttributes( final String[] atr ) {
    319                if( atr != null ) {
    320                        attrs = new String[atr.length];
    321                        System.arraycopy( atr,0,attrs,0,atr.length );
    322                }
    323        }
    324
    325        /**
    326         * 検索の一部として返される属性を文字列配列で取得します。
    327         *
    328         * setAttributes で、設定した文字列配列が返されます。
    329         * 属性配列に、 null をセットした場合、全属性が返されます。
    330         *
    331         * @return      返される属性を識別する属性 ID の配列
    332         */
    333        public  String[] getAttributes() {
    334//              return attrs.clone() ;
    335                return (attrs == null) ? new String[0] : attrs.clone() ;
    336        }
    337
    338        /**
    339         * 初期コンテキストファクトリを指定します(初期値:システムパラメータ の INITIAL_CONTEXT_FACTORY)。
    340         *
    341         * 初期値は、システムパラメータ の INITIAL_CONTEXT_FACTORY 属性です。
    342         * 例)com.sun.jndi.ldap.LdapCtxFactory
    343         *
    344         * @param       ctx INITIAL_CONTEXT_FACTORY属性
    345         */
    346        public void setInitctx( final String ctx ) {
    347                initctx = StringUtil.nval( ctx, initctx );
    348        }
    349
    350        /**
    351         * サービスプロバイダの構成情報を指定します(初期値:システムパラメータ の LDAP_PROVIDER_URL)。
    352         *
    353         * プロトコルとサーバーとポートを指定します。
    354         * 例)『ldap://ldap.opengion.org:389』
    355         *
    356         * @param       url PROVIDER_URL属性
    357         */
    358        public void setProviderURL( final String url ) {
    359                providerURL = StringUtil.nval( url, providerURL );
    360        }
    361
    362        /**
    363         * 検索するコンテキストまたはオブジェクトの名前を設定します(初期値:システムパラメータ の LDAP_SEARCH_BASE)。
    364         *
    365         * 例)『soOUID=employeeuser,o=opengion,c=JP』
    366         *
    367         * @param       base SEARCHBASE属性
    368         */
    369        public void setSearchbase( final String base ) {
    370                searchbase = StringUtil.nval( base, searchbase );
    371        }
    372
    373        /**
    374         * 属性の取得元のオブジェクトの名前を設定します(初期値:システムパラメータ の LDAP_ENTRYDN)。
    375         *
    376         * 例)『cn=inquiry-sys,o=opengion,c=JP』
    377         *
    378         * @param       dn 取得元のオブジェクトの名前
    379         */
    380        public void setEntrydn( final String dn ) {
    381                entrydn = StringUtil.nval( dn, entrydn );
    382        }
    383
    384        /**
    385         * 属性の取得元のオブジェクトのパスワードを設定します(初期値:システムパラメータ の LDAP_PASSWORD)。
    386         *
    387         * @og.rev 4.2.2.0 (2008/05/10) LDAP パスワード取得対応
    388         *
    389         * @param       pwd 取得元のオブジェクトのパスワード
    390         */
    391        public void setPassword( final String pwd ) {
    392                password = StringUtil.nval( pwd, password );
    393        }
    394
    395        /**
    396         * 検索した結果を表示する表示順をファイル属性名で指定します。
    397         *
    398         * attributes 属性で指定するキー、または、LDAPから返されたキーについて
    399         * その属性でソートします。逆順を行う場合は、DESC を指定のカラム名の後ろに
    400         * 付けて下さい。
    401         *
    402         * @param       ordr    ソートキーを指定。
    403         */
    404        public void setOrderBy( final String ordr ) {
    405                orderBy = StringUtil.csv2Array( ordr );
    406
    407                desc = new boolean[orderBy.length];
    408                for( int i=0; i<orderBy.length; i++ ) {
    409                        String key = orderBy[i].trim();
    410                        int ad = key.indexOf( " DESC" ) ;
    411                        if( ad > 0 ) {
    412                                desc[i] = true;
    413                                key = key.substring( 0,ad );
    414                        }
    415                        else {
    416                                desc[i] = false;
    417                        }
    418                        orderBy[i] = key ;
    419                }
    420        }
    421
    422        /**
    423         * リストオブジェクトをヘッダーキーに対応させてソートします。
    424         *
    425         * @og.rev 4.2.2.0 (2008/05/10) ソート条件を増やします。
    426         *
    427         * @param       in              ソートするリストオブジェクト
    428         * @param       headers ソートするキーになる文字列配列
    429         *
    430         * @return      ソート結果のリストオブジェクト
    431         */
    432        private List<String[]> sort( final List<String[]> in,final String[] headers ) {
    433                // 4.2.2.0 (2008/05/10) ソート条件を増やします。
    434                if( orderBy == null || orderBy.length == 0 ||
    435                        headers == null || headers.length == 0 ||
    436//                      in.size() == 0                                                          ) { return in; }
    437                        in.isEmpty()                                                            ) { return in; }
    438
    439                int[] no = new int[orderBy.length];
    440                for( int i=0; i<orderBy.length; i++ ) {
    441                        String key = orderBy[i] ;
    442                        no[i] = -1;     // 未存在時のマーカー
    443                        for( int j=0; j<headers.length; j++ ) {
    444                                if( key.equalsIgnoreCase( headers[j] ) ) {
    445                                        no[i] = j ;     break;
    446                                }
    447                        }
    448                        if( no[i] < 0 ) {
    449                                String errMsg = "指定の Order BY キーは、ヘッダー列に存在しません。"
    450                                                        + "order Key=[" + key + "] , attri=["
    451                                                        + StringUtil.array2csv( headers ) + "]" + HybsSystem.CR ;
    452                                throw new HybsSystemException( errMsg );
    453                        }
    454                }
    455
    456                String[][] data = in.toArray( new String[in.size()][(in.get(0)).length] );
    457                Arrays.sort( data, new IdComparator( no,desc ) );
    458                List<String[]> rtn = new ArrayList<String[]>();
    459                for( int i=0; i<data.length; i++ ) {
    460                        rtn.add( data[i] );
    461                }
    462                return rtn ;
    463        }
    464
    465        /**
    466         * LDAPの検索結果を並び替える為の Comparator実装内部クラスです。
    467         *
    468         * @og.group その他入力
    469         *
    470         * @version  4.0
    471         * @author       Kazuhiko Hasegawa
    472         * @since    JDK5.0,
    473         */
    474        private static class IdComparator implements Comparator<String[]>,Serializable {
    475                private static final long serialVersionUID = 4000 ;     // 4.0.0 (2005/01/31)
    476
    477                private final int[]             no ;
    478                private final boolean[] desc ;
    479                private final int               cnt ;
    480
    481                /**
    482                 * コンストラクター
    483                 *
    484                 * @param       no      int[]  ソートするリストオブジェクト
    485                 * @param       desc    boolean[]       ソートするキーになる文字列配列
    486                 */
    487                public IdComparator( final int[] no , final boolean[] desc ) {
    488                        this.no         = no;
    489                        this.desc       = desc;
    490                        cnt                     = no.length;
    491                }
    492
    493                /**
    494                 * Comparator インターフェースのcompareメソッド
    495                 *
    496                 * 順序付けのために 2 つの引数を比較します。
    497                 * 最初の引数が 2 番目の引数より小さい場合は負の整数、
    498                 * 両方が等しい場合は 0、最初の引数が 2 番目の引数より
    499                 * 大きい場合は正の整数を返します。
    500                 *
    501                 * @og.rev 5.5.2.6 (2012/05/25) findbugs対応。トリッキーな値の置き換えをやめます。
    502                 *
    503                 * @param       s1      比較対象の最初のオブジェクト
    504                 * @param       s2      比較対象の 2 番目のオブジェクト
    505                 * @return      最初の引数が 2 番目の引数より小さい場合は負の整数、両方が等しい場合は 0、最初の引数が 2 番目の引数より大きい場合は正の整数
    506                 */
    507                public int compare( final String[] s1,final String[] s2 ) {
    508                        if( s1 == null ) { return -1; }
    509
    510                        for( int i=0; i<cnt; i++ ) {
    511                                if( s1[no[i]] == null ) { return -1; }
    512                                if( s2[no[i]] == null ) { return 1; }   // 5.5.2.6 (2012/05/25) 比較を途中で止めないために、nullチェックしておく。
    513                                // 5.5.2.6 (2012/05/25) findbugs対応
    514//                              int rtn = s1[no[i]].compareTo( s2[no[i]] ) ;
    515//                              if( desc[i] ) { rtn = -rtn; }
    516                                int rtn = (desc[i]) ? s2[no[i]].compareTo( s1[no[i]] ) : s1[no[i]].compareTo( s2[no[i]] ) ;
    517                                if( rtn != 0 ) { return rtn ;}
    518                        }
    519                        return 0;
    520                }
    521
    522        //      public boolean equals(Object obj) {
    523        //              return ( this == obj );
    524        //      }
    525        }
    526
    527        /**
    528         * このオブジェクトの文字列表現を返します。
    529         * 基本的にデバッグ目的に使用します。
    530         *
    531         * @return このクラスの文字列表現
    532         */
    533        @Override
    534        public String toString() {
    535                StringBuilder buf = new StringBuilder( HybsSystem.BUFFER_MIDDLE );
    536                buf.append( "  initctx      [" ).append( initctx      ).append( "]" ).append( HybsSystem.CR );
    537                buf.append( "  providerURL  [" ).append( providerURL  ).append( "]" ).append( HybsSystem.CR );
    538                buf.append( "  entrydn      [" ).append( entrydn      ).append( "]" ).append( HybsSystem.CR );
    539                buf.append( "  searchbase   [" ).append( searchbase   ).append( "]" ).append( HybsSystem.CR );
    540                buf.append( "  searchScope  [" ).append( searchScope  ).append( "]" ).append( HybsSystem.CR );
    541                buf.append( "  executeCount [" ).append( executeCount ).append( "]" ).append( HybsSystem.CR );
    542                buf.append( "  attributes   [" ).append( StringUtil.array2line( attrs,"," ) );
    543                buf.append( "]" ).append( HybsSystem.CR );
    544
    545                return buf.toString();
    546        }
    547}