    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() ) { getDataChange( vals.next(),buf ) ;}      // 4.2.2.0 (2008/05/10)
    165                                                while ( vals.hasMore() ) {
    166                                                        buf.append( "," ) ;
    167                                                        getDataChange( vals.next(),buf ) ;      // 4.2.2.0 (2008/05/10)
    168                                                }
    169                                                values[i] = buf.toString();
    170                                                flag = true;
    171                                        }
    172                                }
    173                                if( flag ) {
    174                                        list.add( values );
    175                                        executeCount++ ;
    176                                }
    177                        }
    178                        if( results != null ) { results.close(); }
    179                } catch ( NamingException ex ) {
    180                        String errMsg = "List オブジェクトの検索に失敗しました。"
    181                                                + HybsSystem.CR
    182                                                + "searchbase や、entrydn の記述をご確認ください。"
    183                                                + HybsSystem.CR
    184                                                + "searchbase:" + searchbase
    185                                                + " , entrydn:" + entrydn ;
    186                        throw new HybsSystemException( errMsg,ex );             // 3.5.5.4 (2004/04/15) 引数の並び順変更
    187                }
    188                return sort( list,attrs ) ;
    189        }
    190
    191        /**
    192         * LDAPから取得したデータの変換を行います。
    193         *
    194         * 主に、バイト配列(byte[]) オブジェクトの場合、文字列に戻します。
    195         *
    196         * @og.rev 4.2.2.0 (2008/05/10) 新規追加
    197         *
    198         * @param       obj     主にバイト配列データ
    199         * @param       buf     元のStringBuilder
    200         *
    201         * @return      データを追加したStringBuilder
    202         */
    203        private StringBuilder getDataChange( final Object obj, final StringBuilder buf ) {
    204                if( obj == null ) { return buf; }
    205                else if( obj instanceof byte[] ) {
    206                        byte[] bb = (byte[])obj ;
    207                        char[] chs = new char[bb.length];
    208                        for( int i=0; i<bb.length; i++ ) {
    209                                chs[i] = (char)bb[i];
    210                        }
    211                        buf.append( chs );
    212                }
    213                else {
    214                        buf.append( obj ) ;
    215                }
    216
    217                return buf ;
    218        }
    219
    220        /**
    221         * 検索範囲(OBJECT/ONELEVEL/SUBTREE)を設定します(初期値:LDAP_SEARCH_SCOPE)。
    222         *
    223         * 検索範囲を OBJECT_SCOPE、ONELEVEL_SCOPE、SUBTREE_SCOPE のどれか 1 つです。
    224         * 指定文字列は、それぞれ『OBJECT』『ONELEVEL』『SUBTREE』です。
    225         *
    226         * @param       scope   SearchControlsの検索範囲
    227         */
    228        public void setSearchScope( final String scope ) {
    229                searchScope = StringUtil.nval( scope, searchScope );
    230                if( ! "OBJECT".equals( searchScope ) &&
    231                        ! "ONELEVEL".equals( searchScope ) &&
    232                        ! "SUBTREE".equals( searchScope ) ) {
    233                                String errMsg = "検索範囲は、『OBJECT』『ONELEVEL』『SUBTREE』の中から選択して下さい。"
    234                                                                + "[" + searchScope + "]" ;
    235                                throw new HybsSystemException( errMsg );
    236                }
    237        }
    238
    239        /**
    240         * 引数の searchScope 文字列(『OBJECT』『ONELEVEL』『SUBTREE』のどれか)を、
    241         * SearchControls クラス定数である、OBJECT_SCOPE、ONELEVEL_SCOPE、SUBTREE_SCOPE のどれか
    242         *  1 つに設定します。
    243         *
    244         * @param       scope   searchScope文字列
    245         *
    246         * @return      SearchControls定数
    247         */
    248        private int changeScopeString( final String scope ) {
    249                final int rtnScope;
    250                if( "OBJECT".equals( scope ) )        { rtnScope = SearchControls.OBJECT_SCOPE ; }
    251                else if( "ONELEVEL".equals( scope ) ) { rtnScope = SearchControls.ONELEVEL_SCOPE ; }
    252                else if( "SUBTREE".equals( scope ) )  { rtnScope = SearchControls.SUBTREE_SCOPE ; }
    253                else {
    254                        String errMsg = "Search Scope in 『OBJECT』『ONELEVEL』『SUBTREE』Selected"
    255                                                        + "[" + searchScope + "]" ;
    256                        throw new HybsSystemException( errMsg );
    257                }
    258                return rtnScope ;
    259        }
    260
    261        /**
    262         * これらの SearchControls の時間制限をミリ秒単位で設定します(初期値:0[無制限])。
    263         *
    264         * 値が 0 の場合、無制限に待つことを意味します。
    265         *
    266         * @param       limit   ミリ秒単位の時間制限(初期値:無制限)
    267         */
    268        public void setTimeLimit( final int limit ) {
    269                timeLimit = limit;
    270        }
    271
    272        /**
    273         * 検索中のリンクへの間接参照を有効または無効[true/false]にします(初期値:false)。
    274         *
    275         * 検索中のリンクへの間接参照を有効または無効にします。
    276         *
    277         * @param       deref   リンクを逆参照する場合は true、そうでない場合は false(初期値:false)
    278         */
    279        public void setDerefLinkFlag( final boolean deref ) {
    280                derefLinkFlag = deref;
    281        }
    282
    283        /**
    284         * 結果の一部としてオブジェクトを返すことを有効または無効[true/false]にします(初期値:false)。
    285         *
    286         * 無効にした場合、オブジェクトの名前およびクラスだけが返されます。
    287         * 有効にした場合、オブジェクトが返されます。
    288         *
    289         * @param       pbjflag オブジェクトが返される場合は true、そうでない場合は false(初期値:false)
    290         */
    291        public void setReturningObjFlag( final boolean pbjflag ) {
    292                returningObjFlag = pbjflag;
    293        }
    294
    295        /**
    296         * レジストリの最大検索件数をセットします(初期値:0[無制限])。
    297         *
    298         * DBTableModelのデータとして登録する最大件数をこの値に設定します。
    299         * サーバーのメモリ資源と応答時間の確保の為です。
    300         * 0 は、無制限です。(初期値は、無制限です。)
    301         *
    302         * @param       count   レジストリの最大検索件数
    303         */
    304        public void setMaxRowCount( final int count ) {
    305                maxRowCount = count;
    306        }
    307
    308        /**
    309         * 検索の一部として返される属性を文字列配列でセットします。
    310         *
    311         * null は属性が何も返されないことを示します。
    312         * このメソッドからは、空の配列をセットすることは出来ません。
    313         *
    314         * @param       atr     返される属性を識別する属性 ID の配列
    315         */
    316        public void setAttributes( final String[] atr ) {
    317                if( atr != null ) {
    318                        attrs = new String[atr.length];
    319                        System.arraycopy( atr,0,attrs,0,atr.length );
    320                }
    321        }
    322
    323        /**
    324         * 検索の一部として返される属性を文字列配列で取得します。
    325         *
    326         * setAttributes で、設定した文字列配列が返されます。
    327         * 属性配列に、 null をセットした場合、全属性が返されます。
    328         *
    329         * @return      返される属性を識別する属性 ID の配列
    330         */
    331        public  String[] getAttributes() {
    332                return (attrs == null) ? new String[0] : attrs.clone() ;
    333        }
    334
    335        /**
    336         * 初期コンテキストファクトリを指定します(初期値:システムパラメータ の INITIAL_CONTEXT_FACTORY)。
    337         *
    338         * 初期値は、システムパラメータ の INITIAL_CONTEXT_FACTORY 属性です。
    339         * 例)com.sun.jndi.ldap.LdapCtxFactory
    340         *
    341         * @param       ctx INITIAL_CONTEXT_FACTORY属性
    342         */
    343        public void setInitctx( final String ctx ) {
    344                initctx = StringUtil.nval( ctx, initctx );
    345        }
    346
    347        /**
    348         * サービスプロバイダの構成情報を指定します(初期値:システムパラメータ の LDAP_PROVIDER_URL)。
    349         *
    350         * プロトコルとサーバーとポートを指定します。
    351         * 例)『ldap://ldap.opengion.org:389』
    352         *
    353         * @param       url PROVIDER_URL属性
    354         */
    355        public void setProviderURL( final String url ) {
    356                providerURL = StringUtil.nval( url, providerURL );
    357        }
    358
    359        /**
    360         * 検索するコンテキストまたはオブジェクトの名前を設定します(初期値:システムパラメータ の LDAP_SEARCH_BASE)。
    361         *
    362         * 例)『soOUID=employeeuser,o=opengion,c=JP』
    363         *
    364         * @param       base SEARCHBASE属性
    365         */
    366        public void setSearchbase( final String base ) {
    367                searchbase = StringUtil.nval( base, searchbase );
    368        }
    369
    370        /**
    371         * 属性の取得元のオブジェクトの名前を設定します(初期値:システムパラメータ の LDAP_ENTRYDN)。
    372         *
    373         * 例)『cn=inquiry-sys,o=opengion,c=JP』
    374         *
    375         * @param       dn 取得元のオブジェクトの名前
    376         */
    377        public void setEntrydn( final String dn ) {
    378                entrydn = StringUtil.nval( dn, entrydn );
    379        }
    380
    381        /**
    382         * 属性の取得元のオブジェクトのパスワードを設定します(初期値:システムパラメータ の LDAP_PASSWORD)。
    383         *
    384         * @og.rev 4.2.2.0 (2008/05/10) LDAP パスワード取得対応
    385         *
    386         * @param       pwd 取得元のオブジェクトのパスワード
    387         */
    388        public void setPassword( final String pwd ) {
    389                password = StringUtil.nval( pwd, password );
    390        }
    391
    392        /**
    393         * 検索した結果を表示する表示順をファイル属性名で指定します。
    394         *
    395         * attributes 属性で指定するキー、または、LDAPから返されたキーについて
    396         * その属性でソートします。逆順を行う場合は、DESC を指定のカラム名の後ろに
    397         * 付けて下さい。
    398         *
    399         * @param       ordr    ソートキーを指定。
    400         */
    401        public void setOrderBy( final String ordr ) {
    402                orderBy = StringUtil.csv2Array( ordr );
    403
    404                desc = new boolean[orderBy.length];
    405                for( int i=0; i<orderBy.length; i++ ) {
    406                        String key = orderBy[i].trim();
    407                        int ad = key.indexOf( " DESC" ) ;
    408                        if( ad > 0 ) {
    409                                desc[i] = true;
    410                                key = key.substring( 0,ad );
    411                        }
    412                        else {
    413                                desc[i] = false;
    414                        }
    415                        orderBy[i] = key ;
    416                }
    417        }
    418
    419        /**
    420         * リストオブジェクトをヘッダーキーに対応させてソートします。
    421         *
    422         * @og.rev 4.2.2.0 (2008/05/10) ソート条件を増やします。
    423         *
    424         * @param       in              ソートするリストオブジェクト
    425         * @param       headers ソートするキーになる文字列配列
    426         *
    427         * @return      ソート結果のリストオブジェクト
    428         */
    429        private List<String[]> sort( final List<String[]> in,final String[] headers ) {
    430                // 4.2.2.0 (2008/05/10) ソート条件を増やします。
    431                if( orderBy == null || orderBy.length == 0 ||
    432                        headers == null || headers.length == 0 ||
    433                        in.isEmpty()                                                            ) { return in; }
    434
    435                int[] no = new int[orderBy.length];
    436                for( int i=0; i<orderBy.length; i++ ) {
    437                        String key = orderBy[i] ;
    438                        no[i] = -1;     // 未存在時のマーカー
    439                        for( int j=0; j<headers.length; j++ ) {
    440                                if( key.equalsIgnoreCase( headers[j] ) ) {
    441                                        no[i] = j ;     break;
    442                                }
    443                        }
    444                        if( no[i] < 0 ) {
    445                                String errMsg = "指定の Order BY キーは、ヘッダー列に存在しません。"
    446                                                        + "order Key=[" + key + "] , attri=["
    447                                                        + StringUtil.array2csv( headers ) + "]" + HybsSystem.CR ;
    448                                throw new HybsSystemException( errMsg );
    449                        }
    450                }
    451
    452                String[][] data = in.toArray( new String[in.size()][(in.get(0)).length] );
    453                Arrays.sort( data, new IdComparator( no,desc ) );
    454                List<String[]> rtn = new ArrayList<String[]>();
    455                for( int i=0; i<data.length; i++ ) {
    456                        rtn.add( data[i] );
    457                }
    458                return rtn ;
    459        }
    460
    461        /**
    462         * LDAPの検索結果を並び替える為の Comparator実装内部クラスです。
    463         *
    464         * @og.group その他入力
    465         *
    466         * @version  4.0
    467         * @author       Kazuhiko Hasegawa
    468         * @since    JDK5.0,
    469         */
    470        private static class IdComparator implements Comparator<String[]>,Serializable {
    471                private static final long serialVersionUID = 400020050131L ;    // 4.0.0.0 (2005/01/31)
    472
    473                private final int[]             no ;
    474                private final boolean[] desc ;
    475                private final int               cnt ;
    476
    477                /**
    478                 * コンストラクター
    479                 *
    480                 * @param       no      int[]  ソートするリストオブジェクト
    481                 * @param       desc    boolean[]       ソートするキーになる文字列配列
    482                 */
    483                public IdComparator( final int[] no , final boolean[] desc ) {
    484                        this.no         = no;
    485                        this.desc       = desc;
    486                        cnt                     = no.length;
    487                }
    488
    489                /**
    490                 * Comparator インターフェースのcompareメソッド
    491                 *
    492                 * 順序付けのために 2 つの引数を比較します。
    493                 * 最初の引数が 2 番目の引数より小さい場合は負の整数、
    494                 * 両方が等しい場合は 0、最初の引数が 2 番目の引数より
    495                 * 大きい場合は正の整数を返します。
    496                 *
    497                 * @og.rev 5.5.2.6 (2012/05/25) findbugs対応。トリッキーな値の置き換えをやめます。
    498                 *
    499                 * @param       s1      比較対象の最初のオブジェクト
    500                 * @param       s2      比較対象の 2 番目のオブジェクト
    501                 * @return      最初の引数が 2 番目の引数より小さい場合は負の整数、両方が等しい場合は 0、最初の引数が 2 番目の引数より大きい場合は正の整数
    502                 */
    503                public int compare( final String[] s1,final String[] s2 ) {
    504                        if( s1 == null ) { return -1; }
    505
    506                        for( int i=0; i<cnt; i++ ) {
    507                                if( s1[no[i]] == null ) { return -1; }
    508                                if( s2[no[i]] == null ) { return 1; }   // 5.5.2.6 (2012/05/25) 比較を途中で止めないために、nullチェックしておく。
    509                                // 5.5.2.6 (2012/05/25) findbugs対応
    510                                int rtn = desc[i] ? s2[no[i]].compareTo( s1[no[i]] ) : s1[no[i]].compareTo( s2[no[i]] ) ;
    511                                if( rtn != 0 ) { return rtn ;}
    512                        }
    513                        return 0;
    514                }
    515
    516        //      public boolean equals(Object obj) {
    517        //              return ( this == obj );
    518        //      }
    519        }
    520
    521        /**
    522         * このオブジェクトの文字列表現を返します。
    523         * 基本的にデバッグ目的に使用します。
    524         *
    525         * @return このクラスの文字列表現
    526         */
    527        @Override
    528        public String toString() {
    529                StringBuilder buf = new StringBuilder( HybsSystem.BUFFER_MIDDLE );
    530                buf.append( "  initctx      [" ).append( initctx      ).append( "]" ).append( HybsSystem.CR );
    531                buf.append( "  providerURL  [" ).append( providerURL  ).append( "]" ).append( HybsSystem.CR );
    532                buf.append( "  entrydn      [" ).append( entrydn      ).append( "]" ).append( HybsSystem.CR );
    533                buf.append( "  searchbase   [" ).append( searchbase   ).append( "]" ).append( HybsSystem.CR );
    534                buf.append( "  searchScope  [" ).append( searchScope  ).append( "]" ).append( HybsSystem.CR );
    535                buf.append( "  executeCount [" ).append( executeCount ).append( "]" ).append( HybsSystem.CR );
    536                buf.append( "  attributes   [" ).append( StringUtil.array2line( attrs,"," ) );
    537                buf.append( "]" ).append( HybsSystem.CR );
    538
    539                return buf.toString();
    540        }
    541}