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         * 初期処理(ファイルオープン、DBオープン等)に使用します。
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//              if( debug ) { println( arg.toString() ); }                                      // 5.7.3.0 (2014/02/07) デバッグ情報
161
162                FileString fs = new FileString();
163                fs.setFilename( keywordFile );
164                fs.setEncode( outEncode );
165//              String[] lines = fs.getValue( "\n" );
166                String[] lines = fs.getValue( CR );                                     // 5.7.3.2 (2014/02/28) \n でなく、CR とします。
167                int len = lines.length;
168                if( len == 0 ) {
169                        String errMsg = "keywordFile の内容が 読み取れませんでした。[" + keywordFile + "]" ;
170                        throw new RuntimeException( errMsg );
171                }
172
173                println( "keywordFile を、" + len + "件読み取りました。" );
174                List<String> keyList = new ArrayList<String>( len );
175                List<String> cngList = new ArrayList<String>( len );
176
177                for( int i=0; i<len; i++ ) {
178        //              String line = lines[i].trim();
179                        String line = lines[i];
180                        int indx = line.indexOf( '\t' );
181                        if( indx <= 0 ) { continue ; }       // TAB が先頭や、存在しない行は読み飛ばす。
182                        // 5.7.3.2 (2014/02/28) debug の表示と、キーワードの \t の使用、trim() 廃止
183//                      keyList.add( line.substring( 0,indx ).trim() );
184//                      String cng = line.substring( indx+1 ).trim();
185                        String key = line.substring( 0,indx );
186                        String cng = line.substring( indx+1 );
187
188                        if( ignoreCase ) { key = key.toUpperCase(Locale.JAPAN); }       // 5.7.3.2 (2014/02/28) ignoreCase の実装漏れ
189
190                        if( debug ) { println( "[" + key + "]⇒[" + cng + "]" ); }
191
192//                      key = StringUtil.replace( key,"\\n",CR );               // 5.7.3.2 (2014/02/28) キーワードに \n は使えない。
193                        key = StringUtil.replace( key,"\\t","\t" );
194
195//                      cng = StringUtil.replace( cng,"\\n",CR );               // 5.7.3.2 (2014/02/28) キーワードに \n は使えない。
196                        cng = StringUtil.replace( cng,"\\t","\t" );
197
198                        keyList.add( key );
199                        cngList.add( cng );
200                }
201                keyword = keyList.toArray( new String[keyList.size()] );
202                change  = cngList.toArray( new String[cngList.size()] );
203
204                // 5.7.3.2 (2014/02/28) regex=true の場合の処理
205                if( regex ) {
206                        pattern = new Pattern[keyword.length];
207                        for( int i=0; i<keyword.length; i++ ) {
208                                pattern[i] = (ignoreCase) ? Pattern.compile( keyword[i],Pattern.CASE_INSENSITIVE )
209                                                                                  : Pattern.compile( keyword[i] ) ;
210                        }
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.0.0 (2007/11/28) メソッドの戻り値をチェックします。
236         * @og.rev 5.1.2.0 (2010/01/01) 置換するかどうかを指定可能にする(isChange)属性追加
237         * @og.rev 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
238         * @og.rev 5.7.3.2 (2014/02/28) debug の表示と、ignoreCase の実装
239         *
240         * @param       data    オリジナルのLineModel
241         *
242         * @return      処理変換後のLineModel
243         */
244        public LineModel action( final LineModel data ) {
245                inCount++ ;
246                final FileLineModel fileData ;
247                if( data instanceof FileLineModel ) {
248                        fileData = (FileLineModel)data ;
249                }
250                else {
251                        String errMsg = "データが FileLineModel オブジェクトではありません。" + CR ;
252                        throw new RuntimeException( errMsg );
253                }
254
255
256                File org = fileData.getFile() ;
257                String orgName = org.getPath();
258                if( ! org.isFile() ) { return data; }
259
260                if( debug ) { println( "File:" + org ); }               // 5.1.2.0 (2010/01/01) display の条件変更
261
262                BufferedReader reader = FileUtil.getBufferedReader( org,inEncode );
263//              File            tempFile  = new File( org.getPath() + "_temp" );
264//              PrintWriter     tempWrt   = FileUtil.getPrintWriter( tempFile,outEncode );
265                File            tempFile  = null;
266                PrintWriter     tempWrt   = null;
267
268                // 5.1.2.0 (2010/01/01) 置換する場合の前処理
269                if( isChange ) {
270                        tempFile  = new File( orgName + "_temp" );
271                        tempWrt   = FileUtil.getPrintWriter( tempFile,outEncode );
272                }
273
274                boolean nextFlag  = false;
275
276                try {
277                        String line ;
278                        int    lineNo = 0;
279                        while((line = reader.readLine()) != null) {
280                                lineNo++ ;
281                                // 5.7.3.2 (2014/02/28) regex 対応
282                                if( regex ) {
283                                        for( int i=0; i<pattern.length; i++ ) {
284                                                Matcher mt = pattern[i].matcher( line );
285                                                nextFlag = mt.matches();
286                                                if( nextFlag ) {
287                                                        findCount++ ;
288                                                        if( display ) { println( orgName + ":" + lineNo + ":" + keyword[i] + ":" + line ); }
289                                                        if( isChange ) {
290                                                                line = mt.replaceAll( change[i] );
291                                                                cngCount++ ;
292                                                        }
293                                                }
294                                        }
295                                }
296                                else {
297                                        StringBuilder buf = new StringBuilder( line );
298        //                              boolean foundFlag = false;              // 行単位に初期化する。
299                                        for( int i=0; i<keyword.length; i++ ) {
300                                                // 5.7.3.2 (2014/02/28) ignoreCase 対応。
301        //                                      int indx = buf.indexOf( keyword[i] );
302                                                int indx = (ignoreCase) ? buf.toString().toUpperCase(Locale.JAPAN).indexOf( keyword[i] )
303                                                                                                : buf.indexOf( keyword[i] ) ;
304
305                                                // 置換対象発見。行出力用に見つかれば、true にする。
306                                                if( indx >= 0 ) {
307        //                                              foundFlag = true;
308                                                        nextFlag  = true;               // 1度でも見つかれば、true にセット
309                                                        if( display ) { println( orgName + ":" + lineNo + ":" + keyword[i] + ":" + line ); }
310                                                        findCount++ ;
311                                                }
312                                                // 置換対象が見つかっても、isChange=true でなければ、置換処理は行わない。
313                                                if( isChange ) {
314                                                        while( indx >= 0 ) {
315                                                                buf.replace( indx,indx+keyword[i].length(),change[i] );
316                                                                // 5.7.3.2 (2014/02/28) ignoreCase 対応。
317        //                                                      indx = buf.indexOf( keyword[i],indx+change[i].length() );
318                                                                int nxt = indx+change[i].length();
319                                                                indx = (ignoreCase) ? buf.toString().toUpperCase(Locale.JAPAN).indexOf( keyword[i],nxt )
320                                                                                                        : buf.indexOf( keyword[i],nxt );
321
322                //                                              nextFlag = true;                        // キーワードが存在したファイル。
323                                                                cngCount++ ;
324//                                                              findCount++ ;
325                                                        }
326                                                }
327                                        }
328                                        line = buf.toString();
329                                }
330                                // 5.1.2.0 (2010/01/01) 置換する場合の処理
331                                if( isChange ) {
332//                                      tempWrt.println( buf.toString() );
333                                        tempWrt.println( line );                                // 5.7.3.2 (2014/02/28) regexで出力を共有する為。
334                                }
335                        }
336                }
337                catch ( IOException ex ) {
338                        String errMsg = "処理中にエラーが発生しました。[" + data.getRowNo() + "]件目" + CR
339//                                              + data.toString() ;
340                                                +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
341                        throw new RuntimeException( errMsg,ex );
342                }
343                finally {
344                        Closer.ioClose( reader );
345                        Closer.ioClose( tempWrt );
346                }
347
348                // 5.1.2.0 (2010/01/01) 置換する場合の処理
349                if( isChange ) {
350                        if( nextFlag ) {
351                                if( !org.delete() ) {
352                                        String errMsg = "所定のファイルを削除できませんでした。[" + org + "]" + CR
353                                                        +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
354                                        throw new RuntimeException( errMsg );
355                                }
356                                if( !tempFile.renameTo( org ) ) {
357                                        String errMsg = "所定のファイルをリネームできませんでした。[" + tempFile + "]" + CR
358                                                        +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
359                                        throw new RuntimeException( errMsg );
360                                }
361                        }
362                        else {
363                                if( !tempFile.delete() ) {
364                                        String errMsg = "所定のファイルを削除できませんでした。[" + tempFile + "]" + CR
365                                                        +       "data=[" + data.dataLine() + "]" + CR ;         // 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
366                                        throw new RuntimeException( errMsg );
367                                }
368                        }
369                }
370
371                return (nextFlag) ? data : null ;
372        }
373
374        /**
375         * プロセスの処理結果のレポート表現を返します。
376         * 処理プログラム名、入力件数、出力件数などの情報です。
377         * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
378         * 形式で出してください。
379         *
380         * @return   処理結果のレポート
381         */
382        public String report() {
383                String report = "[" + getClass().getName() + "]" + CR
384                                + TAB + "Search File Count : " + inCount    + CR
385                                + TAB + "Key Find    Count : " + findCount  + CR
386                                + TAB + "Key Change  Count : " + cngCount ;
387
388                return report ;
389        }
390
391        /**
392         * このクラスの使用方法を返します。
393         *
394         * @return      このクラスの使用方法
395         */
396        public String usage() {
397                StringBuilder buf = new StringBuilder();
398
399                buf.append( "Process_GrepChange は、上流から受け取った FileLineModelから、語句を"                        ).append( CR );
400                buf.append( "置換する、ChainProcess インターフェースの実装クラスです。"                                               ).append( CR );
401                buf.append( "Process_Grep との違いは、チェックするファイルのコピーを(キーワードが存在"               ).append( CR );
402                buf.append( "しなくとも)作成することと、検索キーに正規表現が使えない、複数行置き換えが"             ).append( CR );
403                buf.append( "出来ないことです。"                                                                                                                 ).append( CR );
404                buf.append( CR );
405                buf.append( "keywordFile より、置換する語句を含むキーと値のペアー(タブ区切り)を読取り、"      ).append( CR );
406                buf.append( "対象とする語句を置換します。"                                                                                                    ).append( CR );
407                buf.append( "keywordFile に、タブが含まれない行や、先頭にタブが存在している場合は、"         ).append( CR );
408                buf.append( "その行を読み飛ばします。また、区切りタブは何個存在しても構いません。"                        ).append( CR );
409                buf.append( "ただし、タブで区切った前(キー)と後ろ(値)は、trim() されますので、スペース"                ).append( CR );
410                buf.append( "が前後に存在している場合は、ご注意ください。"                                                                    ).append( CR );
411                buf.append( "置換文字(値)は、\t と \n の特殊文字が使用できます。"                                                    ).append( CR );
412                buf.append( "この GrepChange では、語句に、正規表現は使用できません。正規表現のキーワード"      ).append( CR );
413                buf.append( "や文字列を複数行の文字列と置き換える場合は、Process_Grep を使用して下さい。"      ).append( CR );
414                buf.append( "このプログラムでは、上流から受け取った FileLineModel のファイルに対して、"              ).append( CR );
415                buf.append( "置き換えた結果も、同じファイルにセーブします。"                                                           ).append( CR );
416                buf.append( "元のファイルを保存したい場合は、予めバックアップを取得しておいてください。"     ).append( CR );
417                buf.append( "-inEncode は、入力ファイルのエンコード指定になります。"                                          ).append( CR );
418                buf.append( "-outEncode は、出力ファイルのエンコードや、キーワードファイルのエンコード"        ).append( CR );
419                buf.append( "指定になります。(keywordFile は、必ず 出力ファイルと同じエンコードです。)"      ).append( CR );
420                buf.append( "これらのエンコードが無指定の場合は、System.getProperty(\"file.encoding\") "  ).append( CR );
421                buf.append( "で求まる値を使用します。"                                                                                                              ).append( CR );
422                buf.append( CR );
423                buf.append( "上流プロセスでは、Name 属性として、『File』を持ち、値は、Fileオブジェクト"               ).append( CR );
424                buf.append( "である、Process_FileSearch を使用するのが、便利です。それ以外のクラスを"             ).append( CR );
425                buf.append( "使用する場合でも、Name属性と、File オブジェクトを持つ LineModel を受け渡し"   ).append( CR );
426                buf.append( "できれば、使用可能です。"                                                                                                              ).append( CR );
427                buf.append( CR );
428                buf.append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。" ).append( CR );
429                buf.append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に"                ).append( CR );
430                buf.append( "繋げてください。"                                                                                                                          ).append( CR );
431                buf.append( CR ).append( CR );
432
433                buf.append( getArgument().usage() ).append( CR );
434
435                return buf.toString();
436        }
437
438        /**
439         * このクラスは、main メソッドから実行できません。
440         *
441         * @param       args    コマンド引数配列
442         */
443        public static void main( final String[] args ) {
444                LogWriter.log( new Process_GrepChange().usage() );
445        }
446}