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