/*
 * 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.system.OgRuntimeException;							// 6.4.2.0 (2016/01/29)
import org.opengion.fukurou.system.OgCharacterException;						// 6.5.0.1 (2016/10/21)
import org.opengion.fukurou.system.Closer;
import org.opengion.fukurou.system.LogWriter;
import org.opengion.fukurou.util.Argument;
import org.opengion.fukurou.util.FileUtil;
import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.util.CommentLineParser;								// 6.3.1.1 (2015/07/10)
import org.opengion.fukurou.util.FileInfo;										// 6.4.0.2 (2015/12/11)
import org.opengion.hayabusa.common.HybsSystem;									// 8.1.3.0 (2022/06/03)

import java.util.Arrays;
import java.util.Enumeration;													// 8.1.3.0 (2022/06/03)
import java.util.jar.JarFile;													// 8.1.3.0 (2022/06/03)
import java.util.jar.JarEntry;													// 8.1.3.0 (2022/06/03)
import java.util.LinkedHashMap;
import java.util.Locale;														// 8.1.3.0 (2022/06/03)
import java.util.Map;
import java.util.regex.Pattern;
import java.util.regex.Matcher;

import java.io.File;
import java.io.PrintWriter;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;												// 8.1.3.0 (2022/06/03)
import java.io.InputStream;														// 8.1.3.0 (2022/06/03)
import java.nio.charset.CharacterCodingException;								// 6.3.1.0 (2015/06/28)

/**
 * Process_Grep は、上流から受け取った FileLineModelから、文字列を見つけ出す
 * ChainProcess ｲﾝﾀｰﾌｪｰｽの実装ｸﾗｽです。
 *
 * 正規表現の keyword を上流から受け取った FileLineModel から検索します。
 * 見つかった対象ﾌｧｲﾙから、指定の文字列を置換する場合は、-change か
 * -changeFile で、keyword を置換する文字列を指定して下さい。
 * 置換する文字列には、\t と \n の特殊文字が使用できます。
 *
 * 処理対象は、通常は、１行づつ読み取りながら処理を行います。存在ﾁｪｯｸの場合は、
 * 見つかった時点で処理を中止します。これは、該当箇所をﾋﾟｯｸｱｯﾌﾟするのではなく、
 * 存在しているかどうかを判断して、あれば、下流に流すというのが目的だからです。
 * keyword を、改行を含む正規表現で、検索・置換する場合は、-useBulkRead 属性を
 * true に設定してください。これは、入力ﾌｧｲﾙを一括して読み込みます。
 * -ignoreCase は、検索時にｷｰの大文字小文字を無視するように指定します。
 * -notEquals は、結果(見つかればtrue)を反転(見つからなければtrue)します。
 * これは、行単位ではなく、ﾌｧｲﾙ単位に判定しますので、change 指定した場合
 * でも、対象行は、見つかった行です。ただし、下流に対して、見つからない
 * 場合だけ処理を継続させます。
 * -inEncode は、入力ﾌｧｲﾙのｴﾝｺｰﾄﾞ指定になります。
 * -outEncode は、出力ﾌｧｲﾙのｴﾝｺｰﾄﾞや、changeFileで指定の置換文字列ﾌｧｲﾙの
 * ｴﾝｺｰﾄﾞ指定になります。(changeFile は、必ず 出力ﾌｧｲﾙと同じｴﾝｺｰﾄﾞです。)
 * これらのｴﾝｺｰﾄﾞが無指定の場合は、System.getProperty("file.encoding") で
 * 求まる値を使用します。
 * -changeFile を使用することで、複数行の文字列に置換することが可能です。
 * -outfile では、処理を行ったﾌｧｲﾙ名一覧をｾｰﾌﾞします。
 *
 * 上流(ﾌﾟﾛｾｽﾁｪｲﾝのﾃﾞｰﾀは上流から渡されます。)からのLineModel の
 * ﾌｧｲﾙｵﾌﾞｼﾞｪｸﾄより、指定の文字列が含まれているか検索します。
 * 上流ﾌﾟﾛｾｽでは、Name 属性として、『File』を持ち、値は、Fileｵﾌﾞｼﾞｪｸﾄ
 * である、Process_FileSearch を使用するのが、便利です。それ以外のｸﾗｽを
 * 使用する場合でも、Name属性と、File ｵﾌﾞｼﾞｪｸﾄを持つ LineModel を受け渡し
 * できれば、使用可能です。
 *
 * ※ 6.3.1.1 (2015/07/10) useOmitCmnt、useAllFind 機能追加
 *
 * 引数文字列中に空白を含む場合は、ﾀﾞﾌﾞﾙｺｰﾃｰｼｮﾝ("") で括って下さい。
 * 引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に
 * 繋げてください。
 *
 * ※ 8.1.3.0 (2022/06/03) jarPrefix、jarSuffix、jarInstr、useRegexp、saveFile 追加
 *    jar ﾌｧｲﾙの中身も検索します。その際、jarﾌｧｲﾙに圧縮されているﾌｧｲﾙ名での
 *    絞り込みができるように、指定できる属性を追加します。
 *    ただし、jarﾌｧｲﾙ内の検索は、useAllFind=true(置換ではなく検索だけ最後まで行う)のみです。
 *    上流から jar ﾌｧｲﾙが指定された場合は、常に検索対象になります。
 *
 * @og.formSample
 *  Process_Grep -keyword=検索文字列 -ignoreCase=true -outfile=OUTFILE -encode=UTF-8
 *
 *    -keyword=ｷｰﾜｰﾄﾞ        ：検索する語句
 *   [-ignoreCase=大文字小文字 ] ：検索時に大文字小文字を区別しない(true)かどうか(初期値:区別する[false])
 *   [-notEquals=判定結果の反転] ：判定結果を反転させる(true)かどうか(初期値:反転させない[false])
 *   [-inEncode=入力ｴﾝｺｰﾄﾞ     ] ：入力ﾌｧｲﾙのｴﾝｺｰﾄﾞﾀｲﾌﾟ
 *   [-outEncode=出力ｴﾝｺｰﾄﾞ    ] ：出力ﾌｧｲﾙや置換ﾌｧｲﾙのｴﾝｺｰﾄﾞﾀｲﾌﾟ
 *   [-change=置換文字列       ] ：-change="ABCD" \t や \n などの特殊文字が使用できます。
 *   [-changeFile=置換ﾌｧｲﾙ     ] ：-changeFile=change.txt このﾌｧｲﾙの記述すべてと置換します。
 *                                     -change と、-changeFile は、同時に指定できません。
 *                                     置換機能使用時は、必ず、_backup というﾌｧｲﾙが作成されます。
 *   [-insert=[HEAD/CHANGE/BEFORE/AFTER/TAIL]   ]
 *                               : 置換でなく挿入する場合の位置を指定します(初期値:CHANGE)
 *                                 ｽﾍﾟｰｽで区切って数字を記述すると、挿入位置にｵﾌｾｯﾄできます。
 *   [-delete=[false/true]     ] : 置換でなく削除します(初期値:false)
 *   [-skipRowCount=ｽｷｯﾌﾟ行数  ] : 先頭行から、ｽｷｯﾌﾟする行数を指定します(useBulkRead時には使用されません)
 *   [-useBackup=[false/true]  ] ：trueは、backupﾌｧｲﾙを作成します(初期値:false)
 *   [-useBulkRead=[false/true]] ：trueは、入力ﾌｧｲﾙを一括読込します(初期値:false)
 *   [-useAllFind=[false/true] ] ：置換ではなく検索だけ最後まで行う場合、trueを指定します(初期値:false)
 *   [-useOmitCmnt=[false/true]] ：ｺﾒﾝﾄ部分を削除したﾌｧｲﾙでgrep処理を行うかどうかを指定(初期値:false)
 *   [-errAbend=[true/false]   ] ：異常発生時に、処理を中断(true)するか、継続(false)するかを指定する(初期値:true[中断する])
 *   [-jarPrefix=接頭辞        ] ：File････,View････,など、指定の接頭辞で始まるjarﾌｧｲﾙを中を検索 8.1.3.0 (2022/06/03)
 *   [-jarSuffix=接尾辞        ] ：.txt|.java|.jsp.... など、指定の接尾辞で終わるjarﾌｧｲﾙを中を検索 8.1.3.0 (2022/06/03)
 *   [-jarInstr=部分文字列     ] ：jarﾌｧｲﾙを中と一致する部分文字列を指定 8.1.3.0 (2022/06/03)
 *   [-useRegexp=[false/true]  ] ：trueは、正規表現で検索します(初期値:false) 8.1.3.0 (2022/06/03)
 *   [-saveFile=保存ﾌｧｲﾙ       ] ：検索結果を指定ﾌｧｲﾙに保存します 8.1.3.0 (2022/06/03)
 *   [-display=[false/true]    ] ：trueは、検索状況を表示します(初期値:false)
 *   [-debug=[false/true]      ] ：ﾃﾞﾊﾞｯｸﾞ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class Process_Grep extends AbstractProcess implements ChainProcess {
	private static final String[] INSERT_LIST = { "HEAD","CHANGE","BEFORE","AFTER","TAIL" };	// 6.2.4.0 (2015/05/15)
	private static final String ENCODE		= "UTF-8";											// 8.1.3.0 (2022/06/03)
	private static final String AUTO_ENCODE	= "autoEncode";										// 8.1.3.0 (2022/06/03)

	/** 8.1.3.0 (2022/06/03) 拡張子をｴﾝｺｰﾄﾞに変換するMap */
	private static final Map<String,String> EXT2ENC = Map.ofEntries(
		Map.entry("bat"  , "Windows-31J"),	Map.entry("ken"  , "Windows-31J"),
		Map.entry("sql"  , "Windows-31J"),	Map.entry("vbs"  , "Windows-31J"),
		Map.entry("css"  , "UTF-8"),		Map.entry("html" , "UTF-8"),
		Map.entry("java" , "UTF-8"),		Map.entry("js"   , "UTF-8"),
		Map.entry("jsp"  , "UTF-8"),		Map.entry("xml"  , "UTF-8"),
		Map.entry("jar"  , "UTF-8")												// 除外されない為に追記
	);

	private Pattern pattern;
	private String	keyword;
	private boolean	ignoreCase;
	private boolean	notEquals;
	private String	inEncode;
	private String	outEncode;
	private String	change;
	private String	insert		= "CHANGE";										// "HEAD","CHANGE","BEFORE","AFTER","TAIL" のどれか
	private int		insOffset;													// "BEFORE","AFTER" 時のｵﾌｾｯﾄ
	private boolean	useBackup;
	private boolean	useBulkRead;												// 4.0.1.0 (2007/12/14) 一括読込
	private boolean	delete;
	private boolean	useAllFind;													// 6.3.1.1 (2015/07/10) 最後まで検索
	private boolean	useOmitCmnt;												// 6.3.1.1 (2015/07/10) ｺﾒﾝﾄ除外
	private boolean	errAbend	= true;											// 6.3.1.0 (2015/06/28) 中断する
	private String	jarPrefix;													// 8.1.3.0 (2022/06/03)
	private String	jarSuffix;													// 8.1.3.0 (2022/06/03)
	private String	jarInstr;													// 8.1.3.0 (2022/06/03)
	private boolean	useRegexp;													// 8.1.3.0 (2022/06/03)
	private String	saveFile;													// 8.1.3.0 (2022/06/03)
	private boolean	display;
	private boolean	debug;														// 5.1.2.0 (2010/01/01)

	private int		inCount;
	private int		findCount;
	private int		cngCount;
	private int		skipRowCount;												// 6.2.4.0 (2015/05/15) 行ｽｷｯﾌﾟ
	private PrintWriter		outWriter;											// 8.1.3.0 (2022/06/03) PrintWriterｵﾌﾞｼﾞｪｸﾄ
	private final String	fileURL	= HybsSystem.sys( "FILE_URL" );				// 8.1.3.0 (2022/06/03) ﾌｧｲﾙURL

	/** staticｲﾆｼｬﾗｲｻﾞ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
	private static final Map<String,String> MUST_PROPARTY   ;		// ［ﾌﾟﾛﾊﾟﾃｨ］必須ﾁｪｯｸ用 Map
	/** staticｲﾆｼｬﾗｲｻﾞ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
	private static final Map<String,String> USABLE_PROPARTY ;		// ［ﾌﾟﾛﾊﾟﾃｨ］整合性ﾁｪｯｸ Map

	static {
		MUST_PROPARTY = new LinkedHashMap<>();
		MUST_PROPARTY.put( "keyword",	"検索する語句(必須)" );

		USABLE_PROPARTY = new LinkedHashMap<>();
		USABLE_PROPARTY.put( "ignoreCase",	"検索時に大文字小文字を区別しない(true)かどうか。" +
										CR + "(初期値:区別する[false])" );
		USABLE_PROPARTY.put( "notEquals",	"検索時に判定結果を反転させる(true)かどうか。" +
										CR + "(初期値:反転させない[false])" );
		USABLE_PROPARTY.put( "inEncode",	"入力ﾌｧｲﾙのｴﾝｺｰﾄﾞﾀｲﾌﾟ" );
		USABLE_PROPARTY.put( "outEncode",	"出力ﾌｧｲﾙや置換ﾌｧｲﾙのｴﾝｺｰﾄﾞﾀｲﾌﾟ" );
		USABLE_PROPARTY.put( "change",		"置換文字列 例: -change=\"ABCD\" \\t や \\n などの特殊文字が使用できます。" );
		USABLE_PROPARTY.put( "changeFile",	"置換文字列ﾌｧｲﾙ 例: -changeFile=change.txt" +
										CR + "-change と、-changeFile は、同時に指定できません。" +
										CR + "置換機能使用時は、必ず、_backup というﾌｧｲﾙが作成されます。" );
		USABLE_PROPARTY.put( "insert",		"[HEAD/CHANGE/BEFORE/AFTER/TAIL]:置換でなく挿入する場合の位置を指定します(初期値:CHANGE)"  +
										CR + "ｽﾍﾟｰｽで区切って数字を記述すると、挿入位置にｵﾌｾｯﾄできます。" );
		USABLE_PROPARTY.put( "delete",		"[false/true]:trueは、置換でなく削除します(初期値:false)" );
		USABLE_PROPARTY.put( "skipRowCount","先頭行から、ｽｷｯﾌﾟする行数を指定します。" );									// 6.2.4.0 (2015/05/15)
		USABLE_PROPARTY.put( "useBackup",	"[false/true]:trueは、backupﾌｧｲﾙを作成します(初期値:false)" );
		USABLE_PROPARTY.put( "useBulkRead",	"[false/true]:trueは、入力ﾌｧｲﾙを一括読込します(初期値:false)" );
		USABLE_PROPARTY.put( "useAllFind",	"置換ではなく検索だけ最後まで行う場合、trueを指定します(初期値:false)" );	// 6.3.1.1 (2015/07/10)
		USABLE_PROPARTY.put( "useOmitCmnt",	"ｺﾒﾝﾄ部分を削除したﾌｧｲﾙでgrep処理を行うかどうかを指定(初期値:false)" );		// 6.3.1.1 (2015/07/10)
		USABLE_PROPARTY.put( "jarPrefix",	"File････,View････,など、指定の接頭辞で始まるjarﾌｧｲﾙを中を検索" );			// 8.1.3.0 (2022/06/03)
		USABLE_PROPARTY.put( "jarSuffix",	".txt|.java|.jsp.... など、指定の接尾辞で終わるjarﾌｧｲﾙを中を検索" );		// 8.1.3.0 (2022/06/03)
		USABLE_PROPARTY.put( "jarInstr",	"jarﾌｧｲﾙを中と一致する部分文字列を指定" );									// 8.1.3.0 (2022/06/03)
		USABLE_PROPARTY.put( "useRegexp",	"[false/true]:trueは、正規表現で検索します(初期値:false)" );				// 8.1.3.0 (2022/06/03)
		USABLE_PROPARTY.put( "errAbend",	"異常発生時に、処理を中断(true)するか、継続(false)するか" +
										CR + "(初期値:true:中断する)" );												// 6.3.1.0 (2015/06/28)
		USABLE_PROPARTY.put( "saveFile",	"検索結果を指定ﾌｧｲﾙに保存します" );											// 8.1.3.0 (2022/06/03)
		USABLE_PROPARTY.put( "display",		"[false/true]:trueは、検索状況を表示します(初期値:false)" );
		USABLE_PROPARTY.put( "debug",		"ﾃﾞﾊﾞｯｸﾞ情報を標準出力に表示する(true)かしない(false)か" +
											CR + "(初期値:false:表示しない)" );
	}

	/**
	 * ﾃﾞﾌｫﾙﾄｺﾝｽﾄﾗｸﾀｰ。
	 * このｸﾗｽは、動的作成されます。ﾃﾞﾌｫﾙﾄｺﾝｽﾄﾗｸﾀｰで、
	 * super ｸﾗｽに対して、必要な初期化を行っておきます。
	 *
	 */
	public Process_Grep() {
		super( "org.opengion.fukurou.process.Process_Grep",MUST_PROPARTY,USABLE_PROPARTY );
	}

	/**
	 * ﾌﾟﾛｾｽの初期化を行います。初めに一度だけ、呼び出されます。
	 * 初期処理(ﾌｧｲﾙｵｰﾌﾟﾝ、DBｵｰﾌﾟﾝ等)に使用します。
	 *
	 * @og.rev 6.3.1.0 (2015/06/28) errAbend属性追加。
	 * @og.rev 6.3.1.1 (2015/07/10) useOmitCmnt、useAllFind 機能追加
	 * @og.rev 8.1.3.0 (2022/06/03) jarﾌｧｲﾙ内の検索、ｵｰﾄｴﾝｺｰﾄﾞ対応
	 *
	 * @param	paramProcess	ﾃﾞｰﾀﾍﾞｰｽの接続先情報などを持っているｵﾌﾞｼﾞｪｸﾄ
	 */
	public void init( final ParamProcess paramProcess ) {
		final Argument arg = getArgument();

		keyword			= arg.getProparty( "keyword");
		ignoreCase		= arg.getProparty( "ignoreCase"	,ignoreCase	);
		notEquals		= arg.getProparty( "notEquals"	,notEquals	);
		inEncode		= arg.getProparty( "inEncode"	,System.getProperty("file.encoding"));
		outEncode		= arg.getProparty( "outEncode"	,System.getProperty("file.encoding"));
		useBackup		= arg.getProparty( "useBackup"	,useBackup	);
		useBulkRead		= arg.getProparty( "useBulkRead",useBulkRead);		// 4.0.1.0 (2007/12/14)
		delete			= arg.getProparty( "delete"		,delete		);
		insert			= arg.getProparty( "insert"		,insert		);
		change			= arg.getFileProparty( "change"	,"changeFile",outEncode,false );
		skipRowCount	= arg.getProparty( "skipRowCount",0			);		// 6.2.4.0 (2015/05/15)
		useAllFind		= arg.getProparty( "useAllFind"	,useAllFind);		// 6.3.1.1 (2015/07/10)
		useOmitCmnt		= arg.getProparty( "useOmitCmnt",useOmitCmnt);		// 6.3.1.1 (2015/07/10)
		errAbend		= arg.getProparty( "errAbend"	,errAbend	);		// 6.3.1.0 (2015/06/28) errAbend属性追加
		jarPrefix		= arg.getProparty( "jarPrefix"	,jarPrefix	);		// 8.1.3.0 (2022/06/03)
		jarSuffix		= arg.getProparty( "jarSuffix"	,jarSuffix	);		// 8.1.3.0 (2022/06/03)
		jarInstr		= arg.getProparty( "jarInstr"	,jarInstr	);		// 8.1.3.0 (2022/06/03)
		useRegexp		= arg.getProparty( "useRegexp"	,useRegexp	);		// 8.1.3.0 (2022/06/03)
		saveFile		= arg.getProparty( "saveFile"	,saveFile	);		// 8.1.3.0 (2022/06/03)
		display			= arg.getProparty( "display"	,display	);
		debug			= arg.getProparty( "debug"		,debug		);		// 5.1.2.0 (2010/01/01)

		if( change != null ) {
			final int adrs = insert.indexOf( ' ' );	// ｵﾌｾｯﾄ数字の有無
			if( adrs > 0 ) {
				insOffset = Integer.parseInt( insert.substring( adrs+1 ) );
				insert    = insert.substring( 0,adrs );
			}

			boolean isOK = false;
			for( int i=0; i<INSERT_LIST.length; i++ ) {
				if( insert.equalsIgnoreCase( INSERT_LIST[i] ) ) {
					isOK = true; break;
				}
			}
			if( !isOK ) {
				// 実行時ｴﾗｰではないので、errAbend 対象外
				final String errMsg = "insert は、" + Arrays.toString( INSERT_LIST )
									+ " から指定してください。" + CR
									+ "-insert=[" + insert + "]" ;
				throw new OgRuntimeException( errMsg );
			}

			change = StringUtil.replace( change,"\\n",CR );
			change = StringUtil.replace( change,"\\t","\t" );
		}

		if( delete ) { change = ""; }	// 削除は、"" 文字列と置換します。

		// 8.1.3.0 (2022/06/03) Modify
//		if( ignoreCase ) {
//			pattern = Pattern.compile( keyword,Pattern.CASE_INSENSITIVE );
//		}
//		else {
//			pattern = Pattern.compile( keyword );
//		}
		// 大文字小文字を区別しない
		if( ignoreCase ) {
			if( useRegexp ) { pattern = Pattern.compile( keyword,Pattern.CASE_INSENSITIVE ); }	// 正規表現を使用する
			else { keyword = keyword.toLowerCase( Locale.JAPAN ); }
		}
		// 大文字小文字を区別する
		else {
			if( useRegexp ) { pattern = Pattern.compile( keyword ); }							// 正規表現を使用しない
		}

		// jarﾌｧｲﾙの接頭辞／接尾辞／部分文字列
		if( StringUtil.isNotNull(jarPrefix) ){ jarPrefix = jarPrefix.toLowerCase( Locale.JAPAN ); }
		if( StringUtil.isNotNull(jarSuffix) ){ jarSuffix = jarSuffix.toLowerCase( Locale.JAPAN ); }
		if( StringUtil.isNotNull(jarInstr)  ){ jarInstr = jarInstr.toLowerCase( Locale.JAPAN ); }

		// 8.1.3.0 (2022/06/03) 保存ﾌｧｲﾙ指定有り
		if( StringUtil.isNotNull(saveFile) ){
			final String filename = HybsSystem.url2dir( StringUtil.urlAppend( fileURL, saveFile ) );
			outWriter = FileUtil.getPrintWriter( new File( filename ), ENCODE, true );
		}
	}

	/**
	 * ﾌﾟﾛｾｽの終了を行います。最後に一度だけ、呼び出されます。
	 * 終了処理(ﾌｧｲﾙｸﾛｰｽﾞ、DBｸﾛｰｽﾞ等)に使用します。
	 *
	 * @og.rev 8.1.3.0 (2022/06/03) jarﾌｧｲﾙ内の検索、ｵｰﾄｴﾝｺｰﾄﾞ対応
	 *
	 * @param	isOK	ﾄｰﾀﾙで、OKだったかどうか[true:成功/false:失敗]
	 */
	public void end( final boolean isOK ) {
		// 8.1.3.0 (2022/06/03) 保存ﾌｧｲﾙ指定有り
		if( StringUtil.isNotNull(saveFile) ){
			Closer.ioClose( outWriter );
		}
	}

	/**
	 * 引数の LineModel を処理するﾒｿｯﾄﾞです。
	 * 変換処理後の LineModel を返します。
	 * 後続処理を行わない場合(ﾃﾞｰﾀのﾌｨﾙﾀﾘﾝｸﾞを行う場合)は、
	 * null ﾃﾞｰﾀを返します。つまり、null ﾃﾞｰﾀは、後続処理を行わない
	 * ﾌﾗｸﾞの代わりにも使用しています。
	 * なお、変換処理後の LineModel と、ｵﾘｼﾞﾅﾙの LineModel が、
	 * 同一か、ｺﾋﾟｰ(ｸﾛｰﾝ)かは、各処理ﾒｿｯﾄﾞ内で決めています。
	 * ﾄﾞｷｭﾒﾝﾄに明記されていない場合は、副作用が問題になる場合は、
	 * 各処理ごとに自分でｺﾋﾟｰ(ｸﾛｰﾝ)して下さい。
	 *
	 * @og.rev 4.0.1.0 (2007/12/14) ﾌｧｲﾙの一括処理対応。
	 * @og.rev 5.7.2.2 (2014/01/24) ｴﾗｰ時にﾃﾞｰﾀも出力します。
	 * @og.rev 6.3.1.0 (2015/06/28) errAbend属性追加。
	 * @og.rev 8.1.3.0 (2022/06/03) jarﾌｧｲﾙ内の検索、ｵｰﾄｴﾝｺｰﾄﾞ対応
	 *
	 * @param	data	ｵﾘｼﾞﾅﾙのLineModel
	 *
	 * @return	処理変換後のLineModel
	 */
	@Override	// ChainProcess
	public LineModel action( final LineModel data ) {
		inCount++ ;

		final FileLineModel fileData ;
		if( data instanceof FileLineModel ) {
			fileData = (FileLineModel)data ;
		}
		else {
			// これは、ﾌﾟﾛｸﾞﾗﾏｰの問題なので、errAbend 対象外
			final String errMsg = "データが FileLineModel オブジェクトではありません。" + CR ;
			throw new OgRuntimeException( errMsg );
		}

		final File file = fileData.getFile() ;
		if( !file.isFile() ) {
			if( display ) { println( data.dataLine() ); }		// 5.1.2.0 (2010/01/01) display の条件変更
			return data;
		}

		boolean isFind = false ;	// 6.3.1.0 (2015/06/28) errAbend属性追加に伴う、初期化漏れ対応
		try {
			String fileLine = null;
			int firstLineNo = -1;

			// 8.1.3.0 (2022/06/03) 拡張子によるｴﾝｺｰﾄﾞ指定
			final String mapEnc = getEncode( file );
			// 8.1.3.0 (2022/06/03) 該当するｴﾝｺｰﾄﾞ無し
			if( !"-".equals(mapEnc) ) {
				// 8.1.3.0 (2022/06/03) jarﾌｧｲﾙ内の検索
//				if( useBulkRead ) { fileLine    = findKeywordAsBulk( file ); }
//				else			  { firstLineNo = findKeyword( file ); }
				if( useBulkRead ) { fileLine = findKeywordAsBulk( file,mapEnc ); }
				else {
					if( file.getName().endsWith( ".jar" ) )	{ findJarKeyword( file ); }		// firstLineNo は、常に -1
					else									{ firstLineNo = findKeyword( file,mapEnc ); }
				}
			}

			isFind = fileLine != null || firstLineNo >= 0 ;

			// 置換処理 ただし、見つかったときのみ実行
			if( change != null && isFind ) {
				// 入力ﾌｧｲﾙは、ｵﾘｼﾞﾅﾙ_backup ﾌｧｲﾙとする。過去のﾌｧｲﾙを削除
				final File inFile = new File( file.getPath() + "_backup" );
				if( inFile.exists() && !inFile.delete() ) {
					final String errMsg = "過去のBKUPファイルを削除できませんでした。[" + inFile + "]" + CR
								+	"data=[" + data.dataLine() + "]" + CR ;		// 5.7.2.2 (2014/01/24) ｴﾗｰ時にﾃﾞｰﾀも出力します。
					// try の中から throw するのは、行儀がよくないが、catch ﾌﾞﾛｯｸで errAbend処理する。
					throw new OgRuntimeException( errMsg );
				}

				// ｵﾘｼﾞﾅﾙのﾌｧｲﾙを、_backup ﾌｧｲﾙ名に先に変換する。
				final File fromFile = new File( file.getPath() );
				if( !fromFile.renameTo( inFile ) ) {
					final String errMsg = "所定のファイルをリネームできませんでした。[" + fromFile + "]" + CR
								+	"data=[" + data.dataLine() + "]" + CR ;		// 5.7.2.2 (2014/01/24) ｴﾗｰ時にﾃﾞｰﾀも出力します。
					// try の中から throw するのは、行儀がよくないが、catch ﾌﾞﾛｯｸで errAbend処理する。
					throw new OgRuntimeException( errMsg );
				}

				// 変換処理 本体
				if( useBulkRead ) { changeKeywordAsBulk( fileLine,file ); }
//				else			  { changeKeyword( inFile,file,firstLineNo ); }
				else			  { changeKeyword( inFile,file,firstLineNo,mapEnc ); }	// 8.1.3.0 (2022/06/03)

				// backup を使わない場合は、削除する。
				// 4.0.0.0 (2007/11/29) 入れ子if の統合
				if( !useBackup && !inFile.delete() ) {
					final String errMsg = "所定のファイルを削除できませんでした。[" + inFile + "]" + CR
								+	"data=[" + data.dataLine() + "]" + CR ;		// 5.7.2.2 (2014/01/24) ｴﾗｰ時にﾃﾞｰﾀも出力します。
					// try の中から throw するのは、行儀がよくないが、catch ﾌﾞﾛｯｸで errAbend処理する。
					throw new OgRuntimeException( errMsg );
				}
			}
		}
		catch( final RuntimeException ex ) {
			final String errMsg = "処理中にエラーが発生しました。[" + data.getRowNo() + "]件目" + CR
						+	"data=[" + data.dataLine() + "]" + CR ;		// 5.7.2.2 (2014/01/24) ｴﾗｰ時にﾃﾞｰﾀも出力します。
			// 6.3.1.0 (2015/06/28) errAbend属性追加。
			throwException( errMsg,ex,errAbend );
		}

//		if( display && ( notEquals ^ isFind ) ) { println( data.dataLine() ); }		// 5.1.2.0 (2010/01/01) display の条件変更
		if( notEquals ^ isFind && display ) { println( data.dataLine() ); }			// 5.1.2.0 (2010/01/01) display の条件変更	// 6.9.7.0 (2018/05/14) PMD Useless parentheses.
		return notEquals ^ isFind ? data : null ;
	}

	/**
	 * ｷｰﾜｰﾄﾞが存在しているかどうかをﾁｪｯｸします。
	 * ここでは、１行づつ読み取りながら、すべてのｷｰﾜｰﾄﾞをﾋﾟｯｸｱｯﾌﾟします。
	 *
	 * @og.rev 8.1.3.0 (2022/06/03) jarﾌｧｲﾙ内の検索、ｵｰﾄｴﾝｺｰﾄﾞ対応
	 * @og.rev 8.5.2.0 (2023/07/14) -change=置換文字列 の指定がなくても findCount がｾｯﾄされるように対応
	 *
	 * @param	file	検索元のﾌｧｲﾙｵﾌﾞｼﾞｪｸﾄ
	 */
	private void findJarKeyword( final File file ) {
		JarFile jarFile = null;

		try {
			jarFile = new JarFile( file );

			final Enumeration<JarEntry> flEnum = jarFile.entries() ;			// Generics警告対応
			while( flEnum.hasMoreElements() ) {
				final JarEntry ent = flEnum.nextElement();						// Generics警告対応
				if( ent.isDirectory() ) { continue; }

				final String fileName = ent.getName();							// jarﾌｧｲﾙ内のﾌｧｲﾙ

				// 拡張子によるｴﾝｺｰﾄﾞ指定
				final String mapEnc = getEncode( fileName );
				// 該当するｴﾝｺｰﾄﾞ無し
				if( "-".equals(mapEnc) ) { continue; }

				final String lowName = fileName.toLowerCase( Locale.JAPAN );
				if( ( jarPrefix == null || lowName.startsWith( jarPrefix ) )	&&
					( jarSuffix == null || lowName.endsWith( jarSuffix )   )	&&
					( jarInstr  == null || lowName.contains( jarInstr )    ) ) {

					InputStream stream = null;
					try {
						stream = jarFile.getInputStream( ent ) ;

						final BufferedReader reader = new BufferedReader( new InputStreamReader( stream, mapEnc ) );

						final CommentLineParser clp = useOmitCmnt ? new CommentLineParser( FileInfo.getSUFIX( fileName ) ) : null;
						try {
							String line ;
							int lineNo = 0;
							while((line = reader.readLine()) != null) {
								lineNo++ ;										// 注意：ここで返す行数は、ｺﾒﾝﾄ行を含む行数とする

								// useOmitCmnt 機能(ｺﾒﾝﾄ行を削除する処理を入れる)
								if( useOmitCmnt ) {
									line = clp.line( line );
									if( line == null ) { continue; }			// 戻り値が null の場合は、行として不成立
								}

								// ｷｰﾜｰﾄﾞを含むかどうか判定
								if( isKeyword( line ) ) {
									final String msg = file.getPath() + "\\" + fileName + '(' + lineNo + "):" + line ;
									if( debug ) {
										final String buf = "DEBUG:\t" + msg ;
										println( buf );
									}
									// useAllFind=true 相当の処理のみ行う
									println( msg );
									findCount++ ;								// 8.5.2.0 (2023/07/14) Add
									// 保存ﾌｧｲﾙ指定有り
									if( StringUtil.isNotNull(saveFile) ){ outWriter.println( msg ); }

									// useAllFind 機能(最後まで検索を続ける)
									if( !useAllFind ) { break; }
								}
							}
						}
						// nioを使用すると UTF-8とShuft-JISで、ｴﾗｰになる
						catch( final CharacterCodingException ex ) {
							final String errMsg = "文字のエンコード・エラーが発生しました。" + CR
												+	"  ﾌァイルのエンコードが指定のエンコードと異なります。" + CR
												+	" [" + file.getPath() + "] , Encode=[" + mapEnc + "]" ;
							// 呼出元で errAbend 処理するので、そのまま throw しておく
							throw new OgCharacterException( errMsg,ex );
						}
						catch( final IOException ex ) {
							final String errMsg = "キーワードファイル読取エラーが発生しました。" + CR
												+	" [" + file.getPath() + "] , Encode=[" + mapEnc + "]" ;
							// 呼出元で errAbend 処理するので、そのまま throw しておく
							throw new OgRuntimeException( errMsg,ex );
						}
						finally {
							Closer.ioClose( reader );
						}
					}
					finally {
						Closer.ioClose( stream );
					}
				}
			}
		}
		catch( final IOException ex ) {
			final String errMsg = "キーワードファイル読取エラーが発生しました。" + CR
								+	" [" + file.getPath() + "] , Encode=[" + inEncode + "]" ;
			// 呼出元で errAbend 処理するので、そのまま throw しておく
			throw new OgRuntimeException( errMsg,ex );
		}
		finally {
			Closer.zipClose( jarFile );
		}
	}

	/**
	 * 拡張子をｴﾝｺｰﾄﾞに変換します。
	 *
	 * @og.rev 8.1.3.0 (2022/06/03) jarﾌｧｲﾙ内の検索、ｵｰﾄｴﾝｺｰﾄﾞ対応
	 *
	 * @param	file	検索元のﾌｧｲﾙ
	 * @return	ｴﾝｺｰﾄﾞ
	 */
	private String getEncode( final File file ) {
		return getEncode( file.getName() );
	}

	/**
	 * 拡張子をｴﾝｺｰﾄﾞに変換します。
	 *
	 * @og.rev 8.1.3.0 (2022/06/03) jarﾌｧｲﾙ内の検索、ｵｰﾄｴﾝｺｰﾄﾞ対応
	 *
	 * @param	fileName	検索元のﾌｧｲﾙ名
	 * @return	ｴﾝｺｰﾄﾞ
	 */
	private String getEncode( final String fileName ) {
		// 自動判定
		if( AUTO_ENCODE.equalsIgnoreCase( inEncode ) ) {
			final String sufix = FileInfo.getSUFIX( fileName );					// 拡張子取得
			return StringUtil.nval( EXT2ENC.get( sufix ), "-" );
		}
		else {
			return StringUtil.nval( inEncode, "-" );
		}
	}

	/**
	 * 該当の行にｷｰﾜｰﾄﾞを含むかどうか判定します。
	 *
	 * @og.rev 8.1.3.0 (2022/06/03) jarﾌｧｲﾙ内の検索、ｵｰﾄｴﾝｺｰﾄﾞ対応
	 *
	 * @param	line	検索元のﾌｧｲﾙの行
	 * @return	ｷｰﾜｰﾄﾞを含むかどうか[true/false]
	 */
	private boolean isKeyword( final String line ) {
		// 正規表現を使用する
		if( useRegexp ){
			final Matcher mach = pattern.matcher( line );
			return mach.find();
		}
		// 正規表現を使用しない
		else {
			// 大文字小文字を区別しない
			if( ignoreCase ) {
				return line.toLowerCase( Locale.JAPAN ).contains( keyword );
			}
			// 大文字小文字を区別する
			else {
				return line.contains( keyword );
			}
		}
	}

	/**
	 * ｷｰﾜｰﾄﾞが存在しているかどうかをﾁｪｯｸします。
	 * ここでは、１行づつ読み取りながら、最初に見つかった時点で制御を返します。
	 * よって、複数行にまたがる keyword でのﾏｯﾁﾝｸﾞは出来ませんが、大きな
	 * ﾌｧｲﾙ等での検索には、効率的です。
	 *
	 * @og.rev 4.0.1.0 (2007/12/14) 新規追加
	 * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、ｴﾗｰになる。
	 * @og.rev 6.3.1.0 (2015/06/28) errAbend属性追加。
	 * @og.rev 6.3.1.1 (2015/07/10) useOmitCmnt、useAllFind 機能追加
	 * @og.rev 6.4.0.2 (2015/12/11) CommentLineParser 改造。
	 * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。
	 * @og.rev 8.1.3.0 (2022/06/03) jarﾌｧｲﾙ内の検索、ｵｰﾄｴﾝｺｰﾄﾞ対応
	 * @og.rev 8.5.2.0 (2023/07/14) -change=置換文字列 の指定がなくても findCount がｾｯﾄされるように対応
	 *
	 * @param	file	検索元のﾌｧｲﾙｵﾌﾞｼﾞｪｸﾄ
	 * @param	mapEnc	検索元のﾌｧｲﾙｴﾝｺｰﾄﾞ
	 *
	 * @return	最初に見つかった行番号(見つからなければ、-1 を返す)
	 */
