001/*
002 * Copyright (c) 2009 The openGion Project.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
013 * either express or implied. See the License for the specific language
014 * governing permissions and limitations under the License.
015 */
016package org.opengion.fukurou.process;
017
018import java.util.Map ;
019import java.util.LinkedHashMap ;
020import java.util.List ;
021import java.util.ArrayList ;
022import java.util.Locale ;                               // 5.7.3.2 (2014/02/28) ignoreCase が実装されていなかった。
023import java.util.regex.Pattern;                 // 5.7.3.2 (2014/02/28) regexを利用する場合
024import java.util.regex.Matcher;                 // 5.7.3.2 (2014/02/28) regexを利用する場合
025
026import java.io.File;
027import java.io.PrintWriter;
028import java.io.BufferedReader;
029import java.io.IOException;
030import java.nio.charset.CharacterCodingException;                       // 6.3.1.0 (2015/06/28)
031
032import org.opengion.fukurou.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
033import org.opengion.fukurou.system.Closer ;
034import org.opengion.fukurou.system.LogWriter;
035import org.opengion.fukurou.util.Argument;
036import org.opengion.fukurou.util.FileUtil;
037import org.opengion.fukurou.util.StringUtil ;
038import org.opengion.fukurou.util.CommentLineParser;                     // 8.5.4.2 (2024/01/12)
039
040/**
041 * Process_GrepChange は、上流から受け取った FileLineModelから、語句を
042 * 置換する、ChainProcess インターフェースの実装クラスです。
043 *
044 * Process_Grep との違いは、チェックするファイルのコピーを(キーワードが存在
045 * しなくとも)作成することと、検索キーに正規表現が使えない、複数行置き換えが
046 * 出来ないことです。
047 *
048 * keywordFile より、置換する語句を含むキーと値のペアー(タブ区切り)を読取り、
049 * 対象とする語句を置換します。
050 * keywordFile に、タブが含まれない行や、先頭にタブが存在している場合は、
051 * その行を読み飛ばします。また、区切りタブは何個存在しても構いません。
052 * 置換文字(値)は、\t の特殊文字が使用できます。
053 * この GrepChange では、語句に、正規表現は使用できません。正規表現のキーワード
054 * や文字列を複数行の文字列と置き換える場合は、Process_Grep を使用してください。
055 * このプログラムでは、上流から受け取った FileLineModel のファイルに対して、
056 * <del>6.3.1.1 (2015/07/10) 置き換えた結果も、同じファイルにセーブします。</del>
057 * 元のファイルを保存したい場合は、予めバックアップを取得しておいてください。
058 * -inEncode は、入力ファイルのエンコード指定になります。
059 * -outEncode は、出力ファイルのエンコードや、キーワードファイルの
060 * エンコード指定になります。(keywordFile は、必ず 出力ファイルと同じエンコードです。)
061 * これらのエンコードが無指定の場合は、System.getProperty("file.encoding") で
062 * 求まる値を使用します。
063 *
064 * 5.7.3.2 (2014/02/28)
065 * -regex=true で、キーワードに正規表現を利用できます。具体的には、String#replaceAll(String,String)
066 * を利用して置換します。
067 * 通常の置換処理は、indexOf で見つけて、StringBuilder#replace(int,int,String) を繰り返して処理しています。
068 * -ignoreCase=true で、検索キーワードに大文字小文字を区別しない処理が可能です。
069 *
070 * 6.3.1.1 (2015/07/10)
071 * ※ 出力ファイルを別フォルダにコピー置換する機能を追加します。
072 *    方法は、Process_FileCopy と同様、inPath と outPath を指定します。
073 * ※ useWordUnit="true" を指定すると、出来るだけ、Grep対象を、単語単位で置換しようとします。
074 *    具体的には、キーワード文字列の前後に、""(ダブルクオート)、''(シングルクオート)、><(タグ記号)、空白、改行を
075 *    付加して、それらを含めてマッチした場合のみ置換する方法を取ります。
076 *
077 * ※ 8.5.4.2 (2024/01/12) useOmitCmnt コメント除外追加
078 *      コメント除外は、isChange=false(置換しない)場合にのみ有効にします。
079 *
080 * 上流プロセスでは、Name 属性として、『File』を持ち、値は、Fileオブジェクト
081 * である、Process_FileSearch を使用するのが、便利です。それ以外のクラスを
082 * 使用する場合でも、Name属性と、File オブジェクトを持つ LineModel を受け渡し
083 * できれば、使用可能です。
084 *
085 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
086 * 引数文字列の 『=』 の前後には、スペースは挟めません。必ず、-key=value の様に
087 * 繋げてください。
088 *
089 *  Process_GrepChange -keyword=検索文字列 -ignoreCase=true -outfile=OUTFILE -encode=UTF-8
090 *
091 *    -keywordFile=キーワード      :置換する語句を含むキーと値のペアー(タブ区切り)
092 *   [-ignoreCase=[false/true]   ] :検索時に大文字小文字を区別しない(true)かどうか(初期値:false[区別する])
093 *   [-regex=[false/true]        ] :キーワードに正規表現を利用する(true)かどうか(初期値:false[利用しない])
094 *   [-isChange=置換可否         ] :置換処理を実施する(true)かどうか(初期値:置換する[true])
095 *   [-useOmitCmnt=[false/true]  ] :コメント部分を削除したファイルでgrep処理を行うかどうかを指定(初期値:false)
096 *   [-inPath=入力共通パス       ] :上流で検索されたファイルパスの共通部分
097 *   [-inEncode=入力エンコード   ] :入力ファイルのエンコードタイプ
098 *   [-outEncode=出力エンコード  ] :出力ファイルやキーワードファイルのエンコードタイプ
099 *   [-outPath=出力共通パス      ] :出力するファイルパスの共通部分
100 *   [-useWordUnit=単語単位置換  ] :出来るだけ、Grep対象を、単語単位で置換しようとします(初期値:false[部分置換])
101 *   [-errAbend=[true/false]     ] :異常発生時に、処理を中断(true)するか、継続(false)するかを指定する(初期値:true[中断する])
102 *   [-display=[false/true]      ] :結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
103 *   [-debug=[false/true]        ] :デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
104 *
105 * @version  4.0
106 * @author   Kazuhiko Hasegawa
107 * @since    JDK5.0,
108 */
109public class Process_GrepChange extends AbstractProcess implements ChainProcess {
110        /** 6.3.1.1 (2015/07/10) useWordUnit="true" 時に、使用することになります。 */
111        private static final String IN_PTN = "([\"'><\\\t\\\n  ])";     // 6.3.1.1 (2015/07/10) ほとんど同じなので、共有する。
112
113        private String[]        keyword ;
114        private String[]        change  ;
115        /** 5.7.3.2 (2014/02/28) キーワードに正規表現を利用する場合 */
116        private Pattern[]       pattern ;
117        private boolean         ignoreCase      ;
118        /** 5.7.3.2 (2014/02/28) キーワードに正規表現を利用するかどうか */
119        private boolean         regex           ;
120        /** 5.1.2.0 (2010/01/01) 置換するかどうかを指定可能にする */
121        private boolean         isChange        = true;
122        /** 8.5.4.2 (2024/01/12) コメント除外 */
123        private boolean         useOmitCmnt;
124        private String          inEncode        ;
125        private String          outPath         ;                       // 6.3.1.1 (2015/07/10)
126        private String          outEncode       ;
127        /** 6.3.1.1 (2015/07/10) 部分置換 */
128        private boolean         useWordUnit     ;
129        /** 6.3.1.1 (2015/07/10) 中断する */
130        private boolean         errAbend        = true;
131        /** false:表示しない */
132        private boolean         display         ;
133        /** 5.7.3.0 (2014/02/07) デバッグ情報 */
134        private boolean         debug           ;
135
136        private int             inPathLen               ;                       // 6.3.1.1 (2015/07/10)
137        private boolean isEquals                ;                       // 6.3.1.1 (2015/07/10)
138
139        private int             inCount         ;
140        private int             findCount       ;
141        private int             cngCount        ;
142
143        /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
144        private static final Map<String,String> MUST_PROPARTY   ;               // [プロパティ]必須チェック用 Map
145        /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
146        private static final Map<String,String> USABLE_PROPARTY ;               // [プロパティ]整合性チェック Map
147
148        static {
149                MUST_PROPARTY = new LinkedHashMap<>();
150                MUST_PROPARTY.put( "keywordFile",       "置換する語句を含むキーと値のペアー(タブ区切り)(必須)" );
151
152                USABLE_PROPARTY = new LinkedHashMap<>();
153                USABLE_PROPARTY.put( "ignoreCase",      "検索時に大文字小文字を区別しない(true)かどうか。" +
154                                                                                CR + "(初期値:区別する[false])" );
155                USABLE_PROPARTY.put( "regex",           "キーワードに正規表現を利用する(true)かどうか。" +
156                                                                                CR + "(初期値:利用しない[false])" );    // 5.7.3.2 (2014/02/28)
157                USABLE_PROPARTY.put( "isChange",                "置換処理を実施する(true)かどうか" +
158                                                                                CR + "(初期値:置換する[true])" );
159                USABLE_PROPARTY.put( "useOmitCmnt",     "コメント部分を削除したファイルでgrep処理を行うかどうかを指定(初期値:false)" );                // 8.5.4.2 (2024/01/12)
160                USABLE_PROPARTY.put( "inPath",          "入力するファイルパスの共通部分" );    // 6.3.1.1 (2015/07/10)
161                USABLE_PROPARTY.put( "inEncode",        "入力ファイルのエンコードタイプ" );
162                USABLE_PROPARTY.put( "outPath",         "出力するファイルパスの共通部分" );    // 6.3.1.1 (2015/07/10)
163                USABLE_PROPARTY.put( "outEncode",       "出力ファイルやキーワードファイルのエンコードタイプ" );
164                USABLE_PROPARTY.put( "useWordUnit",     "出来るだけ、Grep対象を、単語単位で置換" +
165                                                                                CR + "(初期値:false:部分置換)" );                      // 6.3.1.1 (2015/07/10)
166                USABLE_PROPARTY.put( "errAbend",        "異常発生時に、処理を中断(true)するか、継続(false)するか" +
167                                                                                CR + "(初期値:true:中断する)" );                       // 6.3.1.0 (2015/06/28)
168                USABLE_PROPARTY.put( "display",         "結果を標準出力に表示する(true)かしない(false)か" +
169                                                                                CR + "(初期値:false:表示しない)" );
170                USABLE_PROPARTY.put( "debug",   "デバッグ情報を標準出力に表示する(true)かしない(false)か" +
171                                                                                CR + "(初期値:false:表示しない)" );             // 5.7.3.0 (2014/02/07) デバッグ情報
172        }
173
174        /**
175         * デフォルトコンストラクター。
176         * このクラスは、動的作成されます。デフォルトコンストラクターで、
177         * super クラスに対して、必要な初期化を行っておきます。
178         *
179         */
180        public Process_GrepChange() {
181                super( "org.opengion.fukurou.process.Process_GrepChange",MUST_PROPARTY,USABLE_PROPARTY );
182        }
183
184        /**
185         * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
186         * 初期処理(ファイルオープン、DBオープン等)に使用します。
187         *
188         * @og.rev 5.1.2.0 (2010/01/01) 置換するかどうかを指定可能にする(isChange)属性追加
189         * @og.rev 5.7.3.2 (2014/02/28) debug の表示と、キーワードの \t の使用、trim() 廃止、ignoreCase の実装、regex の追加
190         * @og.rev 6.3.1.1 (2015/07/10) 出力ファイルを別フォルダにコピー置換する機能を追加
191         * @og.rev 6.3.1.1 (2015/07/10) useWordUnit="true" 時は、出来るだけ、Grep対象を、単語単位で置換しようとします
192         * @og.rev 6.4.5.1 (2016/04/28) FileStringのコンストラクター変更
193         * @og.rev 6.4.5.2 (2016/05/06) fukurou.util.FileString から、fukurou.util.FileUtil に移動。
194         * @og.rev 8.5.4.2 (2024/01/12) useOmitCmnt コメント除外追加
195         *
196         * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
197         */
198        public void init( final ParamProcess paramProcess ) {
199                final Argument arg = getArgument();
200
201                final String keywordFile = arg.getProparty( "keywordFile" );
202                ignoreCase      = arg.getProparty( "ignoreCase" , ignoreCase            );
203                regex           = arg.getProparty( "regex"              , regex                         );              // 5.7.3.2 (2014/02/28)
204                isChange        = arg.getProparty( "isChange"   , isChange                      );              // 5.1.2.0 (2010/01/01)
205                useOmitCmnt = arg.getProparty( "useOmitCmnt", useOmitCmnt               );              // 8.5.4.2 (2024/01/12)
206                inEncode        = arg.getProparty( "inEncode"   , System.getProperty("file.encoding"));
207                outPath         = arg.getProparty( "outPath"    , null                          );              // 6.3.1.1 (2015/07/10)
208                outEncode       = arg.getProparty( "outEncode"  , System.getProperty("file.encoding"));
209                useWordUnit     = arg.getProparty( "useWordUnit", useWordUnit           );              // 6.3.1.1 (2015/07/10)
210                errAbend        = arg.getProparty( "errAbend"   , errAbend                      );              // 6.3.1.1 (2015/07/10)
211                display         = arg.getProparty( "display"    , display                       );
212                debug           = arg.getProparty( "debug"              , debug                         );              // 5.7.3.0 (2014/02/07) デバッグ情報
213
214                // 6.3.1.1 (2015/07/10) 入力と出力が同じか?
215                final String inPath = arg.getProparty( "inPath" , null );       // 6.3.4.0 (2015/08/01)
216                isEquals  = outPath == null || inPath == null || inPath.equalsIgnoreCase( outPath );
217                inPathLen = inPath == null ? 0 : inPath.length();
218
219                // 6.4.5.1 (2016/04/28) FileStringのコンストラクター変更
220                final List<String> list = FileUtil.getLineList( keywordFile , outEncode );      // 6.4.5.2 (2016/05/06)
221                final int len = list.size();                                                            // 6.4.5.2 (2016/05/06)
222                if( len == 0 ) {
223                        // これは、初期情報取込み処理なので、errAbend 対象外
224                        final String errMsg = "keywordFile の内容が 読み取れませんでした。[" + keywordFile + "]" ;
225                        throw new OgRuntimeException( errMsg );
226                }
227
228                println( "keywordFile を、" + len + "件読み取りました。" );
229                final List<String> keyList = new ArrayList<>( len );
230                final List<String> cngList = new ArrayList<>( len );
231
232                for( final String line : list ) {
233        //              String line = lines[i].trim();
234                        final int indx = line.indexOf( '\t' );
235                        if( indx <= 0 ) { continue ; }  // TAB が先頭や、存在しない行は読み飛ばす。
236                        // 5.7.3.2 (2014/02/28) debug の表示と、キーワードの \t の使用、trim() 廃止
237                        String key = line.substring( 0,indx );
238                        String cng = line.substring( indx+1 );
239
240                        if( ignoreCase ) { key = key.toUpperCase(Locale.JAPAN); }       // 5.7.3.2 (2014/02/28) ignoreCase の実装漏れ
241
242                        if( debug ) { println( "[" + key + "]⇒[" + cng + "]" ); }
243
244                        key = StringUtil.replace( key,"\\t","\t" );
245                        cng = StringUtil.replace( cng,"\\t","\t" );
246
247                        keyList.add( key );
248                        cngList.add( cng );
249                }
250//              keyword = keyList.toArray( new String[keyList.size()] );
251//              change  = cngList.toArray( new String[cngList.size()] );
252                keyword = keyList.toArray( new String[0] );     // 8.5.4.2 (2024/01/12) PMD 7.0.0 OptimizableToArrayCall 対応
253                change  = cngList.toArray( new String[0] );     // 8.5.4.2 (2024/01/12) PMD 7.0.0 OptimizableToArrayCall 対応
254
255                // 5.7.3.2 (2014/02/28) regex=true の場合の処理
256                if( regex ) {
257                        pattern = new Pattern[keyword.length];
258                        for( int i=0; i<keyword.length; i++ ) {
259                                // 6.3.1.1 (2015/07/10) useWordUnit="true" 時は、出来るだけ、Grep対象を、単語単位で置換しようとします
260                                final String keywd = useWordUnit ? ( IN_PTN + keyword[i] + IN_PTN ) : keyword[i] ;
261                                pattern[i] = ignoreCase ? Pattern.compile( keywd , Pattern.CASE_INSENSITIVE )
262                                                                                : Pattern.compile( keywd ) ;
263                        }
264                }
265        }
266
267        /**
268         * プロセスの終了を行います。最後に一度だけ、呼び出されます。
269         * 終了処理(ファイルクローズ、DBクローズ等)に使用します。
270         *
271         * @param   isOK トータルで、OKだったかどうか[true:成功/false:失敗]
272         */
273        public void end( final boolean isOK ) {
274                // ここでは処理を行いません。
275        }
276
277        /**
278         * 引数の LineModel を処理するメソッドです。
279         * 変換処理後の LineModel を返します。
280         * 後続処理を行わない場合(データのフィルタリングを行う場合)は、
281         * null データを返します。つまり、null データは、後続処理を行わない
282         * フラグの代わりにも使用しています。
283         * なお、変換処理後の LineModel と、オリジナルの LineModel が、
284         * 同一か、コピー(クローン)かは、各処理メソッド内で決めています。
285         * ドキュメントに明記されていない場合は、副作用が問題になる場合は、
286         * 各処理ごとに自分でコピー(クローン)して下さい。
287         *
288         * @og.rev 4.0.0.0 (2007/11/28) メソッドの戻り値をチェックします。
289         * @og.rev 5.1.2.0 (2010/01/01) 置換するかどうかを指定可能にする(isChange)属性追加
290         * @og.rev 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
291         * @og.rev 5.7.3.2 (2014/02/28) debug の表示と、ignoreCase の実装
292         * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
293         * @og.rev 6.3.1.1 (2015/07/10) 出力ファイルを別フォルダにコピー置換する機能を追加
294         * @og.rev 6.3.1.1 (2015/07/10) useWordUnit="true" 時は、出来るだけ、Grep対象を、単語単位で置換しようとします
295         *
296         * @param       data    オリジナルのLineModel
297         *
298         * @return      処理変換後のLineModel
299         */
300        @Override       // ChainProcess
301        public LineModel action( final LineModel data ) {
302                inCount++ ;
303                final FileLineModel fileData ;
304                if( data instanceof FileLineModel ) {
305                        fileData = (FileLineModel)data ;
306                }
307                else {
308                        // これは、プログラマーの問題なので、errAbend 対象外
309                        final String errMsg = "データが FileLineModel オブジェクトではありません。" + CR ;
310                        throw new OgRuntimeException( errMsg );
311                }
312
313                final File org = fileData.getFile() ;
314                if( ! org.isFile() ) { return data; }
315
316                if( debug ) { println( "File:" + org ); }               // 5.1.2.0 (2010/01/01) display の条件変更
317
318                File            tempFile  = null;
319                PrintWriter     tempWrt   = null;
320
321                // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point.
322                final String orgName = org.getPath();
323
324                // 5.1.2.0 (2010/01/01) 置換する場合の前処理
325                if( isChange ) {
326                        // 6.3.1.1 (2015/07/10) 出力が同じ場合は、従来通り temp出力して、置き換える。
327                        if( isEquals ) {
328                                tempFile  = new File( orgName + "_temp" );
329                        }
330                        else {
331                                // 入出力が異なる場合
332                                tempFile  = new File( outPath, org.getAbsolutePath().substring( inPathLen ) );
333                                fileData.setFile( tempFile );   // tempFile は、出力ファイルの事。
334                                // 出力先のフォルダが無ければ作成
335                                final File parent = tempFile.getParentFile();
336                                if( parent != null && ! parent.exists() && !parent.mkdirs() ) {
337                                        final String errMsg = "所定のフォルダが作成できませんでした。[" + parent + "]" + CR
338                                                                + " inCount=[" + inCount + "]件" + CR
339                                                                + " data=[" + data.dataLine() + "]" + CR ;              // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
340                                        throwException( errMsg,errAbend );
341                                        return null;    // ログだけの場合は、以下の処理は打ち切り。
342                                }
343                        }
344
345                        tempWrt = FileUtil.getPrintWriter( tempFile,outEncode );
346                }
347
348                boolean nextFlag = false;
349
350                // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid instantiating new objects inside loops
351                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
352
353                final BufferedReader reader = FileUtil.getBufferedReader( org,inEncode );
354                final CommentLineParser clp = useOmitCmnt ? new CommentLineParser( "jsp" ) : null;      // 8.5.4.2 (2024/01/12)
355                try {
356                        String line ;
357                        int lineNo = 0;
358                        while((line = reader.readLine()) != null) {
359                                lineNo++ ;
360                                // 8.5.4.2 (2024/01/12) useOmitCmnt 機能(コメント行を削除する処理を入れる)
361                                if( useOmitCmnt ) {
362                                        line = clp.line( line );
363                                        if( line == null ) { continue; }                        // 戻り値が null の場合は、行として不成立
364                                }
365
366                                // 5.7.3.2 (2014/02/28) regex 対応
367                                if( regex ) {
368                                        for( int i=0; i<pattern.length; i++ ) {
369                                                final Matcher mt = pattern[i].matcher( line );
370                                                if( mt.matches() ) {
371                                                        nextFlag = true;                        // 1度でも見つかれば、true にセット
372                                                        findCount++ ;
373                                                        if( display ) { println( orgName + ":" + lineNo + ":" + keyword[i] + ":" + line ); }
374                                                        if( isChange ) {
375                                                                // 6.3.1.1 (2015/07/10) useWordUnit="true" 時は、出来るだけ、Grep対象を、単語単位で置換しようとします
376                                                                line = mt.replaceAll( "$1" + change[i] + "$2" );        // 前方参照
377                                                                cngCount++ ;
378                                                        }
379                                                }
380                                        }
381                                }
382                                else {
383                                        final String line2 = ignoreCase ? line.toUpperCase(Locale.JAPAN) : line ;               //  6.4.1.1 (2016/01/16) 先に、必要な処理を行う。
384                                        buf.setLength(0);                               // new StringBuilder の代わり。
385                                        buf.append( line );
386                                        for( int i=0; i<keyword.length; i++ ) {
387                                                int indx = line2.indexOf( keyword[i] );
388                                                // 置換対象発見。行出力用に見つかれば、true にする。
389                                                if( indx >= 0 ) {
390                                                        // 6.3.1.1 (2015/07/10) useWordUnit="true" 時は、出来るだけ、Grep対象を、単語単位で置換しようとします
391                                                        // 検索結果の前後の文字が、IN_PTN に含まれている場合のみ、見つかったことにする。
392                                                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 CollapsibleIfStatements
393//                                                      if( useWordUnit ) {
394//                                                              // 見つかった場所のひとつ前の文字が、IN_PTN に存在しなければ、見つからなかった。
395//                                                              if( indx > 0 && IN_PTN.indexOf( line.charAt( indx-1 ) ) < 0 ||
396//                                                                      // 見つかった場所のひとつ後ろの文字が、IN_PTN に存在しなければ、見つからなかった。
397//                                                                      line.length() < (indx+1) && IN_PTN.indexOf( line.charAt( indx+1 ) ) < 0 ) {
398                                                        if( useWordUnit
399                                                                // 見つかった場所のひとつ前の文字が、IN_PTN に存在しなければ、見つからなかった。
400                                                                && ( indx > 0 && IN_PTN.indexOf( line.charAt( indx-1 ) ) < 0 ||
401                                                                        // 見つかった場所のひとつ後ろの文字が、IN_PTN に存在しなければ、見つからなかった。
402                                                                        line.length() < (indx+1) && IN_PTN.indexOf( line.charAt( indx+1 ) ) < 0 ) ) {
403                                                                                // 対象外になったキーワードと行を表示します。
404                                                                                if( display ) { println( "NoChange:" + orgName + ":" + lineNo + ":" + keyword[i] + ":" + line ); }
405                                                                                continue;
406//                                                              }
407                                                        }
408                                                        nextFlag  = true;                               // 1度でも見つかれば、true にセット
409                                                        if( display ) { println( orgName + ":" + lineNo + ":" + keyword[i] + ":" + line ); }
410                                                        findCount++ ;
411
412                                                        // 6.4.1.1 (2016/01/16) 見つかったときだけ、置換処理を実施するように変更。
413                                                        // 置換対象が見つかっても、isChange=true でなければ、置換処理は行わない。
414                                                        if( isChange ) {
415                                                                while( indx >= 0 ) {
416                                                                        buf.replace( indx,indx+keyword[i].length(),change[i] );
417                                                                        // 5.7.3.2 (2014/02/28) ignoreCase 対応。
418                                                                        final int nxt = indx+change[i].length();
419                                                                        indx = ignoreCase       ? buf.toString().toUpperCase(Locale.JAPAN).indexOf( keyword[i],nxt )
420                                                                                                                : buf.indexOf( keyword[i],nxt );
421
422                                                                        cngCount++ ;
423                                                                }
424                                                        }
425                                                }
426                                        }
427                                        line = buf.toString();
428                                }
429                                // 5.1.2.0 (2010/01/01) 置換する場合の処理
430                                if( isChange ) {
431                                        tempWrt.println( line );                                // 5.7.3.2 (2014/02/28) regexで出力を共有する為。
432                                }
433                        }
434                }
435                // 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
436                catch( final CharacterCodingException ex ) {
437                        final String errMsg = "文字のエンコード・エラーが発生しました。" + CR
438                                                                +       "  ファイルのエンコードが指定のエンコードと異なります。" + CR
439                                                                +       " [" + org + "] , Encode=[" + inEncode + "]" ;
440                        throwException( errMsg,ex,errAbend );
441                        return null;    // ログだけの場合は、以下の処理は打ち切り。
442                }
443                catch( final IOException ex ) {
444                        final String errMsg = "処理中にエラーが発生しました。" + CR
445                                                + " [" + org + "] , Encode=[" + inEncode + "]" + CR
446                                                + " [" + data.getRowNo() + "]件目" + CR
447                                                + " data=[" + data.dataLine() + "]" + CR ;              // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
448                        throwException( errMsg,ex,errAbend );
449                        return null;    // ログだけの場合は、以下の処理は打ち切り。
450                }
451                finally {
452                        Closer.ioClose( reader );
453                        Closer.ioClose( tempWrt );
454                }
455
456                // 5.1.2.0 (2010/01/01) 置換する場合の処理
457                if( isChange && isEquals ) {            // 6.3.1.1 (2015/07/10) 出力が同じ場合の時のみ、後処理が必要。
458                        if( nextFlag ) {
459                                if( !org.delete() ) {
460                                        final String errMsg = "所定のファイルを削除できませんでした。[" + org + "]" + CR
461                                                        +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
462                                        throwException( errMsg,errAbend );
463                                }
464                                if( !tempFile.renameTo( org ) ) {
465                                        final String errMsg = "所定のファイルをリネームできませんでした。[" + tempFile + "]" + CR
466                                                        +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
467                                        throwException( errMsg,errAbend );
468                                }
469                        }
470                        else {
471                                if( !tempFile.delete() ) {
472                                        final String errMsg = "所定のファイルを削除できませんでした。[" + tempFile + "]" + CR
473                                                        +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
474                                        throwException( errMsg,errAbend );
475                                }
476                        }
477                }
478
479                return nextFlag ? data : null ;
480        }
481
482        /**
483         * プロセスの処理結果のレポート表現を返します。
484         * 処理プログラム名、入力件数、出力件数などの情報です。
485         * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
486         * 形式で出してください。
487         *
488         * @return   処理結果のレポート
489         */
490        public String report() {
491                // 7.2.9.5 (2020/11/28) PMD:Consider simply returning the value vs storing it in local variable 'XXXX'
492                return "[" + getClass().getName() + "]" + CR
493//              final String report = "[" + getClass().getName() + "]" + CR
494                                + TAB + "Search File Count : " + inCount    + CR
495                                + TAB + "Key Find    Count : " + findCount  + CR
496                                + TAB + "Key Change  Count : " + cngCount ;
497
498//              return report ;
499        }
500
501        /**
502         * このクラスの使用方法を返します。
503         *
504         * @return      このクラスの使用方法
505         * @og.rtnNotNull
506         */
507        public String usage() {
508                final StringBuilder buf = new StringBuilder( 1200 )
509                        .append( "Process_GrepChange は、上流から受け取った FileLineModelから、語句を"                   ).append( CR )
510                        .append( "置換する、ChainProcess インターフェースの実装クラスです。"                                  ).append( CR )
511                        .append( "Process_Grep との違いは、チェックするファイルのコピーを(キーワードが存在"          ).append( CR )
512                        .append( "しなくとも)作成することと、検索キーに正規表現が使えない、複数行置き換えが"        ).append( CR )
513                        .append( "出来ないことです。"                                                                                                                    ).append( CR )
514                        .append( CR )
515                        .append( "keywordFile より、置換する語句を含むキーと値のペアー(タブ区切り)を読取り、" ).append( CR )
516                        .append( "対象とする語句を置換します。"                                                                                                       ).append( CR )
517                        .append( "keywordFile に、タブが含まれない行や、先頭にタブが存在している場合は、"            ).append( CR )
518                        .append( "その行を読み飛ばします。また、区切りタブは何個存在しても構いません。"                   ).append( CR )
519                        .append( "ただし、タブで区切った前(キー)と後ろ(値)は、trim() されますので、スペース"   ).append( CR )
520                        .append( "が前後に存在している場合は、ご注意ください。"                                                                       ).append( CR )
521                        .append( "置換文字(値)は、\t と \n の特殊文字が使用できます。"                                                       ).append( CR )
522                        .append( "この GrepChange では、語句に、正規表現は使用できません。正規表現のキーワード" ).append( CR )
523                        .append( "や文字列を複数行の文字列と置き換える場合は、Process_Grep を使用して下さい。" ).append( CR )
524                        .append( "このプログラムでは、上流から受け取った FileLineModel のファイルに対して、" ).append( CR )
525                        .append( "置き換えた結果も、同じファイルにセーブします。"                                                              ).append( CR )
526                        .append( "元のファイルを保存したい場合は、予めバックアップを取得しておいてください。"        ).append( CR )
527                        .append( "-inEncode は、入力ファイルのエンコード指定になります。"                                             ).append( CR )
528                        .append( "-outEncode は、出力ファイルのエンコードや、キーワードファイルのエンコード"   ).append( CR )
529                        .append( "指定になります。(keywordFile は、必ず 出力ファイルと同じエンコードです。)" ).append( CR )
530                        .append( "これらのエンコードが無指定の場合は、System.getProperty(\"file.encoding\")"      ).append( CR )
531                        .append( "で求まる値を使用します。"                                                                                                         ).append( CR )
532                        .append( CR )
533//                      .append( "上流プロセスでは、Name 属性として、『File』を持ち、値は、Fileオブジェクト"  ).append( CR )
534//                      .append( "である、Process_FileSearch を使用するのが、便利です。それ以外のクラスを"                ).append( CR )
535//                      .append( "使用する場合でも、Name属性と、File オブジェクトを持つ LineModel を受け渡し"      ).append( CR )
536//                      .append( "できれば、使用可能です。"                                                                                                         ).append( CR )
537                        .append( CHAIN_FILE_USAGE )             // 8.5.6.1 (2024/03/29) 継承元使用
538                        .append( CR )
539//                      .append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。"    ).append( CR )
540//                      .append( "引数文字列の 『=』 の前後には、空白は挟めません。必ず、-key=value の様に"          ).append( CR )
541//                      .append( "繋げてください。"                                                                                                                             ).append( CR )
542                        .append( PROCESS_PARAM_USAGE )  // 8.5.6.1 (2024/03/29) 継承元使用
543                        .append( CR ).append( CR )
544                        .append( getArgument().usage() ).append( CR );
545
546                return buf.toString();
547        }
548
549        /**
550         * このクラスは、main メソッドから実行できません。
551         *
552         * @param       args    コマンド引数配列
553         */
554        public static void main( final String[] args ) {
555                LogWriter.log( new Process_GrepChange().usage() );
556        }
557}