/*
 * 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.HybsEntry ;
import org.opengion.fukurou.util.LogWriter;
import org.opengion.fukurou.util.StringUtil;		// 5.7.2.3 (2014/01/31)

import java.util.Map ;
import java.util.LinkedHashMap ;

/**
 * Process_TableFilter は、上流から受け取ったデータをフィルタする、
 * ChainProcess インターフェースの実装クラスです。
 *
 * 上流(プロセスチェインのデータは上流から下流へと渡されます。)から
 * 受け取ったLineModel を元に、項目のフィルタリングを行います。
 * 条件が成立した場合は、下流に流します。複数の条件を指定できますが、
 * すべて AND で判定されます。
 * (設定条件すべてを満たす場合のみ、下流にデータを流します。)
 *
 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に
 * 繋げてください。
 *
 * @og.formSample
 *  Process_TableFilter
 *
 *   [ -prefix_XXXX=接頭辞    ] ：項目名(XXXX)が、指定の接頭辞で始まる場合、条件成立。
 *   [ -suffix_XXXX=接尾辞    ] ：項目名(XXXX)が、指定の接尾辞で終わる場合、条件成立。
 *   [ -instr_XXXX=部分文字列 ] ：項目名(XXXX)が、指定の部分文字列と一致する場合、条件成立。
 *   [ -equals_XXXX=一致      ] ：項目名(XXXX)が、文字列と一致する場合、条件成立。文字列は、大文字小文字は区別しません(equalsIgnoreCase)。
 *   [ -match_XXXX=正規表現   ] ：項目名(XXXX)が、正規表現と一致する場合、条件成立。
 *   [ -unmatch_XXXX=正規表現 ] ：項目名(XXXX)が、正規表現と一致しない場合、条件成立。
 *   [ -const_XXXX=固定値     ] ：-const_FGJ=1
 *                                     項目名(XXXX)に、固定値を設定します。
 *   [ -replace_XXXX=固定値   ] ：-replace_BIKO="YYYY⇒ZZZZ"	(元先指定は、⇒で区切ります。)
 *                                     項目名(XXXX)の文字列から、YYYY という文字を ZZZZ に置換します。
 *                                -replace_FGJ="_:0 A:1 B:2"	(元値:新値 のｽﾍﾟｰｽ区切り)
 *                                     区切り文字に ⇒ を使用しない場合は、CASE処理を行います。
 *                                     項目名(XXXX)の値を、元値 から 新値 に置換します。(完全一致)
 *   [ -display=[false/true]  ] ：結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *   [ -debug=[false/true]    ] ：デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class Process_TableFilter extends AbstractProcess implements ChainProcess {
	/** replace_ で使用する区切り記号  {@value} */
	public static final char REP_SEP		= '⇒'	;	// 4.3.1.1 (2008/08/24)

	private static final String PREFIX_KEY	= "prefix_"	;
	private static final String SUFFIX_KEY	= "suffix_"	;
	private static final String INSTR_KEY	= "instr_"	;
	private static final String EQUALS_KEY	= "equals_"	;
	private static final String MATCH_KEY	= "match_"	;
	private static final String UNMATCH_KEY	= "unmatch_";
	private static final String CONST_KEY	= "const_" 	;
	private static final String REPLACE_KEY	= "replace_" ;		// 4.3.1.1 (2008/08/24)

	private final LineModelFilter filter = new LineModelFilter();

	private boolean		display		;		// 表示しない
	private boolean		debug		;		// 5.7.3.0 (2014/02/07) デバッグ情報

	private String[]	cnstClm		;		// 固定値を設定するカラム名
	private int[]		cnstClmNos	;		// 固定値を設定するカラム番号
	private String[]	constVal	;		// カラム番号に対応した固定値

	// 4.3.1.1 (2008/08/24) replace 置換関係に必要なデータ
	private String[]	repClm		;		// 置換を設定するカラム名
	private int[]		repClmNos	;		// 置換を設定するカラム番号
	private String[]	repValFrom	;		// カラム番号に対応した置換元文字列
	private String[]	repValTo	;		// カラム番号に対応した置換後文字列

	// 5.7.2.3 (2014/01/31) replace 置換の case処理関係に必要なデータ
	private String[]	caseVals	;		// カラム番号に対応した置換後文字列

	private boolean		firstRow	= true;		// 最初の一行目
	private int			count		;

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

	static {
		mustProparty = new LinkedHashMap<>();

		usableProparty = new LinkedHashMap<>();
		usableProparty.put( PREFIX_KEY	,	"項目名(XXXX)が、指定の接頭辞で始まる場合、条件成立。" );
		usableProparty.put( SUFFIX_KEY	,	"項目名(XXXX)が、指定の接尾辞で終わる場合、条件成立。" );
		usableProparty.put( INSTR_KEY	,	"項目名(XXXX)が、指定の部分文字列と一致する場合、条件成立。" );
		usableProparty.put( EQUALS_KEY	,	"項目名(XXXX)が、文字列と一致する場合、条件成立。" +
										CR + "(大文字小文字は区別しない)" );
		usableProparty.put( MATCH_KEY	,	"項目名(XXXX)が、正規表現と一致する場合、条件成立。" );
		usableProparty.put( UNMATCH_KEY	,	"項目名(XXXX)が、正規表現と一致しない場合、条件成立。" );
		usableProparty.put( CONST_KEY	,	"項目名(XXXX)に、固定値を設定します。" );
		// 4.3.1.1 (2008/08/24) replace 置換関係
		usableProparty.put( REPLACE_KEY	,	"項目名(XXXX)の文字列から、YYYY⇒ZZZZ で部分置換します。" +
										CR + "項目名(XXXX)の文字列から、_:0 A:1 B:2でCASE置換します。" );
		usableProparty.put( "display"	,	"結果を標準出力に表示する(true)かしない(false)か" +
										CR + "(初期値:false:表示しない)" );
		usableProparty.put( "debug",	"デバッグ情報を標準出力に表示する(true)かしない(false)か" +
										CR + "(初期値:false:表示しない)" );		// 5.7.3.0 (2014/02/07) デバッグ情報
	}

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

	/**
	 * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
	 * 初期処理(ファイルオープン、ＤＢオープン等)に使用します。
	 *
	 * @og.rev 4.3.1.1 (2008/08/24) 置換関係対応
	 * @og.rev 5.7.2.3 (2014/01/31) replace 置換の case処理。
	 *
	 * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
	 */
	public void init( final ParamProcess paramProcess ) {
		final Argument arg = getArgument();

		display = arg.getProparty( "display",display );
		debug	= arg.getProparty("debug",debug);				// 5.7.3.0 (2014/02/07) デバッグ情報

		HybsEntry[] entry = arg.getEntrys( PREFIX_KEY );
		for( int i=0; i<entry.length; i++ ) {
			filter.add( FilterOperation.PREFIX, entry[i].getKey(), entry[i].getValue() );
		}

		entry = arg.getEntrys( SUFFIX_KEY );
		for( int i=0; i<entry.length; i++ ) {
			filter.add( FilterOperation.SUFFIX, entry[i].getKey(), entry[i].getValue() );
		}

		entry = arg.getEntrys( INSTR_KEY );
		for( int i=0; i<entry.length; i++ ) {
			filter.add( FilterOperation.INSTR, entry[i].getKey(), entry[i].getValue() );
		}

		entry = arg.getEntrys( EQUALS_KEY );
		for( int i=0; i<entry.length; i++ ) {
			filter.add( FilterOperation.EQUALS, entry[i].getKey(), entry[i].getValue() );
		}

		entry = arg.getEntrys( MATCH_KEY );
		for( int i=0; i<entry.length; i++ ) {
			filter.add( FilterOperation.MATCH, entry[i].getKey(), entry[i].getValue() );
		}

		entry = arg.getEntrys( UNMATCH_KEY );
		for( int i=0; i<entry.length; i++ ) {
			filter.add( FilterOperation.UNMATCH, entry[i].getKey(), entry[i].getValue() );
		}

		final HybsEntry[] cnstKey = arg.getEntrys( CONST_KEY );
		final int csize	= cnstKey.length;
		cnstClm		= new String[csize];
		constVal	= new String[csize];
		for( int i=0; i<csize; i++ ) {
			cnstClm[i]	= cnstKey[i].getKey();
			constVal[i]	= cnstKey[i].getValue();
		}

		// 4.3.1.1 (2008/08/24) replace 置換関係
		final HybsEntry[] repKey = arg.getEntrys( REPLACE_KEY );
		final int rsize	= repKey.length;
		repClm		= new String[rsize];
		repValFrom	= new String[rsize];
		repValTo	= new String[rsize];
		caseVals	= new String[rsize];				// 5.7.2.3 (2014/01/31) replace 置換の case処理
		for( int i=0; i<rsize; i++ ) {
			repClm[i]	= repKey[i].getKey();
			final String val	= repKey[i].getValue();			// val は、YYYY⇒ZZZZ の形式
			if( val != null ) {
				final int ad = val.indexOf( REP_SEP );		// REP_SEP は、'⇒'
				if( ad >= 0 ) {
					repValFrom[i]	= val.substring( 0,ad );
					repValTo[i]		= val.substring( ad+1 );
				}
				else {
					// 5.7.2.3 (2014/01/31) replace 置換の case処理
					caseVals[i]		= val;				// 5.7.2.3 (2014/01/31) replace 置換の case処理
				}
			}
		}
	}

	/**
	 * 引数の LineModel を処理するメソッドです。
	 * 変換処理後の LineModel を返します。
	 * 後続処理を行わない場合(データのフィルタリングを行う場合)は、
	 * null データを返します。つまり、null データは、後続処理を行わない
	 * フラグの代わりにも使用しています。
	 * なお、変換処理後の LineModel と、オリジナルの LineModel が、
	 * 同一か、コピー(クローン)かは、各処理メソッド内で決めています。
	 * ドキュメントに明記されていない場合は、副作用が問題になる場合は、
	 * 各処理ごとに自分でコピー(クローン)して下さい。
	 *
	 * @og.rev 4.3.1.1 (2008/08/24) 置換関係対応
	 * @og.rev 5.7.2.3 (2014/01/31) replace 置換の case処理。
	 *
	 * @param   data	オリジナルのLineModel
	 *
	 * @return	処理変換後のLineModel
	 */
	public LineModel action( final LineModel data ) {
		count++ ;

	//	if( display ) { println( data.dataLine() ); }

		if( !filter.filter( data ) ) {
			return null;		// 不一致
		}

		if( firstRow ) {
			final int csize	= cnstClm.length;
			cnstClmNos	= new int[csize];
			for( int i=0; i<csize; i++ ) {
				cnstClmNos[i] = data.getColumnNo( cnstClm[i] );
			}

	 		// 4.3.1.1 (2008/08/24) 置換関係対応
			final int rsize	= repClm.length;
			repClmNos	= new int[rsize];
			for( int i=0; i<rsize; i++ ) {
				repClmNos[i] = data.getColumnNo( repClm[i] );
			}

			firstRow = false;
			if( display ) { println( data.nameLine() ); }		// 5.7.3.0 (2014/02/07) デバッグ情報
		}

		if( debug ) { println( "Before:" + data.dataLine() ); }		// 5.1.2.0 (2010/01/01) display の条件変更

		for( int i=0; i<cnstClm.length; i++ ) {
			data.setValue( cnstClmNos[i],constVal[i] );
		}

 		// 4.3.1.1 (2008/08/24) 置換関係対応
		for( int i=0; i<repClm.length; i++ ) {
			// 5.7.2.3 (2014/01/31) Object が null の時の処理がおかしかったので修正
			String val = "";
			final Object obj = data.getValue( repClmNos[i] );
			if( obj != null ) { val = String.valueOf( obj ); }

			if( caseVals[i] == null ) {		// 従来からのリプレース処理
				val = val.replaceAll( repValFrom[i],repValTo[i] );
			}
			else {
				// 5.7.2.3 (2014/01/31) replace 置換の case処理。
				val = StringUtil.caseReplace( val , caseVals[i] , false );
			}
			data.setValue( repClmNos[i],val );
		}

		if( debug ) { println( "After :" + data.dataLine() ); }		// 5.1.2.0 (2010/01/01) display の条件変更
		else if( display ) { println( data.dataLine() ); }		// 5.1.2.0 (2010/01/01) display の条件変更
		return data;
	}

	/**
	 * プロセスの終了を行います。最後に一度だけ、呼び出されます。
	 * 終了処理(ファイルクローズ、ＤＢクローズ等)に使用します。
	 *
	 * @og.rev 4.3.1.1 (2008/08/24) 置換関係対応
	 * @og.rev 5.7.2.3 (2014/01/31) replace 置換の case処理。
	 *
	 * @param   isOK トータルで、OKだったかどうか[true:成功/false:失敗]
	 */
	public void end( final boolean isOK ) {
		cnstClm		= null;		// 固定値を設定するカラム名
		cnstClmNos	= null;		// 固定値を設定するカラム番号
		constVal	= null;		// カラム番号に対応した固定値

		repClm		= null;		// 置換を設定するカラム名
		repClmNos	= null;		// 置換を設定するカラム番号
		repValFrom	= null;		// カラム番号に対応した置換元文字列
		repValTo	= null;		// カラム番号に対応した置換後文字列
		caseVals	= null;		// 5.7.2.3 (2014/01/31) replace 置換の case処理
	}

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

		return report ;
	}

	/**
	 * このクラスの使用方法を返します。
	 *
	 * @return	このクラスの使用方法
	 * @og.rtnNotNull
	 */
	public String usage() {
		final StringBuilder buf = new StringBuilder( BUFFER_LARGE )
			.append( "Process_TableFilter は、上流から受け取ったデータをフィルタする、" 			).append( CR )
			.append( "ChainProcess インターフェースの実装クラスです。"								).append( CR )
			.append( CR )
			.append( "上流(プロセスチェインのデータは上流から下流へと渡されます。)から"				).append( CR )
			.append( "受け取ったLineModel を元に、項目のフィルタリングを行います。"					).append( CR )
			.append( "条件が成立した場合は、下流に流します。複数の条件を指定できますが、"			).append( CR )
			.append( "すべて AND で判定されます。"													).append( CR )
			.append( "(設定条件すべてを満たす場合のみ、下流にデータを流します。)"					).append( CR )
			.append( CR )
			.append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。"	).append( CR )
			.append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に"		).append( CR )
			.append( "繋げてください。"																).append( CR )
			.append( CR ).append( CR )
			.append( getArgument().usage() ).append( CR );

		return buf.toString();
	}

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