//	private int findKeyword( final File file ) {
	private int findKeyword( final File file ,final String mapEnc ) {				// 8.1.3.0 (2022/06/03)
		int firstLineNo = -1;
//		final BufferedReader reader = FileUtil.getBufferedReader( file,inEncode );
		final BufferedReader reader = FileUtil.getBufferedReader( file,mapEnc );	// 8.1.3.0 (2022/06/03)

		// 6.4.0.2 (2015/12/11) CommentLineParser 改造
		final CommentLineParser clp = useOmitCmnt ? new CommentLineParser( FileInfo.getSUFIX( file ) ) : null;
		try {
			String line ;
			int lineNo = 0;
			while((line = reader.readLine()) != null) {
				lineNo++ ;		// 注意：ここで返す行数は、ｺﾒﾝﾄ行を含む行数とする。

				// 6.3.1.1 (2015/07/10) useOmitCmnt 機能。ｺﾒﾝﾄ行を削除する処理を入れる。
				if( useOmitCmnt ) {
					line = clp.line( line );
					if( line == null ) { continue; }	// 戻り値が null の場合は、行として不成立
				}
				// 8.1.3.0 (2022/06/03) Modify
//				final Matcher mach = pattern.matcher( line );
//				if( mach.find() ) {
				// ｷｰﾜｰﾄﾞを含むかどうか判定
				if( isKeyword( line ) ) {
					if( debug ) {
						final String buf = "DEBUG:\t" + file.getPath() + "(" + lineNo + "): " + line ;
						println( buf );
					}

					// 6.3.1.1 (2015/07/10) useAllFind 機能。最後まで検索を続けます。
					if( useAllFind ) {
						final String msg = file.getAbsolutePath() + '(' + lineNo + "):" + line ;
						println( msg );
						findCount++ ;											// 8.5.2.0 (2023/07/14) Add
						// 8.1.3.0 (2022/06/03) 保存ﾌｧｲﾙ指定有り
						if( StringUtil.isNotNull(saveFile) ){ outWriter.println( msg ); }
					}
					else {
						firstLineNo = lineNo;
						break;
					}
				}
			}
		}
		// 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、ｴﾗｰになる。
		catch( final CharacterCodingException ex ) {
			final String errMsg = "文字のエンコード・エラーが発生しました。" + CR
								+	"  ファイルのエンコードが指定のエンコードと異なります。" + CR
//								+	" [" + file.getPath() + "] , Encode=[" + inEncode + "]" ;
								+	" [" + file.getPath() + "] , Encode=[" + mapEnc + "]" ;	// 8.1.3.0 (2022/06/03)
			// 呼出元で、errAbend処理するので、そのまま、throw しておく。
			throw new OgCharacterException( errMsg,ex );	// 6.5.0.1 (2016/10/21)
		}
		catch( final IOException ex ) {
			final String errMsg = "キーワードファイル読取エラーが発生しました。" + CR
//								+	" [" + file.getPath() + "] , Encode=[" + inEncode + "]" ;
								+	" [" + file.getPath() + "] , Encode=[" + mapEnc + "]" ;	// 8.1.3.0 (2022/06/03)
			// 呼出元で、errAbend処理するので、そのまま、throw しておく。
			throw new OgRuntimeException( errMsg,ex );
		}
		finally {
			Closer.ioClose( reader );
		}

		return firstLineNo;
	}

	/**
	 * ｷｰﾜｰﾄﾞが存在しているかどうかをﾁｪｯｸします。
	 * ここでは、ﾌｧｲﾙをすべて読み取ってから、ﾁｪｯｸします。
	 * よって、複数行にまたがる keyword でのﾏｯﾁﾝｸﾞが可能です。
	 *
	 * @og.rev 4.0.1.0 (2007/12/14) 新規追加
	 * @og.rev 6.4.5.1 (2016/04/28) FileStringのｺﾝｽﾄﾗｸﾀｰ変更
	 * @og.rev 6.4.5.2 (2016/05/06) fukurou.util.FileString から、fukurou.util.FileUtil に移動。
	 * @og.rev 8.1.3.0 (2022/06/03) jarﾌｧｲﾙ内の検索、ｵｰﾄｴﾝｺｰﾄﾞ対応
	 * @og.rev 8.5.2.0 (2023/07/14) -change=置換文字列 の指定がなくても findCount がｾｯﾄされるように対応
	 *
	 * @param	file	検索元のﾌｧｲﾙｵﾌﾞｼﾞｪｸﾄ
	 * @param	mapEnc	検索元のﾌｧｲﾙｴﾝｺｰﾄﾞ
	 *
	 * @return	検索元のﾌｧｲﾙの文字列化情報(ただし、見つからなければ、null)
	 */
