/*
 * 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=(&(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は無制限)                          (初期値 0)
 *   [ -match_XXXX=正規表現              ] ：指定のカラムと正規表現で一致時のみ処理( -match_LANG=ABC=[a-zA-Z]* など。)
 *   [ -filter=検索条件                  ] ：検索する LDAP に指定する条件
 *   [ -display=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)

	// 検索範囲。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 final static Map<String,String> mustProparty   ;		// ［プロパティ］必須チェック用 Map
	private final static 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:表示しない)" );
	}

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

	/**
	 * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
	 * 初期処理（ファイルオープン、ＤＢオープン等）に使用します。
	 *
	 * @og.rev 4.2.2.0 (2008/05/10) LDAP パスワード取得対応
	 *
	 * @param   paramProcess 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	= StringUtil.nval( arg.getProparty("timeLimit")		,timeLimit );
		maxRowCount	= StringUtil.nval( arg.getProparty("maxRowCount")	,maxRowCount );
		display		= arg.getProparty("display",display);

		// 属性配列を取得。なければゼロ配列
		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);
	//	}

		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 !" ;
			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	boolean 処理できる: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 int 処理中の行番号
	 * @return  LineModel  処理変換後の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() ) { buf.append( vals.next() ) ;}
					if( vals.hasMore() ) { getDataChange( vals.next(),buf ) ;}	// 4.2.2.0 (2008/05/10)
					while ( vals.hasMore() ) {
						buf.append( "," ) ;
//						buf.append( vals.next() ) ;
						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 Object
	 * @param   buf StringBuilder 元の StringBuilder
	 * @return  StringBuilder データを追加した 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  データベースから取り出して変換した 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    スコープを表す文字列(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	String
	 */
	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 String[]
	 */
	public static void main( final String[] args ) {
		LogWriter.log( new Process_LDAPReader().usage() );
	}
}
