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