//	private String findKeywordAsBulk( final File file ) {
	private String findKeywordAsBulk( final File file,final String mapEnc ) {	// 8.1.3.0 (2022/06/03)

		boolean isFind = false;

		// 6.4.5.1 (2016/04/28) FileStringのｺﾝｽﾄﾗｸﾀｰ変更
//		final String line = FileUtil.getValue( file.getPath() , inEncode );		// 6.4.5.2 (2016/05/06)
		final String line = FileUtil.getValue( file.getPath() , mapEnc );		// 8.1.3.0 (2022/06/03)

		final Matcher mach = pattern.matcher( line );
		if( mach.find() ) {
			if( debug ) { println( "DEBUG:\t" + file.getPath() ); }
			findCount++ ;														// 8.5.2.0 (2023/07/14) Add
			isFind = true;
		}

		return isFind ? line : null;
	}

	/**
	 * ｷｰﾜｰﾄﾞを指定の文字列に置き換えます。
	 * useBackup 属性に true を指定した場合、置き換え後の、backup ﾌｧｲﾙは、
	 * ｵﾘｼﾞﾅﾙ_backup という名称に変わります。
	 * ここでは、１行づつ読み取りながら、変換処理を行います。
	 * よって、複数行にまたがる keyword でのﾏｯﾁﾝｸﾞは出来ませんが、大きな
	 * ﾌｧｲﾙ等での置換でも、ﾒﾓﾘの使用量は抑えられます。
	 *
	 * @og.rev 4.0.1.0 (2007/12/14) 置換処理を独立させます。
	 * @og.rev 6.2.4.0 (2015/05/15) HEAD,TAIL 追加
	 * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、ｴﾗｰになる。
	 * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。
	 * @og.rev 8.1.3.0 (2022/06/03) jarﾌｧｲﾙ内の検索、ｵｰﾄｴﾝｺｰﾄﾞ対応
	 *
	 * @param	inFile	検索元の入力ﾌｧｲﾙｵﾌﾞｼﾞｪｸﾄ
	 * @param	outFile	変換後の出力ﾌｧｲﾙｵﾌﾞｼﾞｪｸﾄ
	 * @param	firstLineNo	ｷｰﾜｰﾄﾞが存在した場合の最初の行番号
	 * @param	mapEnc	検索元のﾌｧｲﾙｴﾝｺｰﾄﾞ
	 */
