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