    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.Map ;
    026import java.util.LinkedHashMap ;
    027import java.util.List ;
    028import java.util.ArrayList ;
    029import java.util.Locale ;                               // 5.7.3.2 (2014/02/28) ignoreCase が実装されていなかった。
    030import java.util.regex.Pattern;                 // 5.7.3.2 (2014/02/28) regexを利用する場合
    031import java.util.regex.Matcher;                 // 5.7.3.2 (2014/02/28) regexを利用する場合
    032
    033import java.io.File;
    034import java.io.PrintWriter;
    035import java.io.BufferedReader;
    036import java.io.IOException;
    037
    038/**
    039 * Process_GrepChange は、上流から受け取った FileLineModelから、語句を
    040 * 置換する、ChainProcess インターフェースの実装クラスです。
    041 *
    042 * Process_Grep との違いは、チェックするファイルのコピーを(キーワードが存在
    043 * しなくとも)作成することと、検索キーに正規表現が使えない、複数行置き換えが
    044 * 出来ないことです。
    045 *
    046 * keywordFile より、置換する語句を含むキーと値のペアー(タブ区切り)を読取り、
    047 * 対象とする語句を置換します。
    048 * keywordFile に、タブが含まれない行や、先頭にタブが存在している場合は、
    049 * その行を読み飛ばします。また、区切りタブは何個存在しても構いません。
    050 * 置換文字(値)は、\t の特殊文字が使用できます。
    051 * この GrepChange では、語句に、正規表現は使用できません。正規表現のキーワード
    052 * や文字列を複数行の文字列と置き換える場合は、Process_Grep を使用してください。
    053 * このプログラムでは、上流から受け取った FileLineModel のファイルに対して、
    054 * 置き換えた結果も、同じファイルにセーブします。
    055 * 元のファイルを保存したい場合は、予めバックアップを取得しておいてください。
    056 * -inEncode は、入力ファイルのエンコード指定になります。
    057 * -outEncode は、出力ファイルのエンコードや、キーワードファイルの
    058 * エンコード指定になります。(keywordFile は、必ず 出力ファイルと同じエンコードです。)
    059 * これらのエンコードが無指定の場合は、System.getProperty("file.encoding") で
    060 * 求まる値を使用します。
    061 *
    062 * 5.7.3.2 (2014/02/28)
    063 * -regex=true で、ｷｰﾜｰﾄﾞに正規表現を利用できます。具体的には、String#replaceAll(String,String)
    064 * を利用して置換します。
    065 * 通常の置換処理は、indexOf で見つけて、StringBuilder#replace(int,int,String) を繰り返して処理しています。
    066 * -ignoreCase=true で、検索ｷｰﾜｰﾄﾞに大文字小文字を区別しない処理が可能です。
    067 *
    068 * 上流プロセスでは、Name 属性として、『File』を持ち、値は、Fileオブジェクト
    069 * である、Process_FileSearch を使用するのが、便利です。それ以外のクラスを
    070 * 使用する場合でも、Name属性と、File オブジェクトを持つ LineModel を受け渡し
    071 * できれば、使用可能です。
    072 *
    073 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
    074 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に
    075 * 繋げてください。
    076 *
    077 *  Process_GrepChange -keyword=検索文字列 -ignoreCase=true -outfile=OUTFILE -encode=UTF-8
    078 *
    079 *    -keywordFile=キーワード    ：置換する語句を含むキーと値のペアー(タブ区切り)
    080 *   [-ignoreCase=[false/true] ] ：検索時に大文字小文字を区別しない(true)かどうか(初期値:false[区別する])
    081 *   [-regex=[false/true]      ] ：ｷｰﾜｰﾄﾞに正規表現を利用する(true)かどうか(初期値:false[利用しない])
    082 *   [-isChange=置換可否       ] ：置換処理を実施する(true)かどうか(初期値:置換する[true])
    083 *   [-inEncode=入力エンコード ] ：入力ファイルのエンコードタイプ
    084 *   [-outEncode=出力エンコード] ：出力ファイルやキーワードファイルのエンコードタイプ
    085 *   [-display=[false/true]    ] ：結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
    086 *   [-debug=[false/true]      ] ：デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
    087 *
    088 * @version  4.0
    089 * @author   Kazuhiko Hasegawa
    090 * @since    JDK5.0,
    091 */
    092public class Process_GrepChange extends AbstractProcess implements ChainProcess {
    093        private String[]        keyword = null;
    094        private String[]        change  = null;
    095        private Pattern[]       pattern = null;                 // 5.7.3.2 (2014/02/28) ｷｰﾜｰﾄﾞに正規表現を利用する場合
    096        private boolean         ignoreCase      = false;
    097        private boolean         regex           = false;        // 5.7.3.2 (2014/02/28) ｷｰﾜｰﾄﾞに正規表現を利用するかどうか
    098        private boolean         isChange        = true;         // 5.1.2.0 (2010/01/01) 置換するかどうかを指定可能にする
    099        private String          inEncode        = null;
    100        private String          outEncode       = null;
    101        private boolean         display         = false;        // 表示しない
    102        private boolean         debug           = false;        // 5.7.3.0 (2014/02/07) デバッグ情報
    103
    104        private int             inCount         = 0;
    105        private int             findCount       = 0;
    106        private int             cngCount        = 0;
    107
    108        private static final Map<String,String> mustProparty   ;                // ［プロパティ］必須チェック用 Map
    109        private static final Map<String,String> usableProparty ;                // ［プロパティ］整合性チェック Map
    110
    111        static {
    112                mustProparty = new LinkedHashMap<String,String>();
    113                mustProparty.put( "keywordFile",        "置換する語句を含むキーと値のペアー(タブ区切り)(必須)" );
    114
    115                usableProparty = new LinkedHashMap<String,String>();
    116                usableProparty.put( "ignoreCase",       "検索時に大文字小文字を区別しない(true)かどうか。" +
    117                                                                                CR + "(初期値:区別する[false])" );
    118                usableProparty.put( "regex",            "ｷｰﾜｰﾄﾞに正規表現を利用する(true)かどうか。" +
    119                                                                                CR + "(初期値:利用しない[false])" );    // 5.7.3.2 (2014/02/28)
    120                usableProparty.put( "isChange",         "置換処理を実施する(true)かどうか" +
    121                                                                                CR + "(初期値:置換する[true])" );
    122                usableProparty.put( "inEncode",         "入力ファイルのエンコードタイプ" );
    123                usableProparty.put( "outEncode",        "出力ファイルやキーワードファイルのエンコードタイプ" );
    124                usableProparty.put( "display",          "結果を標準出力に表示する(true)かしない(false)か" +
    125                                                                                CR + "(初期値:false:表示しない)" );
    126                usableProparty.put( "debug",    "デバッグ情報を標準出力に表示する(true)かしない(false)か" +
    127                                                                                CR + "(初期値:false:表示しない)" );             // 5.7.3.0 (2014/02/07) デバッグ情報
    128        }
    129
    130        /**
    131         * デフォルトコンストラクター。
    132         * このクラスは、動的作成されます。デフォルトコンストラクターで、
    133         * super クラスに対して、必要な初期化を行っておきます。
    134         *
    135         */
    136        public Process_GrepChange() {
    137                super( "org.opengion.fukurou.process.Process_GrepChange",mustProparty,usableProparty );
    138        }
    139
    140        /**
    141         * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
    142         * 初期処理(ファイルオープン、ＤＢオープン等)に使用します。
    143         *
    144         * @og.rev 5.1.2.0 (2010/01/01) 置換するかどうかを指定可能にする(isChange)属性追加
    145         * @og.rev 5.7.3.2 (2014/02/28) debug の表示と、ｷｰﾜｰﾄﾞの \t の使用、trim() 廃止、ignoreCase の実装、regex の追加
    146         *
    147         * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
    148         */
    149        public void init( final ParamProcess paramProcess ) {
    150                Argument arg = getArgument();
    151
    152                String keywordFile = arg.getProparty("keywordFile" );
    153                ignoreCase      = arg.getProparty("ignoreCase",ignoreCase);
    154                regex           = arg.getProparty("regex",regex);                               // 5.7.3.2 (2014/02/28)
    155                isChange        = arg.getProparty("isChange",isChange);                 // 5.1.2.0 (2010/01/01)
    156                inEncode        = arg.getProparty("inEncode",System.getProperty("file.encoding"));
    157                outEncode       = arg.getProparty("outEncode",System.getProperty("file.encoding"));
    158                display         = arg.getProparty("display",display);
    159                debug           = arg.getProparty("debug",debug);                               // 5.7.3.0 (2014/02/07) デバッグ情報
    160
    161                FileString fs = new FileString();
    162                fs.setFilename( keywordFile );
    163                fs.setEncode( outEncode );
    164                String[] lines = fs.getValue( CR );                                     // 5.7.3.2 (2014/02/28) \n でなく、CR とします。
    165                int len = lines.length;
    166                if( len == 0 ) {
    167                        String errMsg = "keywordFile の内容が 読み取れませんでした。[" + keywordFile + "]" ;
    168                        throw new RuntimeException( errMsg );
    169                }
    170
    171                println( "keywordFile を、" + len + "件読み取りました。" );
    172                List<String> keyList = new ArrayList<String>( len );
    173                List<String> cngList = new ArrayList<String>( len );
    174
    175                for( int i=0; i<len; i++ ) {
    176        //              String line = lines[i].trim();
    177                        String line = lines[i];
    178                        int indx = line.indexOf( '\t' );
    179                        if( indx <= 0 ) { continue ; }  // TAB が先頭や、存在しない行は読み飛ばす。
    180                        // 5.7.3.2 (2014/02/28) debug の表示と、ｷｰﾜｰﾄﾞの \t の使用、trim() 廃止
    181                        String key = line.substring( 0,indx );
    182                        String cng = line.substring( indx+1 );
    183
    184                        if( ignoreCase ) { key = key.toUpperCase(Locale.JAPAN); }       // 5.7.3.2 (2014/02/28) ignoreCase の実装漏れ
    185
    186                        if( debug ) { println( "[" + key + "]⇒[" + cng + "]" ); }
    187
    188                        key = StringUtil.replace( key,"\\t","\t" );
    189
    190                        cng = StringUtil.replace( cng,"\\t","\t" );
    191
    192                        keyList.add( key );
    193                        cngList.add( cng );
    194                }
    195                keyword = keyList.toArray( new String[keyList.size()] );
    196                change  = cngList.toArray( new String[cngList.size()] );
    197
    198                // 5.7.3.2 (2014/02/28) regex=true の場合の処理
    199                if( regex ) {
    200                        pattern = new Pattern[keyword.length];
    201                        for( int i=0; i<keyword.length; i++ ) {
    202                                pattern[i] = ignoreCase ? Pattern.compile( keyword[i],Pattern.CASE_INSENSITIVE )
    203                                                                                : Pattern.compile( keyword[i] ) ;
    204                        }
    205                }
    206        }
    207
    208        /**
    209         * プロセスの終了を行います。最後に一度だけ、呼び出されます。
    210         * 終了処理(ファイルクローズ、ＤＢクローズ等)に使用します。
    211         *
    212         * @param   isOK トータルで、OKだったかどうか[true:成功/false:失敗]
    213         */
    214        public void end( final boolean isOK ) {
    215                // ここでは処理を行いません。
    216        }
    217
    218        /**
    219         * 引数の LineModel を処理するメソッドです。
    220         * 変換処理後の LineModel を返します。
    221         * 後続処理を行わない場合(データのフィルタリングを行う場合)は、
    222         * null データを返します。つまり、null データは、後続処理を行わない
    223         * フラグの代わりにも使用しています。
    224         * なお、変換処理後の LineModel と、オリジナルの LineModel が、
    225         * 同一か、コピー(クローン)かは、各処理メソッド内で決めています。
    226         * ドキュメントに明記されていない場合は、副作用が問題になる場合は、
    227         * 各処理ごとに自分でコピー(クローン)して下さい。
    228         *
    229         * @og.rev 4.0.0.0 (2007/11/28) メソッドの戻り値をチェックします。
    230         * @og.rev 5.1.2.0 (2010/01/01) 置換するかどうかを指定可能にする(isChange)属性追加
    231         * @og.rev 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
    232         * @og.rev 5.7.3.2 (2014/02/28) debug の表示と、ignoreCase の実装
    233         *
    234         * @param       data    オリジナルのLineModel
    235         *
    236         * @return      処理変換後のLineModel
    237         */
    238        public LineModel action( final LineModel data ) {
    239                inCount++ ;
    240                final FileLineModel fileData ;
    241                if( data instanceof FileLineModel ) {
    242                        fileData = (FileLineModel)data ;
    243                }
    244                else {
    245                        String errMsg = "データが FileLineModel オブジェクトではありません。" + CR ;
    246                        throw new RuntimeException( errMsg );
    247                }
    248
    249
    250                File org = fileData.getFile() ;
    251                String orgName = org.getPath();
    252                if( ! org.isFile() ) { return data; }
    253
    254                if( debug ) { println( "File:" + org ); }               // 5.1.2.0 (2010/01/01) display の条件変更
    255
    256                BufferedReader reader = FileUtil.getBufferedReader( org,inEncode );
    257                File            tempFile  = null;
    258                PrintWriter     tempWrt   = null;
    259
    260                // 5.1.2.0 (2010/01/01) 置換する場合の前処理
    261                if( isChange ) {
    262                        tempFile  = new File( orgName + "_temp" );
    263                        tempWrt   = FileUtil.getPrintWriter( tempFile,outEncode );
    264                }
    265
    266                boolean nextFlag  = false;
    267
    268                try {
    269                        String line ;
    270                        int    lineNo = 0;
    271                        while((line = reader.readLine()) != null) {
    272                                lineNo++ ;
    273                                // 5.7.3.2 (2014/02/28) regex 対応
    274                                if( regex ) {
    275                                        for( int i=0; i<pattern.length; i++ ) {
    276                                                Matcher mt = pattern[i].matcher( line );
    277                                                nextFlag = mt.matches();
    278                                                if( nextFlag ) {
    279                                                        findCount++ ;
    280                                                        if( display ) { println( orgName + ":" + lineNo + ":" + keyword[i] + ":" + line ); }
    281                                                        if( isChange ) {
    282                                                                line = mt.replaceAll( change[i] );
    283                                                                cngCount++ ;
    284                                                        }
    285                                                }
    286                                        }
    287                                }
    288                                else {
    289                                        StringBuilder buf = new StringBuilder( line );
    290        //                              boolean foundFlag = false;              // 行単位に初期化する。
    291                                        for( int i=0; i<keyword.length; i++ ) {
    292                                                // 5.7.3.2 (2014/02/28) ignoreCase 対応。
    293        //                                      int indx = buf.indexOf( keyword[i] );
    294                                                int indx = ignoreCase   ? buf.toString().toUpperCase(Locale.JAPAN).indexOf( keyword[i] )
    295                                                                                                : buf.indexOf( keyword[i] ) ;
    296
    297                                                // 置換対象発見。行出力用に見つかれば、true にする。
    298                                                if( indx >= 0 ) {
    299        //                                              foundFlag = true;
    300                                                        nextFlag  = true;               // １度でも見つかれば、true にセット
    301                                                        if( display ) { println( orgName + ":" + lineNo + ":" + keyword[i] + ":" + line ); }
    302                                                        findCount++ ;
    303                                                }
    304                                                // 置換対象が見つかっても、isChange=true でなければ、置換処理は行わない。
    305                                                if( isChange ) {
    306                                                        while( indx >= 0 ) {
    307                                                                buf.replace( indx,indx+keyword[i].length(),change[i] );
    308                                                                // 5.7.3.2 (2014/02/28) ignoreCase 対応。
    309        //                                                      indx = buf.indexOf( keyword[i],indx+change[i].length() );
    310                                                                int nxt = indx+change[i].length();
    311                                                                indx = ignoreCase       ? buf.toString().toUpperCase(Locale.JAPAN).indexOf( keyword[i],nxt )
    312                                                                                                        : buf.indexOf( keyword[i],nxt );
    313
    314        //                                                      nextFlag = true;                        // キーワードが存在したファイル。
    315                                                                cngCount++ ;
    316                                                        }
    317                                                }
    318                                        }
    319                                        line = buf.toString();
    320                                }
    321                                // 5.1.2.0 (2010/01/01) 置換する場合の処理
    322                                if( isChange ) {
    323                                        tempWrt.println( line );                                // 5.7.3.2 (2014/02/28) regexで出力を共有する為。
    324                                }
    325                        }
    326                }
    327                catch ( IOException ex ) {
    328                        String errMsg = "処理中にエラーが発生しました。[" + data.getRowNo() + "]件目" + CR
    329                                                +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
    330                        throw new RuntimeException( errMsg,ex );
    331                }
    332                finally {
    333                        Closer.ioClose( reader );
    334                        Closer.ioClose( tempWrt );
    335                }
    336
    337                // 5.1.2.0 (2010/01/01) 置換する場合の処理
    338                if( isChange ) {
    339                        if( nextFlag ) {
    340                                if( !org.delete() ) {
    341                                        String errMsg = "所定のファイルを削除できませんでした。[" + org + "]" + CR
    342                                                        +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
    343                                        throw new RuntimeException( errMsg );
    344                                }
    345                                if( !tempFile.renameTo( org ) ) {
    346                                        String errMsg = "所定のファイルをリネームできませんでした。[" + tempFile + "]" + CR
    347                                                        +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
    348                                        throw new RuntimeException( errMsg );
    349                                }
    350                        }
    351                        else {
    352                                if( !tempFile.delete() ) {
    353                                        String errMsg = "所定のファイルを削除できませんでした。[" + tempFile + "]" + CR
    354                                                        +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
    355                                        throw new RuntimeException( errMsg );
    356                                }
    357                        }
    358                }
    359
    360                return nextFlag ? data : null ;
    361        }
    362
    363        /**
    364         * プロセスの処理結果のレポート表現を返します。
    365         * 処理プログラム名、入力件数、出力件数などの情報です。
    366         * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
    367         * 形式で出してください。
    368         *
    369         * @return   処理結果のレポート
    370         */
    371        public String report() {
    372                String report = "[" + getClass().getName() + "]" + CR
    373                                + TAB + "Search File Count : " + inCount    + CR
    374                                + TAB + "Key Find    Count : " + findCount  + CR
    375                                + TAB + "Key Change  Count : " + cngCount ;
    376
    377                return report ;
    378        }
    379
    380        /**
    381         * このクラスの使用方法を返します。
    382         *
    383         * @return      このクラスの使用方法
    384         */
    385        public String usage() {
    386                StringBuilder buf = new StringBuilder();
    387
    388                buf.append( "Process_GrepChange は、上流から受け取った FileLineModelから、語句を"                        ).append( CR );
    389                buf.append( "置換する、ChainProcess インターフェースの実装クラスです。"                                               ).append( CR );
    390                buf.append( "Process_Grep との違いは、チェックするファイルのコピーを(キーワードが存在"               ).append( CR );
    391                buf.append( "しなくとも)作成することと、検索キーに正規表現が使えない、複数行置き換えが"             ).append( CR );
    392                buf.append( "出来ないことです。"                                                                                                                 ).append( CR );
    393                buf.append( CR );
    394                buf.append( "keywordFile より、置換する語句を含むキーと値のペアー(タブ区切り)を読取り、"      ).append( CR );
    395                buf.append( "対象とする語句を置換します。"                                                                                                    ).append( CR );
    396                buf.append( "keywordFile に、タブが含まれない行や、先頭にタブが存在している場合は、"         ).append( CR );
    397                buf.append( "その行を読み飛ばします。また、区切りタブは何個存在しても構いません。"                        ).append( CR );
    398                buf.append( "ただし、タブで区切った前(キー)と後ろ(値)は、trim() されますので、スペース"                ).append( CR );
    399                buf.append( "が前後に存在している場合は、ご注意ください。"                                                                    ).append( CR );
    400                buf.append( "置換文字(値)は、\t と \n の特殊文字が使用できます。"                                                    ).append( CR );
    401                buf.append( "この GrepChange では、語句に、正規表現は使用できません。正規表現のキーワード"      ).append( CR );
    402                buf.append( "や文字列を複数行の文字列と置き換える場合は、Process_Grep を使用して下さい。"      ).append( CR );
    403                buf.append( "このプログラムでは、上流から受け取った FileLineModel のファイルに対して、"              ).append( CR );
    404                buf.append( "置き換えた結果も、同じファイルにセーブします。"                                                           ).append( CR );
    405                buf.append( "元のファイルを保存したい場合は、予めバックアップを取得しておいてください。"     ).append( CR );
    406                buf.append( "-inEncode は、入力ファイルのエンコード指定になります。"                                          ).append( CR );
    407                buf.append( "-outEncode は、出力ファイルのエンコードや、キーワードファイルのエンコード"        ).append( CR );
    408                buf.append( "指定になります。(keywordFile は、必ず 出力ファイルと同じエンコードです。)"      ).append( CR );
    409                buf.append( "これらのエンコードが無指定の場合は、System.getProperty(\"file.encoding\") "  ).append( CR );
    410                buf.append( "で求まる値を使用します。"                                                                                                              ).append( CR );
    411                buf.append( CR );
    412                buf.append( "上流プロセスでは、Name 属性として、『File』を持ち、値は、Fileオブジェクト"               ).append( CR );
    413                buf.append( "である、Process_FileSearch を使用するのが、便利です。それ以外のクラスを"             ).append( CR );
    414                buf.append( "使用する場合でも、Name属性と、File オブジェクトを持つ LineModel を受け渡し"   ).append( CR );
    415                buf.append( "できれば、使用可能です。"                                                                                                              ).append( CR );
    416                buf.append( CR );
    417                buf.append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。" ).append( CR );
    418                buf.append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に"                ).append( CR );
    419                buf.append( "繋げてください。"                                                                                                                          ).append( CR );
    420                buf.append( CR ).append( CR );
    421
    422                buf.append( getArgument().usage() ).append( CR );
    423
    424                return buf.toString();
    425        }
    426
    427        /**
    428         * このクラスは、main メソッドから実行できません。
    429         *
    430         * @param       args    コマンド引数配列
    431         */
    432        public static void main( final String[] args ) {
    433                LogWriter.log( new Process_GrepChange().usage() );
    434        }
    435}