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.fukurou.process;
017
018import org.opengion.fukurou.util.Argument;
019
020import org.opengion.fukurou.util.StringUtil;
021import org.opengion.fukurou.util.HybsEntry ;
022import org.opengion.fukurou.util.LogWriter;
023
024import java.util.Hashtable;
025import java.util.List;
026import java.util.ArrayList;
027import java.util.Map ;
028import java.util.LinkedHashMap ;
029
030import javax.naming.Context;
031import javax.naming.NamingEnumeration;
032import javax.naming.NamingException;
033import javax.naming.directory.DirContext;
034import javax.naming.directory.InitialDirContext;
035import javax.naming.directory.SearchControls;
036import javax.naming.directory.SearchResult;
037import javax.naming.directory.Attribute;
038import javax.naming.directory.Attributes;
039
040/**
041 * Process_LDAPReaderは、LDAPから読み取った内容を、LineModel に設定後、
042 * 下流に渡す、FirstProcess インターフェースの実装クラスです。
043 *
044 * LDAPから読み取った内容より、LineModelを作成し、下流(プロセスチェインは、
045 * チェインしているため、データは上流から下流へと渡されます。)に渡します。
046 *
047 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
048 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に
049 * 繋げてください。
050 *
051 * @og.formSample
052 *  Process_LDAPReader -attrs=uid,cn,officeName,ou,mail,belongOUID -orderBy=uid -filter=(&(objectClass=person)(|(belongOUID=61200)(belongOUID=61100)))
053 *
054 *   [ -initctx=コンテキストファクトリ   ] :初期コンテキストファクトリ (初期値:com.sun.jndi.ldap.LdapCtxFactory)
055 *   [ -providerURL=サービスプロバイダリ ] :サービスプロバイダリ       (初期値:ldap://ldap.opengion.org:389)
056 *   [ -entrydn=取得元の名前             ] :属性の取得元のオブジェクトの名前 (初期値:cn=inquiry-sys,o=opengion,c=JP)
057 *   [ -password=取得元のパスワード      ] :属性の取得元のパスワード   (初期値:******)
058 *   [ -searchbase=コンテキストベース名  ] :検索するコンテキストのベース名 (初期値:soouid=employeeuser,o=opengion,c=JP)
059 *   [ -searchScope=検索範囲             ] :検索範囲。『OBJECT』『ONELEVEL』『SUBTREE』のどれか(初期値:SUBTREE)
060 *   [ -timeLimit=検索制限時間           ] :結果が返されるまでのミリ秒数。0 の場合無制限(初期値:0)
061 *   [ -attrs=属性の識別子               ] :エントリと一緒に返される属性の識別子。null の場合すべての属性
062 *   [ -columns=属性のカラム名           ] :属性の識別子に対する別名。識別子と同じ場合は『,』のみで区切る。
063 *   [ -maxRowCount=最大検索数           ] :最大検索数(初期値:0[無制限])
064 *   [ -match_XXXX=正規表現              ] :指定のカラムと正規表現で一致時のみ処理( -match_LANG=ABC=[a-zA-Z]* など。)
065 *   [ -filter=検索条件                  ] :検索する LDAP に指定する条件
066 *   [ -referral=REFERAL                 ] :ignore/follow/throw
067 *   [ -display=[false/true]             ] :結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
068 *   [ -debug=[false/true]               ] :デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
069 *
070 * @version  4.0
071 * @author   Kazuhiko Hasegawa
072 * @since    JDK5.0,
073 */
074public class Process_LDAPReader extends AbstractProcess implements FirstProcess {
075        private static final String             INITCTX                 = "com.sun.jndi.ldap.LdapCtxFactory";
076        private static final String             PROVIDER                = "ldap://ldap.opengion.org:389";
077        private static final String             PASSWORD                = "password";
078        private static final String             SEARCH_BASE             = "soouid=employeeuser,o=opengion,c=JP";
079        private static final String             ENTRYDN                 = "cn=inquiry-sys,o=opengion,c=JP";     // 4.2.2.0 (2008/05/10)
080        private static final String             REFERRAL                = ""; // 5.6.7.0 (2013/07/27)
081
082        // 検索範囲。OBJECT_SCOPE、ONELEVEL_SCOPE、SUBTREE_SCOPE のどれか 1 つ
083        private static final String[]   SCOPE_LIST              = new String[] { "OBJECT","ONELEVEL","SUBTREE" };
084        private static final String             SEARCH_SCOPE    = "SUBTREE";
085
086        private static final long       COUNT_LIMIT             = 0;                    // 返すエントリの最大数。0 の場合、フィルタを満たすエントリをすべて返す
087
088        private String                  filter                          = null;         // "employeeNumber=87019";
089        private int                             timeLimit                       = 0;                    // 結果が返されるまでのミリ秒数。0 の場合、無制限
090        private String[]                attrs                           = null;                 // エントリと一緒に返される属性の識別子。null の場合、すべての属性を返す。空の場合、属性を返さない
091        private String[]                columns                         = null;                 // 属性の識別子に対する、別名。識別子と同じ場合は、『,』のみで区切る。
092        private static final boolean    RETURN_OBJ_FLAG         = false;                // true の場合、エントリの名前にバインドされたオブジェクトを返す。false 場合、オブジェクトを返さない
093        private static final boolean    DEREF_LINK_FLAG         = false;                // true の場合、検索中にリンクを間接参照する
094
095        private int                             executeCount            = 0;                    // 検索/実行件数
096        private int                     maxRowCount                     = 0;                    // 最大検索数(0は無制限)
097
098        // 3.8.0.9 (2005/10/17) 正規表現マッチ
099        private String[]                matchKey                        = null;                 // 正規表現
100        private boolean                 display                         = false;                // 表示しない
101        private boolean                 debug                           = false;        // 5.7.3.0 (2014/02/07) デバッグ情報
102
103        private static final Map<String,String> mustProparty   ;          // [プロパティ]必須チェック用 Map
104        private static final Map<String,String> usableProparty ;          // [プロパティ]整合性チェック Map
105
106        private NamingEnumeration<SearchResult> nameEnum  = null;         // 4.3.3.6 (2008/11/15) Generics警告対応
107        private LineModel                                               newData         = null;
108        private int                                                             count           = 0;
109
110        static {
111                mustProparty = new LinkedHashMap<String,String>();
112                mustProparty.put( "filter",     "検索条件(必須) 例: (&(objectClass=person)(|(belongOUID=61200)(belongOUID=61100)))" );
113
114                usableProparty = new LinkedHashMap<String,String>();
115                usableProparty.put( "initctx",          "初期コンテキストファクトリ。" + 
116                                                                                        CR + " (初期値:com.sun.jndi.ldap.LdapCtxFactory)" );
117                usableProparty.put( "providerURL",      "サービスプロバイダリ (初期値:ldap://ldap.opengion.org:389)" );
118                usableProparty.put( "entrydn",          "属性の取得元のオブジェクトの名前。" + 
119                                                                                        CR + " (初期値:cn=inquiry-sys,o=opengion,c=JP)" );
120                usableProparty.put( "password",         "属性の取得元のパスワード(初期値:******)" );
121                usableProparty.put( "searchbase",       "検索するコンテキストのベース名。" + 
122                                                                                        CR + " (初期値:soouid=employeeuser,o=opengion,c=JP)" );
123                usableProparty.put( "searchScope",      "検索範囲。『OBJECT』『ONELEVEL』『SUBTREE』のどれか。" + 
124                                                                                        CR + " (初期値:SUBTREE)" );
125                usableProparty.put( "timeLimit",        "結果が返されるまでのミリ秒数。0 の場合無制限(初期値:0)" );
126                usableProparty.put( "attrs",            "エントリと一緒に返される属性の識別子。null の場合すべての属性" );
127                usableProparty.put( "columns",          "属性の識別子に対する別名。識別子と同じ場合は『,』のみで区切る。" );
128                usableProparty.put( "maxRowCount",      "最大検索数(0は無制限)  (初期値:0)" );
129                usableProparty.put( "match_",           "指定のカラムと正規表現で一致時のみ処理" + 
130                                                                                        CR + " ( -match_LANG=ABC=[a-zA-Z]* など。)" );
131                usableProparty.put( "display",          "結果を標準出力に表示する(true)かしない(false)か" + 
132                                                                                        CR + "(初期値:false:表示しない)" );
133                usableProparty.put( "debug",    "デバッグ情報を標準出力に表示する(true)かしない(false)か" +
134                                                                                        CR + "(初期値:false:表示しない)" );             // 5.7.3.0 (2014/02/07) デバッグ情報
135        }
136
137        /**
138         * デフォルトコンストラクター。
139         * このクラスは、動的作成されます。デフォルトコンストラクターで、
140         * super クラスに対して、必要な初期化を行っておきます。
141         *
142         */
143        public Process_LDAPReader() {
144                super( "org.opengion.fukurou.process.Process_LDAPReader",mustProparty,usableProparty );
145        }
146
147        /**
148         * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
149         * 初期処理(ファイルオープン、DBオープン等)に使用します。
150         *
151         * @og.rev 4.2.2.0 (2008/05/10) LDAP パスワード取得対応
152         * @og.rev 5.3.4.0 (2011/04/01) StringUtil.nval ではなく、getProparty の 初期値機能を使う
153         * @og.rev 5.6.7.0 (2013/07/27) REFERRAL対応
154         *
155         * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
156         */
157        public void init( final ParamProcess paramProcess ) {
158                Argument arg = getArgument();
159
160                String  initctx         = arg.getProparty("initctx "    ,INITCTX         );
161                String  providerURL = arg.getProparty("providerURL"     ,PROVIDER        );
162                String  entrydn         = arg.getProparty("entrydn"             ,ENTRYDN         );     // 4.2.2.0 (2008/05/10)
163                String  password        = arg.getProparty("password"    ,PASSWORD        );
164                String  searchbase      = arg.getProparty("searchbase"  ,SEARCH_BASE );
165
166                String  searchScope = arg.getProparty("searchScope"     ,SEARCH_SCOPE , SCOPE_LIST );
167//              timeLimit       = StringUtil.nval( arg.getProparty("timeLimit")         ,timeLimit );
168//              maxRowCount     = StringUtil.nval( arg.getProparty("maxRowCount")       ,maxRowCount );
169                timeLimit       = arg.getProparty("timeLimit",timeLimit );                      // 5.3.4.0 (2011/04/01)
170                maxRowCount     = arg.getProparty("maxRowCount",maxRowCount );          // 5.3.4.0 (2011/04/01)
171                display         = arg.getProparty("display",display);
172                debug           = arg.getProparty("debug",debug);                               // 5.7.3.0 (2014/02/07) デバッグ情報
173//              if( debug ) { println( arg.toString() ); }                      // 5.7.3.0 (2014/02/07) デバッグ情報
174
175                String referral         = arg.getProparty("referral",REFERRAL);  // 5.6.7.0 (2013/07/27)
176
177                // 属性配列を取得。なければゼロ配列
178                attrs           = StringUtil.csv2Array( arg.getProparty("attrs") );
179                if( attrs.length == 0 ) { attrs = null; }
180
181                // 別名定義配列を取得。なければ属性配列をセット
182                columns         = StringUtil.csv2Array( arg.getProparty("columns") );
183                if( columns.length == 0 ) { columns = attrs; }
184
185                // 属性配列が存在し、属性定義数と別名配列数が異なればエラー
186                // 以降は、attrs == null か、属性定義数と別名配列数が同じはず。
187                if( attrs != null && attrs.length != columns.length ) {
188                        String errMsg = "attrs と columns で指定の引数の数が異なります。" +
189                                                " attrs=[" + arg.getProparty("attrs") + "] , columns=[" +
190                                                arg.getProparty("columns") + "]" ;
191                        throw new RuntimeException( errMsg );
192                }
193
194                // 3.8.0.9 (2005/10/17) 正規表現マッチ
195                HybsEntry[] entry = arg.getEntrys( "match_" );
196                int len = entry.length;
197                matchKey        = new String[columns.length];           // 正規表現
198                for( int clm=0; clm<columns.length; clm++ ) {
199                        matchKey[clm] = null;   // 判定チェック有無の初期化
200                        for( int i=0; i<len; i++ ) {
201                                if( columns[clm].equalsIgnoreCase( entry[i].getKey() ) ) {
202                                        matchKey[clm] = entry[i].getValue();
203                                }
204                        }
205                }
206
207                filter = arg.getProparty( "filter" ,filter );
208
209                Hashtable<String,String> env = new Hashtable<String,String>();
210                env.put(Context.INITIAL_CONTEXT_FACTORY, initctx);
211                env.put(Context.PROVIDER_URL, providerURL);
212                // 3.7.1.1 (2005/05/31)
213        //      if( password != null && password.length() > 0 ) {
214                        env.put(Context.SECURITY_CREDENTIALS, password);
215        //      }
216
217                // 4.2.2.0 (2008/05/10) entrydn 属性の追加
218        //      if( entrydn != null && entrydn.length() > 0 ) {
219                        env.put(Context.SECURITY_PRINCIPAL  , entrydn);
220        //      }
221                        
222                env.put( Context.REFERRAL, referral ); // 5.6.7.0 (2013/07/27)
223
224                try {
225                        DirContext ctx = new InitialDirContext(env);
226                        SearchControls constraints = new SearchControls(
227                                                                        changeScopeString( searchScope ),
228                                                                        COUNT_LIMIT                     ,
229                                                                        timeLimit                       ,
230                                                                        attrs                           ,
231                                                                        RETURN_OBJ_FLAG         ,
232                                                                        DEREF_LINK_FLAG
233                                                                                );
234
235                        nameEnum = ctx.search(searchbase, filter, constraints);
236
237                } catch ( NamingException ex ) {
238                        String errMsg = "NamingException !"
239                                        + ex.getMessage();                              // 5.1.8.0 (2010/07/01) errMsg 修正
240                        throw new RuntimeException( errMsg,ex );
241                }
242        }
243
244        /**
245         * プロセスの終了を行います。最後に一度だけ、呼び出されます。
246         * 終了処理(ファイルクローズ、DBクローズ等)に使用します。
247         *
248         * @param   isOK トータルで、OKだったかどうか[true:成功/false:失敗]
249         */
250        public void end( final boolean isOK ) {
251                try {
252                        if( nameEnum  != null ) { nameEnum.close() ;  nameEnum  = null; }
253                }
254                catch ( NamingException ex ) {
255                        String errMsg = "ディスコネクトすることが出来ません。";
256                        throw new RuntimeException( errMsg,ex );
257                }
258        }
259
260        /**
261         * このデータの処理において、次の処理が出来るかどうかを問い合わせます。
262         * この呼び出し1回毎に、次のデータを取得する準備を行います。
263         *
264         * @return      処理できる:true / 処理できない:false
265         */
266        public boolean next() {
267                try {
268                        return nameEnum != null && nameEnum.hasMore() ;
269                }
270                catch ( NamingException ex ) {
271                        String errMsg = "ネクストすることが出来ません。";
272                        throw new RuntimeException( errMsg,ex );
273                }
274        }
275
276        /**
277         * 最初に、 行データである LineModel を作成します
278         * FirstProcess は、次々と処理をチェインしていく最初の行データを
279         * 作成して、後続の ChainProcess クラスに処理データを渡します。
280         *
281         * @og.rev 4.2.2.0 (2008/05/10) LDAP パスワード取得対応
282         *
283         * @param       rowNo   処理中の行番号
284         *
285         * @return      処理変換後のLineModel
286         */
287        public LineModel makeLineModel( final int rowNo ) {
288                count++ ;
289                try {
290                        if( maxRowCount > 0 && maxRowCount <= executeCount ) { return null ; }
291                        SearchResult sRslt = nameEnum.next();           // 4.3.3.6 (2008/11/15) Generics警告対応
292                        Attributes att = sRslt.getAttributes();
293
294                        if( newData == null ) {
295                                newData = createLineModel( att );
296                                if( display ) { println( newData.nameLine() ); }
297                        }
298
299                        for( int i=0; i<attrs.length; i++ ) {
300                                Attribute attr = att.get(attrs[i]);
301                                if( attr != null ) {
302                                        NamingEnumeration<?> vals = attr.getAll();                // 4.3.3.6 (2008/11/15) Generics警告対応
303                                        StringBuilder buf = new StringBuilder();
304//                                      if( vals.hasMore() ) { buf.append( vals.next() ) ;}
305                                        if( vals.hasMore() ) { getDataChange( vals.next(),buf ) ;}      // 4.2.2.0 (2008/05/10)
306                                        while ( vals.hasMore() ) {
307                                                buf.append( "," ) ;
308//                                              buf.append( vals.next() ) ;
309                                                getDataChange( vals.next(),buf ) ;      // 4.2.2.0 (2008/05/10)
310                                        }
311                                        // 3.8.0.9 (2005/10/17) 正規表現マッチしなければ、スルーする。
312                                        String value = buf.toString();
313                                        String key = matchKey[i];
314                                        if( key != null && value != null && !value.matches( key ) ) {
315                                                return null;
316                                        }
317                                        newData.setValue( i, value );
318                                        executeCount++ ;
319                                }
320                        }
321
322                        newData.setRowNo( rowNo );
323                        if( display ) { println( newData.dataLine() ); }
324                }
325                catch ( NamingException ex ) {
326                        String errMsg = "データを処理できませんでした。[" + rowNo + "]件目";
327                        if( newData != null ) { errMsg += newData.toString() ; }
328                        throw new RuntimeException( errMsg,ex );
329                }
330                return newData;
331        }
332
333        /**
334         * LDAPから取得したデータの変換を行います。
335         *
336         * 主に、バイト配列(byte[]) オブジェクトの場合、文字列に戻します。
337         *
338         * @og.rev 4.2.2.0 (2008/05/10) 新規追加
339         *
340         * @param       obj     主にバイト配列オブジェクト
341         * @param       buf     元のStringBuilder
342         *
343         * @return      データを追加した StringBuilder
344         */
345        private StringBuilder getDataChange( final Object obj, final StringBuilder buf ) {
346                if( obj == null ) { return buf; }
347                else if( obj instanceof byte[] ) {
348        //              buf.append( new String( (byte[])obj,"ISO-8859-1" ) );
349                        byte[] bb = (byte[])obj ;
350                        char[] chs = new char[bb.length];
351                        for( int i=0; i<bb.length; i++ ) {
352                                chs[i] = (char)bb[i];
353                        }
354                        buf.append( chs );
355                }
356                else {
357                        buf.append( obj ) ;
358                }
359
360                return buf ;
361        }
362
363        /**
364         * 内部で使用する LineModel を作成します。
365         * このクラスは、プロセスチェインの基点となりますので、新規 LineModel を返します。
366         * Exception 以外では、必ず LineModel オブジェクトを返します。
367         *
368         * @param   att Attributesオブジェクト
369         *
370         * @return      データベースから取り出して変換した LineModel
371         * @throws RuntimeException カラム名を取得できなかった場合。
372         */
373        private LineModel createLineModel( final Attributes att ) {
374                LineModel model = new LineModel();
375
376                try {
377                        // init() でチェック済み。attrs == null か、属性定義数と別名配列数が同じはず。
378                        // attrs が null の場合は、全キー情報を取得します。
379                        if( attrs == null ) {
380                                NamingEnumeration<String> nmEnum = att.getIDs();  // 4.3.3.6 (2008/11/15) Generics警告対応
381                                List<String> lst = new ArrayList<String>();
382                                try {
383                                        while( nmEnum.hasMore() ) {
384                                                lst.add( nmEnum.next() );               // 4.3.3.6 (2008/11/15) Generics警告対応
385                                        }
386                                }
387                                finally {
388                                        nmEnum.close();
389                                }
390                                attrs = lst.toArray( new String[lst.size()] );
391                                columns = attrs;
392                        }
393
394                        int size = columns.length;
395                        model.init( size );
396                        for(int clm = 0; clm < size; clm++) {
397                                model.setName( clm,StringUtil.nval( columns[clm],attrs[clm] ) );
398                        }
399                }
400                catch ( NamingException ex ) {
401                        String errMsg = "ResultSetMetaData から、カラム名を取得できませんでした。";
402                        throw new RuntimeException( errMsg,ex );
403                }
404                return model;
405        }
406
407        /**
408         * スコープを表す文字列を SearchControls の定数に変換します。
409         * 入力文字列は、OBJECT、ONELEVEL、SUBTREEです。変換する定数は、
410         * SearchControls クラスの static 定数です。
411         *
412         * @param    scope スコープを表す文字列(OBJECT、ONELEVEL、SUBTREE)
413         *
414         * @return   SearchControlsの定数(OBJECT_SCOPE、ONELEVEL_SCOPE、SUBTREE_SCOPE)
415         * @see      javax.naming.directory.SearchControls#OBJECT_SCOPE
416         * @see      javax.naming.directory.SearchControls#ONELEVEL_SCOPE
417         * @see      javax.naming.directory.SearchControls#SUBTREE_SCOPE
418         */
419        private int changeScopeString( final String scope ) {
420                int rtnScope ;
421                if( "OBJECT".equals( scope ) )        { rtnScope = SearchControls.OBJECT_SCOPE ; }
422                else if( "ONELEVEL".equals( scope ) ) { rtnScope = SearchControls.ONELEVEL_SCOPE ; }
423                else if( "SUBTREE".equals( scope ) )  { rtnScope = SearchControls.SUBTREE_SCOPE ; }
424                else {
425                        String errMsg = "Search Scope in 『OBJECT』『ONELEVEL』『SUBTREE』Selected"
426                                                        + "[" + scope + "]" ;
427                        throw new RuntimeException( errMsg );
428                }
429                return rtnScope ;
430        }
431
432        /**
433         * プロセスの処理結果のレポート表現を返します。
434         * 処理プログラム名、入力件数、出力件数などの情報です。
435         * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
436         * 形式で出してください。
437         *
438         * @return   処理結果のレポート
439         */
440        public String report() {
441                String report = "[" + getClass().getName() + "]" + CR
442                                + TAB + "Search Filter : " + filter + CR
443                                + TAB + "Input Count   : " + count ;
444
445                return report ;
446        }
447
448        /**
449         * このクラスの使用方法を返します。
450         *
451         * @return      このクラスの使用方法
452         */
453        public String usage() {
454                StringBuilder buf = new StringBuilder();
455
456                buf.append( "Process_LDAPReaderは、LDAPから読み取った内容を、LineModel に設定後、"                        ).append( CR );
457                buf.append( "下流に渡す、FirstProcess インターフェースの実装クラスです。"                                      ).append( CR );
458                buf.append( CR );
459                buf.append( "LDAPから読み取った内容より、LineModelを作成し、下流(プロセスチェインは、"               ).append( CR );
460                buf.append( "チェインしているため、データは上流から下流へと渡されます。)に渡します。"              ).append( CR );
461                buf.append( CR );
462                buf.append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。" ).append( CR );
463                buf.append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に"                ).append( CR );
464                buf.append( "繋げてください。"                                                                                                                          ).append( CR );
465                buf.append( CR ).append( CR );
466
467                buf.append( getArgument().usage() ).append( CR );
468
469                return buf.toString();
470        }
471
472        /**
473         * このクラスは、main メソッドから実行できません。
474         *
475         * @param       args    コマンド引数配列
476         */
477        public static void main( final String[] args ) {
478                LogWriter.log( new Process_LDAPReader().usage() );
479        }
480}