//	private void changeKeyword( final File inFile,final File outFile,final int firstLineNo ) {
	private void changeKeyword( final File inFile,final File outFile,final int firstLineNo,final String mapEnc ) {	// 8.1.3.0 (2022/06/03)

//		final BufferedReader reader = FileUtil.getBufferedReader( inFile,inEncode );
		final BufferedReader reader = FileUtil.getBufferedReader( inFile,mapEnc );	// 8.1.3.0 (2022/06/03)
		final PrintWriter    writer = FileUtil.getPrintWriter( outFile,outEncode );

		String line = null;
		try {
			// 6.2.4.0 (2015/05/15) HEAD,TAIL 追加
			if( "HEAD".equals( insert ) ) {
				writer.println( change );
			}

			int lineNo = 0;
			while((line = reader.readLine()) != null) {
				lineNo++ ;
				if( lineNo <= skipRowCount ) { continue; }		// 6.2.4.0 (2015/05/15)

				if( lineNo >= firstLineNo ) {
					final Matcher mach = pattern.matcher( line );

					String chnStr = null;
					if( "CHANGE".equals( insert ) ) {
						chnStr = strChange( mach );
					}
					else if( "BEFORE".equals( insert ) ) {
						chnStr = strBefore( line,mach );
					}
					else if( "AFTER".equals( insert ) ) {
						chnStr = strAfter( line,mach );
					}

					if( chnStr != null ) {
						line = chnStr;
						cngCount++ ;	// 変換されれば ｶｳﾝﾄ
					}
				}
				writer.println( line );	// readLine() してるので、最後に改行が必要。
			}

			// 6.2.4.0 (2015/05/15) HEAD,TAIL 追加
			if( "TAIL".equals( insert ) ) {
				writer.println( change );
			}
		}
		// 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、ｴﾗｰになる。
		catch( final CharacterCodingException ex ) {
			final String errMsg = "文字のエンコード・エラーが発生しました。" + CR
								+	"  ファイルのエンコードが指定のエンコードと異なります。" + CR
//								+	" [" + inFile + "] , Encode=[" + inEncode + "]" ;
								+	" [" + inFile + "] , Encode=[" + mapEnc + "]" ;	// 8.1.3.0 (2022/06/03)
			// 呼出元で、errAbend処理するので、そのまま、throw しておく。
			throw new OgCharacterException( errMsg,ex );	// 6.5.0.1 (2016/10/21)
		}
		catch( final IOException ex ) {
			final String errMsg = "処理中にエラーが発生しました。[" + line + "]" + CR
//								+	" [" + inFile + "] , Encode=[" + inEncode + "]" ;
								+	" [" + inFile + "] , Encode=[" + mapEnc + "]" ;	// 8.1.3.0 (2022/06/03)
			// 呼出元で、errAbend処理するので、そのまま、throw しておく。
			throw new OgRuntimeException( errMsg,ex );
		}
		finally {
			Closer.ioClose( reader );
			Closer.ioClose( writer );
		}
	}
	/**
	 * ｷｰﾜｰﾄﾞを指定の文字列に置き換えます。
	 * useBackup 属性に true を指定した場合、置き換え後の、backup ﾌｧｲﾙは、
	 * ｵﾘｼﾞﾅﾙ_backup という名称に変わります。
	 * ここでは、ﾌｧｲﾙをすべて読み取ってから、ﾁｪｯｸします。
	 * よって、複数行にまたがる keyword でのﾏｯﾁﾝｸﾞが可能です。
	 *
	 * @og.rev 4.0.1.0 (2007/12/14) 置換処理を独立させます。
	 * @og.rev 6.2.4.0 (2015/05/15) HEAD,TAIL 追加
	 *
	 * @param	fileLine	検索元の行文字列
	 * @param	outFile	出力ﾌｧｲﾙｵﾌﾞｼﾞｪｸﾄ
	 */
	private void changeKeywordAsBulk( final String fileLine,final File outFile ) {
		final PrintWriter writer = FileUtil.getPrintWriter( outFile,outEncode );

		String line = fileLine ;
		try {
			// 6.2.4.0 (2015/05/15) HEAD,TAIL 追加
			if( "HEAD".equals( insert ) ) {
				writer.println( change );
			}

			final Matcher mach = pattern.matcher( line );

			String chnStr = null;
			if( "CHANGE".equals( insert ) ) {
				chnStr = strChange( mach );
			}
			else if( "BEFORE".equals( insert ) ) {
				chnStr = strBefore( line,mach );
			}
			else if( "AFTER".equals( insert ) ) {
				chnStr = strAfter( line,mach );
			}

			if( chnStr != null ) {
				line = chnStr;
				cngCount++ ;	// 変換されれば ｶｳﾝﾄ
			}

			writer.print( line );	// 注意：改行ｺｰﾄﾞは、不要

			// 6.2.4.0 (2015/05/15) HEAD,TAIL 追加
			if( "TAIL".equals( insert ) ) {
				writer.println( change );
			}
		}
		catch( final RuntimeException ex ) {
			final String errMsg = "処理中にエラーが発生しました。[" + outFile.getPath() + "]" ;
			// 呼出元で、errAbend処理するので、そのまま、throw しておく。
			throw new OgRuntimeException( errMsg,ex );
		}
		finally {
			Closer.ioClose( writer );
		}
	}

	/**
	 * insert が、"CHANGE" の場合の処理結果を求めます。
	 * 変換しなかった場合は、null を返します。
	 * これは、変換ｶｳﾝﾄを算出する為のﾌﾗｸﾞ代わりに使用しています。
	 *
	 * @param	mach	ｷｰﾜｰﾄﾞの正規表現
	 *
	 * @return	変換結果(対象行で無い場合は、null)
	 */
	private String strChange( final Matcher mach ) {
		String line = null;
		if( mach.find() ) {
			line = mach.replaceAll( change );
		}
		return line ;
	}

	/**
	 * insert が、"BEFORE" の場合の処理結果を求めます。
	 * 変換しなかった場合は、null を返します。
	 * これは、変換ｶｳﾝﾄを算出する為のﾌﾗｸﾞ代わりに使用しています。
	 *
	 * @param	line	検索行
	 * @param	mach	ｷｰﾜｰﾄﾞの正規表現
	 *
	 * @return	変換結果(対象行で無い場合は、null)
	 */
	private String strBefore( final String line , final Matcher mach ) {
		boolean isChng = false;
		final StringBuilder buf = new StringBuilder( line.length() );
		int indx = 0;
		while( mach.find() ) {
			isChng = true;
			final int strt = mach.start() + insOffset;
			buf.append( line.substring( indx,strt ) );
			buf.append( change );
			indx = strt;
		}

		String rtn = null;
		if( isChng ) {
			buf.append( line.substring( indx ) );
			rtn = buf.toString();
		}

		return rtn ;
	}

	/**
	 * insert が、"AFTER" の場合の処理結果を求めます。
	 * 変換しなかった場合は、null を返します。
	 * これは、変換ｶｳﾝﾄを算出する為のﾌﾗｸﾞ代わりに使用しています。
	 *
	 * @param	line	検索行
	 * @param	mach	ｷｰﾜｰﾄﾞの正規表現
	 *
	 * @return	変換結果(対象行で無い場合は、null)
	 */
	private String strAfter( final String line , final Matcher mach ) {
		boolean isChng = false;
		final StringBuilder buf = new StringBuilder( line.length() );
		int indx = 0;
		while( mach.find() ) {
			isChng = true;
			final int end = mach.end() + insOffset;
			buf.append( line.substring( indx,end ) );
			buf.append( change );
			indx = end;
		}
		String rtn = null;
		if( isChng ) {
			buf.append( line.substring( indx ) );
			rtn = buf.toString();
		}

		return rtn ;
	}

	/**
	 * ﾌﾟﾛｾｽの処理結果のﾚﾎﾟｰﾄ表現を返します。
	 * 処理ﾌﾟﾛｸﾞﾗﾑ名、入力件数、出力件数などの情報です。
	 * この文字列をそのまま、標準出力に出すことで、結果ﾚﾎﾟｰﾄと出来るような
	 * 形式で出してください。
	 *
	 * @return   処理結果のﾚﾎﾟｰﾄ
	 */
	public String report() {
		if( findCount < cngCount ) { findCount = cngCount; }

		// 7.2.9.5 (2020/11/28) PMD:Consider simply returning the value vs storing it in local variable 'XXXX'
		return "[" + getClass().getName() + "]" + CR
//		final String report = "[" + getClass().getName() + "]" + CR
				+ TAB + "Search Keyword    : " + keyword    + CR
				+ TAB + "Search File Count : " + inCount    + CR
				+ TAB + "Key Find    Count : " + findCount  + CR
				+ TAB + "Key Change  Count : " + cngCount ;

//		return report ;
	}

	/**
	 * このｸﾗｽの使用方法を返します。
	 *
	 * @return	このｸﾗｽの使用方法
	 * @og.rtnNotNull
	 */
	public String usage() {
		final StringBuilder buf = new StringBuilder( 1400 )
			.append( "Process_Grep は、上流から受け取った FileLineModelから、文字列を見つけ出す"	).append( CR )
			.append( "ChainProcess ｲﾝﾀｰﾌｪｰｽの実装ｸﾗｽです。"											).append( CR )
			.append( CR )
			.append( "正規表現の keyword を上流から受け取った FileLineModel から検索します。"		).append( CR )
			.append( "見つかった対象ﾌｧｲﾙから、指定の文字列を置換する場合は、-change か"				).append( CR )
			.append( "-changeFile で、keyword を置換する文字列を指定して下さい。"					).append( CR )
			.append( "置換する文字列には、\t と \n の特殊文字が使用できます。"						).append( CR )
			.append( CR )
			.append( "処理対象は、通常は、１行づつ読み取りながら処理を行います。存在ﾁｪｯｸの場合は、"	).append( CR )
			.append( "見つかった時点で処理を中止します。これは、該当箇所をﾋﾟｯｸｱｯﾌﾟするのではなく、"	).append( CR )
			.append( "存在しているかどうかを判断して、あれば、下流に流すというのが目的だからです。"	).append( CR )
			.append( "keyword を、改行を含む正規表現で、検索・置換する場合は、-useBulkRead 属性を"	).append( CR )
			.append( "true に設定してください。これは、入力ﾌｧｲﾙを一括して読み込みます。"			).append( CR )
			.append( "-ignoreCase は、検索時にｷｰの大文字小文字を無視するように指定します。"			).append( CR )
			.append( "-notEquals は、結果(見つかればtrue)を反転(見つからなければtrue)します。"		).append( CR )
			.append( "これは、行単位ではなく、ﾌｧｲﾙ単位に判定しますので、change 指定した場合"		).append( CR )
			.append( "でも、対象行は、見つかった行です。ただし、下流に対して、見つからない"			).append( CR )
			.append( "場合だけ処理を継続させます。"													).append( CR )
			.append( "-inEncode は、入力ﾌｧｲﾙのｴﾝｺｰﾄﾞ指定になります。"								).append( CR )
			.append( "-outEncode は、出力ﾌｧｲﾙのｴﾝｺｰﾄﾞや、changeFileで指定の置換文字列"				).append( CR )
			.append( "ﾌｧｲﾙのｴﾝｺｰﾄﾞ指定になります。(changeFile は、必ず 出力ﾌｧｲﾙと)"					).append( CR )
			.append( "同じｴﾝｺｰﾄﾞです。"																).append( CR )
			.append( "これらのｴﾝｺｰﾄﾞが無指定の場合は、System.getProperty(\"file.encoding\") "		).append( CR )
			.append( "で求まる値を使用します。"														).append( CR )
			.append( "-changeFile を使用することで、複数行の文字列に置換することが可能です。"		).append( CR )
			.append( CR )
			.append( "上流(ﾌﾟﾛｾｽﾁｪｲﾝのﾃﾞｰﾀは上流から渡されます。)からのLineModel の"				).append( CR )
			.append( "ﾌｧｲﾙｵﾌﾞｼﾞｪｸﾄより、指定の文字列が含まれているか検索します。"					).append( CR )
			.append( "上流ﾌﾟﾛｾｽでは、Name 属性として、『File』を持ち、値は、Fileｵﾌﾞｼﾞｪｸﾄ"			).append( CR )
			.append( "である、Process_FileSearch を使用するのが、便利です。それ以外のｸﾗｽを"			).append( CR )
			.append( "使用する場合でも、Name属性と、File ｵﾌﾞｼﾞｪｸﾄを持つ LineModel を受け渡し"		).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_Grep().usage() );
	}
}
