/*
 * 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.hayabusa.resource;

import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.fukurou.util.StringUtil;
import static org.opengion.fukurou.util.HybsConst.CR ;				// 6.1.0.0 (2014/12/26)
import static org.opengion.fukurou.util.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

import java.util.Hashtable;
import java.util.List;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator ;
import java.io.Serializable;

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;

/**
 * LDAPの内容を検索するための、ldapQueryタグです。
 *
 * 検索した結果は、配列で取得します。
 *
 * 下記の項目については、src/resource/システムパラメータ に、予め
 * 設定しておくことで、タグごとに指定する必要がなくなります。
 * ・LDAP_INITIAL_CONTEXT_FACTORY
 * ・LDAP_PROVIDER_URL
 * ・LDAP_ENTRYDN
 * ・LDAP_PASSWORD
 * ・LDAP_SEARCH_BASE
 * ・LDAP_SEARCH_SCOPE
 * ・LDAP_SEARCH_REFERRAL
 *
 * @og.rev 3.7.1.0 (2005/04/15) ＬＤＡＰにアクセスできる、LDAPSearch.java を新規に作成。
 * @og.group その他入力
 *
 * @version  4.0
 * @author	 Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class LDAPSearch {

	private String			initctx 			= HybsSystem.sys( "LDAP_INITIAL_CONTEXT_FACTORY" );
	private String			providerURL 		= HybsSystem.sys( "LDAP_PROVIDER_URL" );
	private String			entrydn 			= HybsSystem.sys( "LDAP_ENTRYDN" );
	private String			password 			= HybsSystem.sys( "LDAP_PASSWORD" );		// 4.2.2.0 (2008/05/10)
	private String			searchbase			= HybsSystem.sys( "LDAP_SEARCH_BASE" );
	private String			referral			= HybsSystem.sys( "LDAP_SEARCH_REFERRAL" ); // 5.6.7.0 (201/07/27)

	// 検索範囲。OBJECT_SCOPE、ONELEVEL_SCOPE、SUBTREE_SCOPE のどれか 1 つ
	private String			searchScope			= HybsSystem.sys( "LDAP_SEARCH_SCOPE" );
	private static final long	COUNTLIMIT		= 0;		// 返すエントリの最大数。0 の場合、フィルタを満たすエントリをすべて返す
	private int				timeLimit			;			// 結果が返されるまでのミリ秒数。0 の場合、無制限
	private String[]		attrs				;			// エントリと一緒に返される属性の識別子。null の場合、すべての属性を返す。空の場合、属性を返さない
	private boolean			returningObjFlag	;			// true の場合、エントリの名前にバインドされたオブジェクトを返す。false 場合、オブジェクトを返さない
	private boolean			derefLinkFlag		;			// true の場合、検索中にリンクを間接参照する

	private int				executeCount		;			// 検索/実行件数
	private int 			maxRowCount			;			// 最大検索数(0は無制限)
	private SearchControls	constraints			;
	private DirContext		ctx					;
	private String[]		orderBy				;			// ｿｰﾄ項目(csv)
	private boolean[]		desc				;			// 降順ﾌﾗｸﾞ

	/**
	 * LDAPパラメータを利用して、LDAP検索用オブジェクトを構築します。
	 *
	 * @og.rev 4.2.2.0 (2008/05/10) LDAP パスワード取得対応
	 * @og.rev 5.6.7.0 (2013/07/27) LDAPのREFERRAL対応
	 *
	 * 通常、パラメータをセット後、search( String filter ) の実行前に、呼びます。
	 */
	public void init() {
		final Hashtable<String,String> env = new Hashtable<>();
		env.put(Context.INITIAL_CONTEXT_FACTORY, initctx);
		env.put(Context.PROVIDER_URL, providerURL);
		if( ! StringUtil.isNull( referral ) ) { // 5.6.7.0 (2013/07/27)
			env.put( Context.REFERRAL, referral ); 
		}
		// 3.7.1.1 (2005/05/31)
		if( ! StringUtil.isNull( password ) ) {
			env.put( Context.SECURITY_CREDENTIALS, password.trim() );
		}
		// 4.2.2.0 (2008/05/10) entrydn 属性の追加
		if( ! StringUtil.isNull( entrydn ) ) {
			env.put( Context.SECURITY_PRINCIPAL  , entrydn );
		}

		try {
			ctx = new InitialDirContext(env);
			constraints = new SearchControls(
									changeScopeString( searchScope ),
									COUNTLIMIT			,
									timeLimit			,
									attrs				,
									returningObjFlag	,
									derefLinkFlag
										);
		} catch ( NamingException ex ) {
			final String errMsg = "LDAP検索用オブジェクトの初期化に失敗しました。" ;
			throw new HybsSystemException( errMsg,ex );		// 3.5.5.4 (2004/04/15) 引数の並び順変更
		}
	}

	/**
	 * LDPA から、値を取り出し、List オブジェクトを作成します。
	 * 引数の headerAdd をtrueにする事により、１件目に、キー情報の配列を返します。
	 *
	 * @og.rev 4.2.2.0 (2008/05/10) LDAP パスワード取得対応
	 *
	 * @param	filter	フィルター文字列
	 *
	 * @return	検索結果の Listオブジェクト
	 */
	public List<String[]> search( final String filter ) {

		final List<String[]> list = new ArrayList<>();
		try {
			final NamingEnumeration<SearchResult> results = ctx.search(searchbase, filter, constraints);	// 4.3.3.6 (2008/11/15) Generics警告対応

			final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );	// 6.1.0.0 (2014/12/26) refactoring
			while (results != null && results.hasMore()) {
				if( maxRowCount > 0 && maxRowCount <= executeCount ) { break ; }
				final SearchResult si = results.next();		// 4.3.3.6 (2008/11/15) Generics警告対応
				final Attributes at = si.getAttributes();
				// attrs が null の場合は、キー情報を取得します。
				if( attrs == null ) {
					final NamingEnumeration<String> ne = at.getIDs();	// 4.3.3.6 (2008/11/15) Generics警告対応
					final List<String> lst = new ArrayList<>();
					while( ne.hasMore() ) {
				         lst.add( ne.next() );	// 4.3.3.6 (2008/11/15) Generics警告対応
					}
					ne.close();
					attrs = lst.toArray( new String[lst.size()] );
				}

				String[] values = new String[attrs.length];
				boolean flag = false;		// 属性チェックフラグ
				for( int i=0; i<attrs.length; i++ ) {
					if( maxRowCount > 0 && maxRowCount <= executeCount ) { break ; }
					final Attribute attr = at.get(attrs[i]);
					if( attr != null ) {
						final NamingEnumeration<?> vals = attr.getAll();			// 4.3.3.6 (2008/11/15) Generics警告対応
						buf.setLength(0);											// 6.1.0.0 (2014/12/26) refactoring
						if( vals.hasMore() ) { getDataChange( vals.next(),buf ) ;}	// 4.2.2.0 (2008/05/10)
						while ( vals.hasMore() ) {
							buf.append( ',' ) ;						// 6.0.2.5 (2014/10/31) char を append する。
							getDataChange( vals.next(),buf ) ;		// 4.2.2.0 (2008/05/10)
						}
						values[i] = buf.toString();
						flag = true;
					}
				}
				if( flag ) {
					list.add( values );
					executeCount++ ;
				}
			}
			if( results != null ) { results.close(); }
		} catch ( NamingException ex ) {
			final String errMsg = "List オブジェクトの検索に失敗しました。"
						+ CR
						+ "searchbase や、entrydn の記述をご確認ください。"
						+ CR
						+ "searchbase:" + searchbase
						+ " , entrydn:" + entrydn ;
			throw new HybsSystemException( errMsg,ex );		// 3.5.5.4 (2004/04/15) 引数の並び順変更
		}
		return sort( list,attrs ) ;
	}

	/**
	 * 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[] ) {
			final 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 ;
	}

	/**
	 * 検索範囲(OBJECT/ONELEVEL/SUBTREE)を設定します(初期値:LDAP_SEARCH_SCOPE)。
	 *
	 * 検索範囲を OBJECT_SCOPE、ONELEVEL_SCOPE、SUBTREE_SCOPE のどれか 1 つです。
	 * 指定文字列は、それぞれ『OBJECT』『ONELEVEL』『SUBTREE』です。
	 *
	 * @param	scope	SearchControlsの検索範囲
	 */
	public void setSearchScope( final String scope ) {
		searchScope = StringUtil.nval( scope, searchScope );
		if( ! "OBJECT".equals( searchScope ) &&
			! "ONELEVEL".equals( searchScope ) &&
			! "SUBTREE".equals( searchScope ) ) {
				final String errMsg = "検索範囲は、『OBJECT』『ONELEVEL』『SUBTREE』の中から選択して下さい。"
								+ "[" + searchScope + "]" ;
				throw new HybsSystemException( errMsg );
		}
	}

	/**
	 * 引数の searchScope 文字列(『OBJECT』『ONELEVEL』『SUBTREE』のどれか)を、
	 * SearchControls クラス定数である、OBJECT_SCOPE、ONELEVEL_SCOPE、SUBTREE_SCOPE のどれか
	 *  1 つに設定します。
	 *
	 * @param	scope	searchScope文字列
	 *
	 * @return	SearchControls定数
	 */
	private int changeScopeString( final String scope ) {
		final 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 {
			final String errMsg = "Search Scope in 『OBJECT』『ONELEVEL』『SUBTREE』Selected"
							+ "[" + searchScope + "]" ;
			throw new HybsSystemException( errMsg );
		}
		return rtnScope ;
	}

	/**
	 * これらの SearchControls の時間制限をミリ秒単位で設定します(初期値:0[無制限])。
	 *
	 * 値が 0 の場合、無制限に待つことを意味します。
	 *
	 * @param	limit	ミリ秒単位の時間制限(初期値:無制限)
	 */
	public void setTimeLimit( final int limit ) {
		timeLimit = limit;
	}

	/**
	 * 検索中のリンクへの間接参照を有効または無効[true/false]にします(初期値:false)。
	 *
	 * 検索中のリンクへの間接参照を有効または無効にします。
	 *
	 * @param	deref	リンクを逆参照する場合は true、そうでない場合は false(初期値:false)
	 */
	public void setDerefLinkFlag( final boolean deref ) {
		derefLinkFlag = deref;
	}

	/**
	 * 結果の一部としてオブジェクトを返すことを有効または無効[true/false]にします(初期値:false)。
	 *
	 * 無効にした場合、オブジェクトの名前およびクラスだけが返されます。
	 * 有効にした場合、オブジェクトが返されます。
	 *
	 * @param	pbjflag	オブジェクトが返される場合は true、そうでない場合は false(初期値:false)
	 */
	public void setReturningObjFlag( final boolean pbjflag ) {
		returningObjFlag = pbjflag;
	}

	/**
	 * レジストリの最大検索件数をセットします(初期値:0[無制限])。
	 *
	 * DBTableModelのデータとして登録する最大件数をこの値に設定します。
	 * サーバーのメモリ資源と応答時間の確保の為です。
	 * 0 は、無制限です。(初期値は、無制限です。)
	 *
	 * @param	count	レジストリの最大検索件数
	 */
	public void setMaxRowCount( final int count ) {
		maxRowCount = count;
	}

	/**
	 * 検索の一部として返される属性を文字列配列でセットします。
	 *
	 * null は属性が何も返されないことを示します。
	 * このメソッドからは、空の配列をセットすることは出来ません。
	 *
	 * @param	atr	返される属性を識別する属性 ID の配列(可変長引数)
	 */
	public void setAttributes( final String... atr ) {
		if( atr != null && atr.length > 0 ) {		// 6.1.1.0 (2015/01/17) 可変長引数でもnullは来る。
			attrs = new String[atr.length];
			System.arraycopy( atr,0,attrs,0,atr.length );
		}
	}

	/**
	 * 検索の一部として返される属性を文字列配列で取得します。
	 *
	 * setAttributes で、設定した文字列配列が返されます。
	 * 属性配列に、 null をセットした場合、全属性が返されます。
	 *
	 * @return	返される属性を識別する属性 ID の配列
	 * @og.rtnNotNull
	 */
	public String[] getAttributes() {
		return (attrs == null) ? new String[0] : attrs.clone() ;
	}

	/**
	 * 初期コンテキストファクトリを指定します(初期値:システムパラメータ の INITIAL_CONTEXT_FACTORY)。
	 *
	 * 初期値は、システムパラメータ の INITIAL_CONTEXT_FACTORY 属性です。
	 * 例)com.sun.jndi.ldap.LdapCtxFactory
	 *
	 * @param	ctx INITIAL_CONTEXT_FACTORY属性
	 */
	public void setInitctx( final String ctx ) {
		initctx = StringUtil.nval( ctx, initctx );
	}

	/**
	 * サービスプロバイダの構成情報を指定します(初期値:システムパラメータ の LDAP_PROVIDER_URL)。
	 *
	 * プロトコルとサーバーとポートを指定します。
	 * 例)『ldap://ldap.opengion.org:389』
	 *
	 * @param	url PROVIDER_URL属性
	 */
	public void setProviderURL( final String url ) {
		providerURL = StringUtil.nval( url, providerURL );
	}

	/**
	 * 検索するコンテキストまたはオブジェクトの名前を設定します(初期値:システムパラメータ の LDAP_SEARCH_BASE)。
	 *
	 * 例)『soOUID=employeeuser,o=opengion,c=JP』
	 *
	 * @param	base SEARCHBASE属性
	 */
	public void setSearchbase( final String base ) {
		searchbase = StringUtil.nval( base, searchbase );
	}

	/**
	 * 属性の取得元のオブジェクトの名前を設定します(初期値:システムパラメータ の LDAP_ENTRYDN)。
	 *
	 * 例)『cn=inquiry-sys,o=opengion,c=JP』
	 *
	 * @param	dn 取得元のオブジェクトの名前
	 */
	public void setEntrydn( final String dn ) {
		entrydn = StringUtil.nval( dn, entrydn );
	}

	/**
	 * 属性の取得元のオブジェクトのパスワードを設定します(初期値:システムパラメータ の LDAP_PASSWORD)。
	 *
	 * @og.rev 4.2.2.0 (2008/05/10) LDAP パスワード取得対応
	 *
	 * @param	pwd 取得元のオブジェクトのパスワード
	 */
	public void setPassword( final String pwd ) {
		password = StringUtil.nval( pwd, password );
	}

	/**
	 * 検索した結果を表示する表示順をファイル属性名で指定します。
	 *
	 * attributes 属性で指定するキー、または、LDAPから返されたキーについて
	 * その属性でソートします。逆順を行う場合は、DESC を指定のカラム名の後ろに
	 * 付けて下さい。
	 *
	 * @param	ordr	ソートキーを指定。
	 */
	public void setOrderBy( final String ordr ) {
		orderBy = StringUtil.csv2Array( ordr );

		desc = new boolean[orderBy.length];
		for( int i=0; i<orderBy.length; i++ ) {
			String key = orderBy[i].trim();
			final int ad = key.indexOf( " DESC" ) ;
			if( ad > 0 ) {
				desc[i] = true;
				key = key.substring( 0,ad );
			}
			else {
				desc[i] = false;
			}
			orderBy[i] = key ;
		}
	}

	/**
	 * リストオブジェクトをヘッダーキーに対応させてソートします。
	 *
	 * @og.rev 4.2.2.0 (2008/05/10) ソート条件を増やします。
	 *
	 * @param	in		ソートするリストオブジェクト
	 * @param	headers	ソートするキーになる文字列配列(可変長引数)
	 *
	 * @return	ソート結果のリストオブジェクト
	 */
	private List<String[]> sort( final List<String[]> in,final String... headers ) {
		// 4.2.2.0 (2008/05/10) ソート条件を増やします。
		if( orderBy == null || orderBy.length == 0 ||
			headers == null || headers.length == 0 ||		// 6.1.1.0 (2015/01/17) 可変長引数でもnullは来る。
			in.isEmpty()								) { return in; }

		int[] no = new int[orderBy.length];
		for( int i=0; i<orderBy.length; i++ ) {
			final String key = orderBy[i] ;
			no[i] = -1;	// 未存在時のマーカー
			for( int j=0; j<headers.length; j++ ) {
				if( key.equalsIgnoreCase( headers[j] ) ) {
					no[i] = j ;	break;
				}
			}
			if( no[i] < 0 ) {
				final String errMsg = "指定の Order BY キーは、ヘッダー列に存在しません。"
							+ "order Key=[" + key + "] , attri=["
							+ StringUtil.array2csv( headers ) + "]" + CR ;
				throw new HybsSystemException( errMsg );
			}
		}

		final String[][] data = in.toArray( new String[in.size()][(in.get(0)).length] );
		Arrays.sort( data, new IdComparator( no,desc ) );
		return Arrays.asList( data );				// 6.2.0.0 (2015/02/27)
	}

	/**
	 * LDAPの検索結果を並び替える為の Comparator実装内部クラスです。
	 *
	 * @og.group その他入力
	 *
	 * @version  4.0
	 * @author	 Kazuhiko Hasegawa
	 * @since    JDK5.0,
	 */
	private static class IdComparator implements Comparator<String[]>,Serializable {
		private static final long serialVersionUID = 400020050131L ;	// 4.0.0.0 (2005/01/31)

		private final int[]		no ;
		private final boolean[]	desc ;
		private final int		cnt ;

		/**
		 * コンストラクター
		 *
		 * @param	no		ソートするリストオブジェクト
		 * @param	desc	ソートするキーになる文字列配列
		 */
		public IdComparator( final int[] no , final boolean[] desc ) {
			this.no		= no;
			this.desc	= desc;
			cnt			= no.length;
		}

		/**
		 * Comparator インターフェースのcompareメソッド
		 *
		 * 順序付けのために 2 つの引数を比較します。
		 * 最初の引数が 2 番目の引数より小さい場合は負の整数、
		 * 両方が等しい場合は 0、最初の引数が 2 番目の引数より
		 * 大きい場合は正の整数を返します。
		 *
		 * @og.rev 5.5.2.6 (2012/05/25) findbugs対応。トリッキーな値の置き換えをやめます。
		 *
		 * @param	s1	比較対象の最初のオブジェクト
		 * @param	s2	比較対象の 2 番目のオブジェクト
		 * @return	最初の引数が 2 番目の引数より小さい場合は負の整数、両方が等しい場合は 0、最初の引数が 2 番目の引数より大きい場合は正の整数
		 */
		public int compare( final String[] s1,final String[] s2 ) {
			if( s1 == null ) { return -1; }

			for( int i=0; i<cnt; i++ ) {
				if( s1[no[i]] == null ) { return -1; }
				if( s2[no[i]] == null ) { return 1; }	// 5.5.2.6 (2012/05/25) 比較を途中で止めないために、nullチェックしておく。
				// 5.5.2.6 (2012/05/25) findbugs対応
				final int rtn = desc[i] ? s2[no[i]].compareTo( s1[no[i]] ) : s1[no[i]].compareTo( s2[no[i]] ) ;
				if( rtn != 0 ) { return rtn ;}
			}
			return 0;
		}

	//	public boolean equals(Object obj) {
	//		return ( this == obj );
	//	}
	}

	/**
	 * このオブジェクトの文字列表現を返します。
	 * 基本的にデバッグ目的に使用します。
	 *
	 * @return このクラスの文字列表現
	 * @og.rtnNotNull
	 */
	@Override
	public String toString() {
		// 6.0.2.5 (2014/10/31) char を append する。
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
			.append( "  initctx      [" ).append( initctx      ).append( ']' ).append( CR )
			.append( "  providerURL  [" ).append( providerURL  ).append( ']' ).append( CR )
			.append( "  entrydn      [" ).append( entrydn      ).append( ']' ).append( CR )
			.append( "  searchbase   [" ).append( searchbase   ).append( ']' ).append( CR )
			.append( "  searchScope  [" ).append( searchScope  ).append( ']' ).append( CR )
			.append( "  executeCount [" ).append( executeCount ).append( ']' ).append( CR )
			.append( "  attributes   [" ).append( StringUtil.array2line( attrs,"," ) )
			.append( ']' ).append( CR );

		return buf.toString();
	}
}
