    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         * 初期処理(ファイルオープン、ＤＢオープン等)に使用します。
    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         * 終了処理(ファイルクローズ、ＤＢクローズ等)に使用します。
    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}