/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.process;

import org.opengion.fukurou.util.Argument;

import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.util.HybsEntry ;
import org.opengion.fukurou.util.LogWriter;

import java.util.Hashtable;
import java.util.List;
import java.util.ArrayList;
import java.util.Map ;
import java.util.LinkedHashMap ;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;

/**
 * Process_LDAPReaderは、LDAPから読み取った内容を、LineModel に設定後、
 * 下流に渡す、FirstProcess インターフェースの実装クラスです。
 *
 * LDAPから読み取った内容より、LineModelを作成し、下流(プロセスチェインは、
 * チェインしているため、データは上流から下流へと渡されます。)に渡します。
 *
 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に
 * 繋げてください。
 *
 * @og.formSample
 *  Process_LDAPReader -attrs=uid,cn,officeName,ou,mail,belongOUID -orderBy=uid -filter=(&amp;(objectClass=person)(|(belongOUID=61200)(belongOUID=61100)))
 *
 *   [ -initctx=コンテキストファクトリ   ] ：初期コンテキストファクトリ (初期値:com.sun.jndi.ldap.LdapCtxFactory)
 *   [ -providerURL=サービスプロバイダリ ] ：サービスプロバイダリ       (初期値:ldap://ldap.opengion.org:389)
 *   [ -entrydn=取得元の名前             ] ：属性の取得元のオブジェクトの名前 (初期値:cn=inquiry-sys,o=opengion,c=JP)
 *   [ -password=取得元のパスワード      ] ：属性の取得元のパスワード   (初期値:******)
 *   [ -searchbase=コンテキストベース名  ] ：検索するコンテキストのベース名 (初期値:soouid=employeeuser,o=opengion,c=JP)
 *   [ -searchScope=検索範囲             ] ：検索範囲。『OBJECT』『ONELEVEL』『SUBTREE』のどれか(初期値:SUBTREE)
 *   [ -timeLimit=検索制限時間           ] ：結果が返されるまでのミリ秒数。0 の場合無制限(初期値:0)
 *   [ -attrs=属性の識別子               ] ：エントリと一緒に返される属性の識別子。null の場合すべての属性
 *   [ -columns=属性のカラム名           ] ：属性の識別子に対する別名。識別子と同じ場合は『,』のみで区切る。
 *   [ -maxRowCount=最大検索数           ] ：最大検索数(初期値:0[無制限])
 *   [ -match_XXXX=正規表現              ] ：指定のカラムと正規表現で一致時のみ処理( -match_LANG=ABC=[a-zA-Z]* など。)
 *   [ -filter=検索条件                  ] ：検索する LDAP に指定する条件
 *   [ -referral=REFERAL                 ] ：ignore/follow/throw
 *   [ -display=[false/true]             ] ：結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *   [ -debug=[false/true]               ] ：デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class Process_LDAPReader extends AbstractProcess implements FirstProcess {
	private static final String		INITCTX 		= "com.sun.jndi.ldap.LdapCtxFactory";
	private static final String		PROVIDER	 	= "ldap://ldap.opengion.org:389";
	private static final String		PASSWORD 		= "password";
	private static final String		SEARCH_BASE		= "soouid=employeeuser,o=opengion,c=JP";
	private static final String		ENTRYDN			= "cn=inquiry-sys,o=opengion,c=JP";	// 4.2.2.0 (2008/05/10)
	private static final String		REFERRAL		= ""; // 5.6.7.0 (2013/07/27)

	// 検索範囲。OBJECT_SCOPE、ONELEVEL_SCOPE、SUBTREE_SCOPE のどれか 1 つ
	private static final String[]	SCOPE_LIST		= new String[] { "OBJECT","ONELEVEL","SUBTREE" };
	private static final String		SEARCH_SCOPE	= "SUBTREE";

	private static final long	COUNT_LIMIT		= 0;			// 返すエントリの最大数。0 の場合、フィルタを満たすエントリをすべて返す

	private String			filter 				= null;		// "employeeNumber=87019";
	private int				timeLimit			= 0;			// 結果が返されるまでのミリ秒数。0 の場合、無制限
	private String[]		attrs				= null;			// エントリと一緒に返される属性の識別子。null の場合、すべての属性を返す。空の場合、属性を返さない
	private String[]		columns				= null;			// 属性の識別子に対する、別名。識別子と同じ場合は、『,』のみで区切る。
	private static final boolean	RETURN_OBJ_FLAG		= false;		// true の場合、エントリの名前にバインドされたオブジェクトを返す。false 場合、オブジェクトを返さない
	private static final boolean	DEREF_LINK_FLAG		= false;		// true の場合、検索中にリンクを間接参照する

	private int				executeCount		= 0;			// 検索/実行件数
	private int 			maxRowCount			= 0;			// 最大検索数(0は無制限)

	// 3.8.0.9 (2005/10/17) 正規表現マッチ
	private String[]		matchKey			= null;			// 正規表現
	private boolean			display				= false;		// 表示しない
	private boolean			debug				= false;	// 5.7.3.0 (2014/02/07) デバッグ情報

	private static final Map<String,String> mustProparty   ;		// ［プロパティ］必須チェック用 Map
	private static final Map<String,String> usableProparty ;		// ［プロパティ］整合性チェック Map

	private NamingEnumeration<SearchResult> nameEnum	= null;		// 4.3.3.6 (2008/11/15) Generics警告対応
	private LineModel						newData		= null;
	private int								count		= 0;

	static {
		mustProparty = new LinkedHashMap<String,String>();
		mustProparty.put( "filter",	"検索条件(必須) 例: (&(objectClass=person)(|(belongOUID=61200)(belongOUID=61100)))" );

		usableProparty = new LinkedHashMap<String,String>();
		usableProparty.put( "initctx",		"初期コンテキストファクトリ。" + 
											CR + " (初期値:com.sun.jndi.ldap.LdapCtxFactory)" );
		usableProparty.put( "providerURL",	"サービスプロバイダリ (初期値:ldap://ldap.opengion.org:389)" );
		usableProparty.put( "entrydn",		"属性の取得元のオブジェクトの名前。" + 
											CR + " (初期値:cn=inquiry-sys,o=opengion,c=JP)" );
		usableProparty.put( "password",		"属性の取得元のパスワード(初期値:******)" );
		usableProparty.put( "searchbase",	"検索するコンテキストのベース名。" + 
											CR + " (初期値:soouid=employeeuser,o=opengion,c=JP)" );
		usableProparty.put( "searchScope",	"検索範囲。『OBJECT』『ONELEVEL』『SUBTREE』のどれか。" + 
											CR + " (初期値:SUBTREE)" );
		usableProparty.put( "timeLimit",	"結果が返されるまでのミリ秒数。0 の場合無制限(初期値:0)" );
		usableProparty.put( "attrs",		"エントリと一緒に返される属性の識別子。null の場合すべての属性" );
		usableProparty.put( "columns",		"属性の識別子に対する別名。識別子と同じ場合は『,』のみで区切る。" );
		usableProparty.put( "maxRowCount",	"最大検索数(0は無制限)  (初期値:0)" );
		usableProparty.put( "match_",		"指定のカラムと正規表現で一致時のみ処理" + 
											CR + " ( -match_LANG=ABC=[a-zA-Z]* など。)" );
		usableProparty.put( "display",		"結果を標準出力に表示する(true)かしない(false)か" + 
											CR + "(初期値:false:表示しない)" );
		usableProparty.put( "debug",	"デバッグ情報を標準出力に表示する(true)かしない(false)か" +
											CR + "(初期値:false:表示しない)" );		// 5.7.3.0 (2014/02/07) デバッグ情報
	}

	/**
	 * デフォルトコンストラクター。
	 * このクラスは、動的作成されます。デフォルトコンストラクターで、
	 * super クラスに対して、必要な初期化を行っておきます。
	 *
	 */
	public Process_LDAPReader() {
		super( "org.opengion.fukurou.process.Process_LDAPReader",mustProparty,usableProparty );
	}

	/**
	 * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
	 * 初期処理(ファイルオープン、ＤＢオープン等)に使用します。
	 *
	 * @og.rev 4.2.2.0 (2008/05/10) LDAP パスワード取得対応
	 * @og.rev 5.3.4.0 (2011/04/01) StringUtil.nval ではなく、getProparty の 初期値機能を使う
	 * @og.rev 5.6.7.0 (2013/07/27) REFERRAL対応
	 *
	 * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
	 */
	public void init( final ParamProcess paramProcess ) {
		Argument arg = getArgument();

		String	initctx 	= arg.getProparty("initctx "	,INITCTX 	 );
		String	providerURL = arg.getProparty("providerURL"	,PROVIDER	 );
		String	entrydn 	= arg.getProparty("entrydn"		,ENTRYDN 	 );	// 4.2.2.0 (2008/05/10)
		String	password 	= arg.getProparty("password"	,PASSWORD 	 );
		String	searchbase	= arg.getProparty("searchbase"	,SEARCH_BASE );

		String	searchScope = arg.getProparty("searchScope"	,SEARCH_SCOPE , SCOPE_LIST );
		timeLimit	= arg.getProparty("timeLimit",timeLimit );			// 5.3.4.0 (2011/04/01)
		maxRowCount	= arg.getProparty("maxRowCount",maxRowCount );		// 5.3.4.0 (2011/04/01)
		display		= arg.getProparty("display",display);
		debug		= arg.getProparty("debug",debug);				// 5.7.3.0 (2014/02/07) デバッグ情報

		String referral		= arg.getProparty("referral",REFERRAL);  // 5.6.7.0 (2013/07/27)

		// 属性配列を取得。なければゼロ配列
		attrs		= StringUtil.csv2Array( arg.getProparty("attrs") );
		if( attrs.length == 0 ) { attrs = null; }

		// 別名定義配列を取得。なければ属性配列をセット
		columns		= StringUtil.csv2Array( arg.getProparty("columns") );
		if( columns.length == 0 ) { columns = attrs; }

		// 属性配列が存在し、属性定義数と別名配列数が異なればエラー
		// 以降は、attrs == null か、属性定義数と別名配列数が同じはず。
		if( attrs != null && attrs.length != columns.length ) {
			String errMsg = "attrs と columns で指定の引数の数が異なります。" +
						" attrs=[" + arg.getProparty("attrs") + "] , columns=[" +
						arg.getProparty("columns") + "]" ;
			throw new RuntimeException( errMsg );
		}

		// 3.8.0.9 (2005/10/17) 正規表現マッチ
		HybsEntry[] entry = arg.getEntrys( "match_" );
		int len = entry.length;
		matchKey	= new String[columns.length];		// 正規表現
		for( int clm=0; clm<columns.length; clm++ ) {
			matchKey[clm] = null;	// 判定チェック有無の初期化
			for( int i=0; i<len; i++ ) {
				if( columns[clm].equalsIgnoreCase( entry[i].getKey() ) ) {
					matchKey[clm] = entry[i].getValue();
				}
			}
		}

		filter = arg.getProparty( "filter" ,filter );

		Hashtable<String,String> env = new Hashtable<String,String>();
		env.put(Context.INITIAL_CONTEXT_FACTORY, initctx);
		env.put(Context.PROVIDER_URL, providerURL);
		// 3.7.1.1 (2005/05/31)
	//	if( password != null && password.length() > 0 ) {
			env.put(Context.SECURITY_CREDENTIALS, password);
	//	}

		// 4.2.2.0 (2008/05/10) entrydn 属性の追加
	//	if( entrydn != null && entrydn.length() > 0 ) {
			env.put(Context.SECURITY_PRINCIPAL  , entrydn);
	//	}
			
		env.put( Context.REFERRAL, referral ); // 5.6.7.0 (2013/07/27)

		try {
			DirContext ctx = new InitialDirContext(env);
			SearchControls constraints = new SearchControls(
									changeScopeString( searchScope ),
									COUNT_LIMIT			,
									timeLimit			,
									attrs				,
									RETURN_OBJ_FLAG		,
									DEREF_LINK_FLAG
										);

			nameEnum = ctx.search(searchbase, filter, constraints);

		} catch ( NamingException ex ) {
			String errMsg = "NamingException !"
					+ ex.getMessage();				// 5.1.8.0 (2010/07/01) errMsg 修正
			throw new RuntimeException( errMsg,ex );
		}
	}

	/**
	 * プロセスの終了を行います。最後に一度だけ、呼び出されます。
	 * 終了処理(ファイルクローズ、ＤＢクローズ等)に使用します。
	 *
	 * @param   isOK トータルで、OKだったかどうか[true:成功/false:失敗]
	 */
	public void end( final boolean isOK ) {
		try {
			if( nameEnum  != null ) { nameEnum.close() ;  nameEnum  = null; }
		}
		catch ( NamingException ex ) {
			String errMsg = "ディスコネクトすることが出来ません。";
			throw new RuntimeException( errMsg,ex );
		}
	}

	/**
	 * このデータの処理において、次の処理が出来るかどうかを問い合わせます。
	 * この呼び出し１回毎に、次のデータを取得する準備を行います。
	 *
	 * @return	処理できる:true / 処理できない:false
	 */
	public boolean next() {
		try {
			return nameEnum != null && nameEnum.hasMore() ;
		}
		catch ( NamingException ex ) {
			String errMsg = "ネクストすることが出来ません。";
			throw new RuntimeException( errMsg,ex );
		}
	}

	/**
	 * 最初に、 行データである LineModel を作成します
	 * FirstProcess は、次々と処理をチェインしていく最初の行データを
	 * 作成して、後続の ChainProcess クラスに処理データを渡します。
	 *
	 * @og.rev 4.2.2.0 (2008/05/10) LDAP パスワード取得対応
	 *
	 * @param	rowNo	処理中の行番号
	 *
	 * @return	処理変換後のLineModel
	 */
	public LineModel makeLineModel( final int rowNo ) {
		count++ ;
		try {
			if( maxRowCount > 0 && maxRowCount <= executeCount ) { return null ; }
			SearchResult sRslt = nameEnum.next();		// 4.3.3.6 (2008/11/15) Generics警告対応
			Attributes att = sRslt.getAttributes();

			if( newData == null ) {
				newData = createLineModel( att );
				if( display ) { println( newData.nameLine() ); }
			}

			for( int i=0; i<attrs.length; i++ ) {
				Attribute attr = att.get(attrs[i]);
				if( attr != null ) {
					NamingEnumeration<?> vals = attr.getAll();		// 4.3.3.6 (2008/11/15) Generics警告対応
					StringBuilder buf = new StringBuilder();
					if( vals.hasMore() ) { getDataChange( vals.next(),buf ) ;}	// 4.2.2.0 (2008/05/10)
					while ( vals.hasMore() ) {
						buf.append( "," ) ;
						getDataChange( vals.next(),buf ) ;	// 4.2.2.0 (2008/05/10)
					}
					// 3.8.0.9 (2005/10/17) 正規表現マッチしなければ、スルーする。
					String value = buf.toString();
					String key = matchKey[i];
					if( key != null && value != null && !value.matches( key ) ) {
						return null;
					}
					newData.setValue( i, value );
					executeCount++ ;
				}
			}

			newData.setRowNo( rowNo );
			if( display ) { println( newData.dataLine() ); }
		}
		catch ( NamingException ex ) {
			String errMsg = "データを処理できませんでした。[" + rowNo + "]件目";
			if( newData != null ) { errMsg += newData.toString() ; }
			throw new RuntimeException( errMsg,ex );
		}
		return newData;
	}

	/**
	 * LDAPから取得したデータの変換を行います。
	 *
	 * 主に、バイト配列(byte[]) オブジェクトの場合、文字列に戻します。
	 *
	 * @og.rev 4.2.2.0 (2008/05/10) 新規追加
	 *
	 * @param	obj	主にバイト配列オブジェクト
	 * @param	buf	元のStringBuilder
	 *
	 * @return	データを追加した StringBuilder
	 */
	private StringBuilder getDataChange( final Object obj, final StringBuilder buf ) {
		if( obj == null ) { return buf; }
		else if( obj instanceof byte[] ) {
	//		buf.append( new String( (byte[])obj,"ISO-8859-1" ) );
			byte[] bb = (byte[])obj ;
			char[] chs = new char[bb.length];
			for( int i=0; i<bb.length; i++ ) {
				chs[i] = (char)bb[i];
			}
			buf.append( chs );
		}
		else {
			buf.append( obj ) ;
		}

		return buf ;
	}

	/**
	 * 内部で使用する LineModel を作成します。
	 * このクラスは、プロセスチェインの基点となりますので、新規 LineModel を返します。
	 * Exception 以外では、必ず LineModel オブジェクトを返します。
	 *
	 * @param   att Attributesオブジェクト
	 *
	 * @return	データベースから取り出して変換した LineModel
	 * @throws RuntimeException カラム名を取得できなかった場合。
	 */
	private LineModel createLineModel( final Attributes att ) {
		LineModel model = new LineModel();

		try {
			// init() でチェック済み。attrs == null か、属性定義数と別名配列数が同じはず。
			// attrs が null の場合は、全キー情報を取得します。
			if( attrs == null ) {
				NamingEnumeration<String> nmEnum = att.getIDs();	// 4.3.3.6 (2008/11/15) Generics警告対応
				List<String> lst = new ArrayList<String>();
				try {
					while( nmEnum.hasMore() ) {
						lst.add( nmEnum.next() );		// 4.3.3.6 (2008/11/15) Generics警告対応
					}
				}
				finally {
					nmEnum.close();
				}
				attrs = lst.toArray( new String[lst.size()] );
				columns = attrs;
			}

			int size = columns.length;
			model.init( size );
			for(int clm = 0; clm < size; clm++) {
				model.setName( clm,StringUtil.nval( columns[clm],attrs[clm] ) );
			}
		}
		catch ( NamingException ex ) {
			String errMsg = "ResultSetMetaData から、カラム名を取得できませんでした。";
			throw new RuntimeException( errMsg,ex );
		}
		return model;
	}

	/**
	 * スコープを表す文字列を SearchControls の定数に変換します。
	 * 入力文字列は、OBJECT、ONELEVEL、SUBTREEです。変換する定数は、
	 * SearchControls クラスの static 定数です。
	 *
	 * @param    scope スコープを表す文字列(OBJECT、ONELEVEL、SUBTREE)
	 *
	 * @return   SearchControlsの定数(OBJECT_SCOPE、ONELEVEL_SCOPE、SUBTREE_SCOPE)
	 * @see      javax.naming.directory.SearchControls#OBJECT_SCOPE
	 * @see      javax.naming.directory.SearchControls#ONELEVEL_SCOPE
	 * @see      javax.naming.directory.SearchControls#SUBTREE_SCOPE
	 */
	private int changeScopeString( final String scope ) {
		int rtnScope ;
		if( "OBJECT".equals( scope ) )        { rtnScope = SearchControls.OBJECT_SCOPE ; }
		else if( "ONELEVEL".equals( scope ) ) { rtnScope = SearchControls.ONELEVEL_SCOPE ; }
		else if( "SUBTREE".equals( scope ) )  { rtnScope = SearchControls.SUBTREE_SCOPE ; }
		else {
			String errMsg = "Search Scope in 『OBJECT』『ONELEVEL』『SUBTREE』Selected"
							+ "[" + scope + "]" ;
			throw new RuntimeException( errMsg );
		}
		return rtnScope ;
	}

	/**
	 * プロセスの処理結果のレポート表現を返します。
	 * 処理プログラム名、入力件数、出力件数などの情報です。
	 * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
	 * 形式で出してください。
	 *
	 * @return   処理結果のレポート
	 */
	public String report() {
		String report = "[" + getClass().getName() + "]" + CR
				+ TAB + "Search Filter : " + filter + CR
				+ TAB + "Input Count   : " + count ;

		return report ;
	}

	/**
	 * このクラスの使用方法を返します。
	 *
	 * @return	このクラスの使用方法
	 */
	public String usage() {
		StringBuilder buf = new StringBuilder();

		buf.append( "Process_LDAPReaderは、LDAPから読み取った内容を、LineModel に設定後、" 			).append( CR );
		buf.append( "下流に渡す、FirstProcess インターフェースの実装クラスです。"					).append( CR );
		buf.append( CR );
		buf.append( "LDAPから読み取った内容より、LineModelを作成し、下流(プロセスチェインは、"		).append( CR );
		buf.append( "チェインしているため、データは上流から下流へと渡されます。)に渡します。"		).append( CR );
		buf.append( CR );
		buf.append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。"	).append( CR );
		buf.append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に"		).append( CR );
		buf.append( "繋げてください。"																).append( CR );
		buf.append( CR ).append( CR );

		buf.append( getArgument().usage() ).append( CR );

		return buf.toString();
	}

	/**
	 * このクラスは、main メソッドから実行できません。
	 *
	 * @param	args	コマンド引数配列
	 */
	public static void main( final String[] args ) {
		LogWriter.log( new Process_LDAPReader().usage() );
	}
}
