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 org.opengion.fukurou.util.Argument;
019import org.opengion.fukurou.util.FileUtil;
020import org.opengion.fukurou.util.FileString;
021import org.opengion.fukurou.util.Closer ;
022import org.opengion.fukurou.util.StringUtil ;
023import org.opengion.fukurou.util.LogWriter;
024
025import java.util.Arrays;
026import java.util.Map ;
027import java.util.LinkedHashMap ;
028import java.util.regex.Pattern;
029import java.util.regex.Matcher;
030
031import java.io.File;
032import java.io.PrintWriter;
033import java.io.BufferedReader;
034import java.io.IOException;
035
036/**
037 * Process_Grep は、上流から受け取った FileLineModelから、文字列を見つけ出す
038 * ChainProcess インターフェースの実装クラスです。
039 *
040 * 正規表現の keyword を上流から受け取った FileLineModel から検索します。
041 * 見つかった対象ファイルから、指定の文字列を置換する場合は、-change か
042 * -changeFile で、keyword を置換する文字列を指定して下さい。
043 * 置換する文字列には、\t と \n の特殊文字が使用できます。
044 *
045 * 処理対象は、通常は、1行づつ読み取りながら処理を行います。存在チェックの場合は、
046 * 見つかった時点で処理を中止します。これは、該当箇所をピックアップするのではなく、
047 * 存在しているかどうかを判断して、あれば、下流に流すというのが目的だからです。
048 * keyword を、改行を含む正規表現で、検索・置換する場合は、-useBulkRead 属性を
049 * true に設定してください。これは、入力ファイルを一括して読み込みます。
050 * -ignoreCase は、正規表現の検索時にキーの大文字小文字を無視するように指定します。
051 * -notEquals は、結果(見つかればtrue)を反転(見つからなければtrue)します。
052 * これは、行単位ではなく、ファイル単位に判定しますので、change 指定した場合
053 * でも、対象行は、見つかった行です。ただし、下流に対して、見つからない
054 * 場合だけ処理を継続させます。
055 * -inEncode は、入力ファイルのエンコード指定になります。
056 * -outEncode は、出力ファイルのエンコードや、changeFileで指定の置換文字列ファイルの
057 * エンコード指定になります。(changeFile は、必ず 出力ファイルと同じエンコードです。)
058 * これらのエンコードが無指定の場合は、System.getProperty("file.encoding") で
059 * 求まる値を使用します。
060 * -changeFile を使用することで、複数行の文字列に置換することが可能です。
061 * -outfile では、処理を行ったファイル名一覧をセーブします。
062 *
063 * 上流(プロセスチェインのデータは上流から渡されます。)からのLineModel の
064 * ファイルオブジェクトより、指定の文字列が含まれているか検索します。
065 * 上流プロセスでは、Name 属性として、『File』を持ち、値は、Fileオブジェクト
066 * である、Process_FileSearch を使用するのが、便利です。それ以外のクラスを
067 * 使用する場合でも、Name属性と、File オブジェクトを持つ LineModel を受け渡し
068 * できれば、使用可能です。
069 *
070 * 引数文字列中に空白を含む場合は、ダブルコーテーション("") で括って下さい。
071 * 引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に
072 * 繋げてください。
073 *
074 * @og.formSample
075 *  Process_Grep -keyword=検索文字列 -ignoreCase=true -outfile=OUTFILE -encode=UTF-8
076 *
077 *    -keyword=キーワード        :検索する語句
078 *   [-ignoreCase=大文字小文字 ] :検索時に大文字小文字を区別しない(true)かどうか(初期値:区別する[false])
079 *   [-notEquals=判定結果の反転] :判定結果を反転させる(true)かどうか(初期値:反転させない[false])
080 *   [-inEncode=入力エンコード ] :入力ファイルのエンコードタイプ
081 *   [-outEncode=出力エンコード] :出力ファイルや置換ファイルのエンコードタイプ
082 *   [-change=置換文字列       ] :-change="ABCD" \t や \n などの特殊文字が使用できます。
083 *   [-changeFile=置換ファイル ] :-changeFile=change.txt このファイルの記述すべてと置換します。
084 *                                     -change と、-changeFile は、同時に指定できません。
085 *                                     置換機能使用時は、必ず、_backup というファイルが作成されます。
086 *   [-insert=[CHANGE/BEFORE/AFTER]   ] : 置換でなく挿入する場合の位置を指定します(初期値:CHANGE)
087 *                                 スペースで区切って数字を記述すると、挿入位置にオフセットできます。
088 *   [-delete=[false/true]     ] : 置換でなく削除します(初期値:false)
089 *   [-useBackup=[false/true]  ] :trueは、backupファイルを作成します(初期値:false)
090 *   [-useBulkRead=[false/true]] :trueは、入力ファイルを一括読込します(初期値:false)
091 *   [-display=[false/true]    ] :trueは、検索状況を表示します(初期値:false)
092 *   [-debug=[false/true]      ] :デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
093 *
094 * @version  4.0
095 * @author   Kazuhiko Hasegawa
096 * @since    JDK5.0,
097 */
098public class Process_Grep extends AbstractProcess implements ChainProcess {
099        private static final String[] INSERT_LIST = new String[] { "CHANGE","BEFORE","AFTER" };
100
101        private Pattern pattern         = null;
102        private String  keyword         = null;
103        private boolean ignoreCase      = false;
104        private boolean notEquals       = false;
105        private String  inEncode        = null;
106        private String  outEncode       = null;
107        private String  change          = null;
108        private String  insert          = "CHANGE";             // "CHANGE","BEFORE","AFTER" のどれか
109        private int             insOffset       = 0;                    // "BEFORE","AFTER" 時のオフセット
110        private boolean useBackup       = false;
111        private boolean useBulkRead     = false;                // 4.0.1.0 (2007/12/14)
112        private boolean delete          = false;
113        private boolean display         = false;
114        private boolean debug           = false;                // 5.1.2.0 (2010/01/01)
115
116        private int             inCount         = 0;
117        private int             findCount       = 0;
118        private int             cngCount        = 0;
119
120        private static final Map<String,String> mustProparty   ;                // [プロパティ]必須チェック用 Map
121        private static final Map<String,String> usableProparty ;                // [プロパティ]整合性チェック Map
122
123        static {
124                mustProparty = new LinkedHashMap<String,String>();
125                mustProparty.put( "keyword",    "検索する語句(必須)" );
126
127                usableProparty = new LinkedHashMap<String,String>();
128                usableProparty.put( "ignoreCase",       "検索時に大文字小文字を区別しない(true)かどうか。" +
129                                                                                CR + "(初期値:区別する[false])" );
130                usableProparty.put( "notEquals",        "検索時に判定結果を反転させる(true)かどうか。" +
131                                                                                CR + "(初期値:反転させない[false])" );
132                usableProparty.put( "inEncode",         "入力ファイルのエンコードタイプ" );
133                usableProparty.put( "outEncode",        "出力ファイルや置換ファイルのエンコードタイプ" );
134                usableProparty.put( "change",           "置換文字列 例: -change=\"ABCD\" \\t や \\n などの特殊文字が使用できます。" );
135                usableProparty.put( "changeFile",       "置換文字列ファイル 例: -changeFile=change.txt" +
136                                                                                CR + "-change と、-changeFile は、同時に指定できません。" +
137                                                                                CR + "置換機能使用時は、必ず、_backup というファイルが作成されます。" );
138                usableProparty.put( "insert",           "[CHANGE/BEFORE/AFTER]:置換でなく挿入する場合の位置を指定します(初期値:CHANGE)"  +
139                                                                                CR + "スペースで区切って数字を記述すると、挿入位置にオフセットできます。" );
140                usableProparty.put( "delete",           "[false/true]:trueは、置換でなく削除します(初期値:false)" );
141                usableProparty.put( "useBackup",        "[false/true]:trueは、backupファイルを作成します(初期値:false)" );
142                usableProparty.put( "useBulkRead",      "[false/true]:trueは、入力ファイルを一括読込します(初期値:false)" );
143                usableProparty.put( "display",          "[false/true]:trueは、検索状況を表示します(初期値:false)" );
144                usableProparty.put( "debug",            "デバッグ情報を標準出力に表示する(true)かしない(false)か" +
145                                                                                        CR + "(初期値:false:表示しない)" );
146        }
147
148        /**
149         * デフォルトコンストラクター。
150         * このクラスは、動的作成されます。デフォルトコンストラクターで、
151         * super クラスに対して、必要な初期化を行っておきます。
152         *
153         */
154        public Process_Grep() {
155                super( "org.opengion.fukurou.process.Process_Grep",mustProparty,usableProparty );
156        }
157
158        /**
159         * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
160         * 初期処理(ファイルオープン、DBオープン等)に使用します。
161         *
162         * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
163         */
164        public void init( final ParamProcess paramProcess ) {
165                Argument arg = getArgument();
166
167                keyword                 = arg.getProparty("keyword");
168                ignoreCase              = arg.getProparty("ignoreCase"  ,ignoreCase);
169                notEquals               = arg.getProparty("notEquals"   ,notEquals);
170                inEncode                = arg.getProparty("inEncode"    ,System.getProperty("file.encoding"));
171                outEncode               = arg.getProparty("outEncode"   ,System.getProperty("file.encoding"));
172                useBackup               = arg.getProparty("useBackup"   ,useBackup);
173                useBulkRead             = arg.getProparty("useBulkRead" ,useBulkRead);  // 4.0.1.0 (2007/12/14)
174                delete                  = arg.getProparty("delete"              ,delete );
175                insert                  = arg.getProparty("insert" ,insert );
176                change                  = arg.getFileProparty( "change","changeFile",outEncode,false );
177                display                 = arg.getProparty("display"             ,display);
178                debug                   = arg.getProparty( "debug"  ,debug );           // 5.1.2.0 (2010/01/01)
179
180                if( change != null ) {
181                        int adrs = insert.indexOf( ' ' );       // オフセット数字の有無
182                        if( adrs > 0 ) {
183                                insOffset = Integer.parseInt( insert.substring( adrs+1 ) );
184                                insert    = insert.substring( 0,adrs );
185                        }
186
187                        boolean isOK = false;
188                        for( int i=0; i<INSERT_LIST.length; i++ ) {
189                                if( insert.equalsIgnoreCase( INSERT_LIST[i] ) ) {
190                                        isOK = true; break;
191                                }
192                        }
193                        if( !isOK ) {
194                                String errMsg = "insert は、" + Arrays.toString( INSERT_LIST )
195                                                                        + " から指定してください。" + CR
196                                                                        + "-insert=[" + insert + "]" ;
197                                throw new RuntimeException( errMsg );
198                        }
199
200                        change = StringUtil.replace( change,"\\n",CR );
201                        change = StringUtil.replace( change,"\\t","\t" );
202                }
203
204                if( delete ) { change = ""; }   // 削除は、"" 文字列と置換します。
205
206                if( ignoreCase ) {
207                        pattern = Pattern.compile( keyword,Pattern.CASE_INSENSITIVE );
208                }
209                else {
210                        pattern = Pattern.compile( keyword );
211                }
212        }
213
214        /**
215         * プロセスの終了を行います。最後に一度だけ、呼び出されます。
216         * 終了処理(ファイルクローズ、DBクローズ等)に使用します。
217         *
218         * @param   isOK トータルで、OKだったかどうか[true:成功/false:失敗]
219         */
220        public void end( final boolean isOK ) {
221                // ここでは処理を行いません。
222        }
223
224        /**
225         * 引数の LineModel を処理するメソッドです。
226         * 変換処理後の LineModel を返します。
227         * 後続処理を行わない場合(データのフィルタリングを行う場合)は、
228         * null データを返します。つまり、null データは、後続処理を行わない
229         * フラグの代わりにも使用しています。
230         * なお、変換処理後の LineModel と、オリジナルの LineModel が、
231         * 同一か、コピー(クローン)かは、各処理メソッド内で決めています。
232         * ドキュメントに明記されていない場合は、副作用が問題になる場合は、
233         * 各処理ごとに自分でコピー(クローン)して下さい。
234         *
235         * @og.rev 4.0.1.0 (2007/12/14) ファイルの一括処理対応。
236         * @og.rev 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
237         *
238         * @param       data    オリジナルのLineModel
239         *
240         * @return      処理変換後のLineModel
241         */
242        public LineModel action( final LineModel data ) {
243                inCount++ ;
244
245                final FileLineModel fileData ;
246                if( data instanceof FileLineModel ) {
247                        fileData = (FileLineModel)data ;
248                }
249                else {
250                        String errMsg = "データが FileLineModel オブジェクトではありません。" + CR ;
251                        throw new RuntimeException( errMsg );
252                }
253
254                File file = fileData.getFile() ;
255                if( ! file.isFile() ) {
256                        if( display ) { println( data.dataLine() ); }           // 5.1.2.0 (2010/01/01) display の条件変更
257                        return data;
258                }
259
260                final boolean isFind ;
261                try {
262                        String fileLine = null;
263                        int firstLineNo = -1;
264                        if( useBulkRead ) { fileLine    = findKeywordAsBulk( file ); }
265                        else                      { firstLineNo = findKeyword( file ); }
266
267                        isFind = fileLine != null || firstLineNo >= 0 ;
268
269                        // 置換処理 ただし、見つかったときのみ実行
270                        if( change != null && isFind ) {
271                                // 入力ファイルは、オリジナル_backup ファイルとする。過去のファイルを削除
272                                File inFile = new File( file.getPath() + "_backup" );
273                                if( inFile.exists() && !inFile.delete() ) {
274                                        String errMsg = "過去のBKUPファイルを削除できませんでした。[" + inFile + "]" + CR
275                                                                +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
276                                        throw new RuntimeException( errMsg );
277                                }
278
279                                // オリジナルのファイルを、_backup ファイル名に先に変換する。
280                                File fromFile = new File( file.getPath() );
281                                if( !fromFile.renameTo( inFile ) ) {
282                                        String errMsg = "所定のファイルをリネームできませんでした。[" + fromFile + "]" + CR
283                                                                +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
284                                        throw new RuntimeException( errMsg );
285                                }
286
287                                // 変換処理 本体
288                                if( useBulkRead ) { changeKeywordAsBulk( fileLine,file ); }
289                                else                      { changeKeyword( inFile,file,firstLineNo ); }
290
291                                // backup を使わない場合は、削除する。
292                                // 4.0.0.0 (2007/11/29) 入れ子if の統合
293                                if( ! useBackup && !inFile.delete() ) {
294                                        String errMsg = "所定のファイルを削除できませんでした。[" + inFile + "]" + CR
295                                                                +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
296                                        throw new RuntimeException( errMsg );
297                                }
298                        }
299                }
300                catch ( RuntimeException ex ) {
301                        String errMsg = "処理中にエラーが発生しました。[" + data.getRowNo() + "]件目" + CR
302                                                +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
303                        throw new RuntimeException( errMsg,ex );
304                }
305
306                if( display && ( notEquals ^ isFind ) ) { println( data.dataLine() ); }         // 5.1.2.0 (2010/01/01) display の条件変更
307                return ( notEquals ^ isFind ) ? data : null ;
308        }
309
310        /**
311         * キーワードが存在しているかどうかをチェックします。
312         * ここでは、1行づつ読み取りながら、最初に見つかった時点で制御を返します。
313         * よって、複数行にまたがる keyword でのマッチングは出来ませんが、大きな
314         * ファイル等での検索には、効率的です。
315         *
316         * @og.rev 4.0.1.0 (2007/12/14) 新規追加
317         *
318         * @param       file    検索元のファイルオブジェクト
319         *
320         * @return      最初に見つかった行番号(見つからなければ、-1 を返す)
321         */
322        private int findKeyword( final File file ) {
323                BufferedReader reader = null;
324
325                int firstLineNo = -1;
326                try {
327                        reader = FileUtil.getBufferedReader( file,inEncode );
328                        String line ;
329                        int lineNo = 0;
330                        while((line = reader.readLine()) != null) {
331                                lineNo++ ;
332                                Matcher mach = pattern.matcher( line );
333                                if( mach.find() ) {
334                                        if( debug ) {
335                                                String buf = "DEBUG:\t" + file.getPath() + "(" + lineNo + "): " + line ;
336                                                println( buf );
337                                        }
338                                        firstLineNo = lineNo;
339                                        break;
340                                }
341                        }
342                }
343                catch ( IOException ex ) {
344                        String errMsg = "ファイル読取エラーが発生しました。[" + file.getPath() + "]" ;
345                        throw new RuntimeException( errMsg,ex );
346                }
347                finally {
348                        Closer.ioClose( reader );
349                }
350
351                return firstLineNo;
352        }
353
354        /**
355         * キーワードが存在しているかどうかをチェックします。
356         * ここでは、ファイルをすべて読み取ってから、チェックします。
357         * よって、複数行にまたがる keyword でのマッチングが可能です。
358         *
359         * @og.rev 4.0.1.0 (2007/12/14) 新規追加
360         *
361         * @param       file    検索元のファイルオブジェクト
362         *
363         * @return      検索元のファイルの文字列化情報(ただし、見つからなければ、null)
364         */
365        private String findKeywordAsBulk( final File file ) {
366
367                boolean isFind = false;
368
369                FileString sf = new FileString();
370                sf.setFilename( file.getPath() );
371                sf.setEncode( inEncode );
372                String line = sf.getValue();
373
374                Matcher mach = pattern.matcher( line );
375                if( mach.find() ) {
376                        if( debug ) { println( "DEBUG:\t" + file.getPath() ); }
377                        isFind = true;
378                }
379
380                return isFind ? line : null;
381        }
382
383        /**
384         * キーワードを指定の文字列に置き換えます。
385         * useBackup 属性に true を指定した場合、置き換え後の、backup ファイルは、
386         * オリジナル_backup という名称に変わります。
387         * ここでは、1行づつ読み取りながら、変換処理を行います。
388         * よって、複数行にまたがる keyword でのマッチングは出来ませんが、大きな
389         * ファイル等での置換でも、メモリの使用量は抑えられます。
390         *
391         * @og.rev 4.0.1.0 (2007/12/14) 置換処理を独立させます。
392         *
393         * @param       inFile  検索元の入力ファイルオブジェクト
394         * @param       outFile 変換後の出力ファイルオブジェクト
395         * @param       firstLineNo     キーワードが存在した場合の最初の行番号
396         */
397        private void changeKeyword( final File inFile,final File outFile,final int firstLineNo ) {
398
399                BufferedReader reader = FileUtil.getBufferedReader( inFile,inEncode );
400                PrintWriter    writer = FileUtil.getPrintWriter( outFile,outEncode );
401
402                String line = null;
403                try {
404                        int lineNo = 0;
405                        while((line = reader.readLine()) != null) {
406                                lineNo++ ;
407                                if( lineNo >= firstLineNo ) {
408                                        Matcher mach = pattern.matcher( line );
409
410                                        String chnStr = null;
411                                        if( "CHANGE".equals( insert ) ) {
412                                                chnStr = strChange( mach );
413                                        }
414                                        else if( "BEFORE".equals( insert ) ) {
415                                                chnStr = strBefore( line,mach );
416                                        }
417                                        else if( "AFTER".equals( insert ) ) {
418                                                chnStr = strAfter( line,mach );
419                                        }
420
421                                        if( chnStr != null ) {
422                                                line = chnStr;
423                                                cngCount++ ;    // 変換されれば カウント
424                                        }
425                                }
426                                writer.println( line ); // readLine() してるので、最後に改行が必要。
427                        }
428                }
429                catch ( IOException ex ) {
430                        String errMsg = "処理中にエラーが発生しました。[" + line + "]" ;
431                        throw new RuntimeException( errMsg,ex );
432                }
433                finally {
434                        Closer.ioClose( reader );
435                        Closer.ioClose( writer );
436                }
437        }
438        /**
439         * キーワードを指定の文字列に置き換えます。
440         * useBackup 属性に true を指定した場合、置き換え後の、backup ファイルは、
441         * オリジナル_backup という名称に変わります。
442         * ここでは、ファイルをすべて読み取ってから、チェックします。
443         * よって、複数行にまたがる keyword でのマッチングが可能です。
444         *
445         * @og.rev 4.0.1.0 (2007/12/14) 置換処理を独立させます。
446         *
447         * @param       fileLine        検索元の行文字列
448         * @param       outFile 出力ファイルオブジェクト
449         */
450        private void changeKeywordAsBulk( final String fileLine,final File outFile ) {
451                PrintWriter writer = FileUtil.getPrintWriter( outFile,outEncode );
452
453                String line = fileLine ;
454                try {
455                        Matcher mach = pattern.matcher( line );
456
457                        String chnStr = null;
458                        if( "CHANGE".equals( insert ) ) {
459                                chnStr = strChange( mach );
460                        }
461                        else if( "BEFORE".equals( insert ) ) {
462                                chnStr = strBefore( line,mach );
463                        }
464                        else if( "AFTER".equals( insert ) ) {
465                                chnStr = strAfter( line,mach );
466                        }
467
468                        if( chnStr != null ) {
469                                line = chnStr;
470                                cngCount++ ;    // 変換されれば カウント
471                        }
472
473                        writer.print( line );   // 注意:改行コードは、不要
474                }
475                catch ( RuntimeException ex ) {
476                        String errMsg = "処理中にエラーが発生しました。[" + outFile.getPath() + "]" ;
477                        throw new RuntimeException( errMsg,ex );
478                }
479                finally {
480                        Closer.ioClose( writer );
481                }
482        }
483
484        /**
485         * insert が、"CHANGE" の場合の処理結果を求めます。
486         * 変換しなかった場合は、null を返します。
487         * これは、変換カウントを算出する為のフラグ代わりに使用しています。
488         *
489         * @param       mach    キーワードの正規表現
490         *
491         * @return      変換結果(対象行で無い場合は、null)
492         */
493        private String strChange( final Matcher mach ) {
494                String line = null;
495                if( mach.find() ) {
496                        line = mach.replaceAll( change );
497                }
498                return line ;
499        }
500
501        /**
502         * insert が、"BEFORE" の場合の処理結果を求めます。
503         * 変換しなかった場合は、null を返します。
504         * これは、変換カウントを算出する為のフラグ代わりに使用しています。
505         *
506         * @param       line    検索行
507         * @param       mach    キーワードの正規表現
508         *
509         * @return      変換結果(対象行で無い場合は、null)
510         */
511        private String strBefore( final String line , final Matcher mach ) {
512                boolean isChng = false;
513                StringBuilder buf = new StringBuilder( line.length() );
514                int indx = 0;
515                while( mach.find() ) {
516                        isChng = true;
517                        int strt = mach.start() + insOffset;
518                        buf.append( line.substring( indx,strt ) );
519                        buf.append( change );
520                        indx = strt;
521                }
522
523                String rtn = null;
524                if( isChng ) {
525                        buf.append( line.substring( indx ) );
526                        rtn = buf.toString();
527                }
528
529                return rtn ;
530        }
531
532        /**
533         * insert が、"AFTER" の場合の処理結果を求めます。
534         * 変換しなかった場合は、null を返します。
535         * これは、変換カウントを算出する為のフラグ代わりに使用しています。
536         *
537         * @param       line    検索行
538         * @param       mach    キーワードの正規表現
539         *
540         * @return      変換結果(対象行で無い場合は、null)
541         */
542        private String strAfter( final String line , final Matcher mach ) {
543                boolean isChng = false;
544                StringBuilder buf = new StringBuilder( line.length() );
545                int indx = 0;
546                while( mach.find() ) {
547                        isChng = true;
548                        int end = mach.end() + insOffset;
549                        buf.append( line.substring( indx,end ) );
550                        buf.append( change );
551                        indx = end;
552                }
553                String rtn = null;
554                if( isChng ) {
555                        buf.append( line.substring( indx ) );
556                        rtn = buf.toString();
557                }
558
559                return rtn ;
560        }
561
562        /**
563         * プロセスの処理結果のレポート表現を返します。
564         * 処理プログラム名、入力件数、出力件数などの情報です。
565         * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
566         * 形式で出してください。
567         *
568         * @return   処理結果のレポート
569         */
570        public String report() {
571                if( findCount < cngCount ) { findCount = cngCount; }
572
573                String report = "[" + getClass().getName() + "]" + CR
574                                + TAB + "Search Keyword    : " + keyword    + CR
575                                + TAB + "Search File Count : " + inCount    + CR
576                                + TAB + "Key Find    Count : " + findCount  + CR
577                                + TAB + "Key Change  Count : " + cngCount ;
578
579                return report ;
580        }
581
582        /**
583         * このクラスの使用方法を返します。
584         *
585         * @return      このクラスの使用方法
586         */
587        public String usage() {
588                StringBuilder buf = new StringBuilder();
589
590                buf.append( "Process_Grep は、上流から受け取った FileLineModelから、文字列を見つけ出す"                ).append( CR );
591                buf.append( "ChainProcess インターフェースの実装クラスです。"                                                            ).append( CR );
592                buf.append( CR );
593                buf.append( "正規表現の keyword を上流から受け取った FileLineModel から検索します。"           ).append( CR );
594                buf.append( "見つかった対象ファイルから、指定の文字列を置換する場合は、-change か"                    ).append( CR );
595                buf.append( "-changeFile で、keyword を置換する文字列を指定して下さい。"                                   ).append( CR );
596                buf.append( "置換する文字列には、\t と \n の特殊文字が使用できます。"                                           ).append( CR );
597                buf.append( CR );
598                buf.append( "処理対象は、通常は、1行づつ読み取りながら処理を行います。存在チェックの場合は、"  ).append( CR );
599                buf.append( "見つかった時点で処理を中止します。これは、該当箇所をピックアップするのではなく、"  ).append( CR );
600                buf.append( "存在しているかどうかを判断して、あれば、下流に流すというのが目的だからです。"            ).append( CR );
601                buf.append( "keyword を、改行を含む正規表現で、検索・置換する場合は、-useBulkRead 属性を"          ).append( CR );
602                buf.append( "true に設定してください。これは、入力ファイルを一括して読み込みます。"                     ).append( CR );
603                buf.append( "-ignoreCase は、検索時にキーの大文字小文字を無視するように指定します。"         ).append( CR );
604                buf.append( "-notEquals は、結果(見つかればtrue)を反転(見つからなければtrue)します。"           ).append( CR );
605                buf.append( "これは、行単位ではなく、ファイル単位に判定しますので、change 指定した場合"          ).append( CR );
606                buf.append( "でも、対象行は、見つかった行です。ただし、下流に対して、見つからない"                        ).append( CR );
607                buf.append( "場合だけ処理を継続させます。"                                                                                                    ).append( CR );
608                buf.append( "-inEncode は、入力ファイルのエンコード指定になります。"                                          ).append( CR );
609                buf.append( "-outEncode は、出力ファイルのエンコードや、changeFileで指定の置換文字列"            ).append( CR );
610                buf.append( "ファイルのエンコード指定になります。(changeFile は、必ず 出力ファイルと)"               ).append( CR );
611                buf.append( "同じエンコードです。"                                                                                                                        ).append( CR );
612                buf.append( "これらのエンコードが無指定の場合は、System.getProperty(\"file.encoding\") "  ).append( CR );
613                buf.append( "で求まる値を使用します。"                                                                                                              ).append( CR );
614                buf.append( "-changeFile を使用することで、複数行の文字列に置換することが可能です。"                 ).append( CR );
615                buf.append( CR );
616                buf.append( "上流(プロセスチェインのデータは上流から渡されます。)からのLineModel の"         ).append( CR );
617                buf.append( "ファイルオブジェクトより、指定の文字列が含まれているか検索します。"                 ).append( CR );
618                buf.append( "上流プロセスでは、Name 属性として、『File』を持ち、値は、Fileオブジェクト"               ).append( CR );
619                buf.append( "である、Process_FileSearch を使用するのが、便利です。それ以外のクラスを"             ).append( CR );
620                buf.append( "使用する場合でも、Name属性と、File オブジェクトを持つ LineModel を受け渡し"   ).append( CR );
621                buf.append( "できれば、使用可能です。"                                                                                                              ).append( CR );
622                buf.append( CR );
623                buf.append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。" ).append( CR );
624                buf.append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に"                ).append( CR );
625                buf.append( "繋げてください。"                                                                                                                          ).append( CR );
626                buf.append( CR ).append( CR );
627
628                buf.append( getArgument().usage() ).append( CR );
629
630                return buf.toString();
631        }
632
633        /**
634         * このクラスは、main メソッドから実行できません。
635         *
636         * @param       args    コマンド引数配列
637         */
638        public static void main( final String[] args ) {
639                LogWriter.log( new Process_Grep().usage() );
640        }
641}