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