    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.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
    019import org.opengion.fukurou.util.Argument;
    020import org.opengion.fukurou.system.LogWriter;
    021import org.opengion.fukurou.util.StringUtil ;
    022import org.opengion.fukurou.util.FileUtil;                                      // 6.4.5.2 (2016/05/06)
    023
    024import org.opengion.fukurou.model.ExcelModel;                           // 6.0.2.0 (2014/09/19)
    025
    026import java.util.Map ;
    027import java.util.LinkedHashMap ;
    028import java.util.List ;
    029import java.util.ArrayList ;
    030
    031import java.io.File;
    032
    033/**
    034 * Process_GrepChangeExcel は、上流から受け取った FileLineModelから、語句を
    035 * 置換する、ChainProcess インターフェースの実装クラスです。
    036 *
    037 * Process_GrepChange との違いは、入力元のファイルが、テキストファイルなのか、
    038 * ネイティブEXCELファイルなのかの違いです。
    039 *
    040 * keywordFile より、置換する語句を含むキーと値のペアー(タブ区切り)を読取り、
    041 * 対象とする語句をセル単位に置換します。
    042 * keywordFile に、タブが含まれない行や、先頭にタブが存在している場合は、
    043 * その行を読み飛ばします。また、区切りタブは何個存在しても構いません。
    044 * ただし、タブで区切った前(キー)と後ろ(値)は、trim() されますので、スペース
    045 * が前後に存在している場合は、ご注意ください。
    046 * 置換文字(値)は、\t と \n の特殊文字が使用できます。
    047 * この GrepChangeExcel では、語句に、正規表現は使用できません。正規表現のキーワード
    048 * や文字列を複数行の文字列と置き換える場合は、Process_Grep を使用してください。
    049 * このプログラムでは、上流から受け取った FileLineModel のファイルに対して、
    050 * 置き換えた結果も、同じファイルにセーブします。
    051 * 元のファイルを保存したい場合は、予めバックアップを取得しておいてください。
    052 * -inEncode は、keywordFileのエンコード指定になります。
    053 * 初期値は、互換性を持つため、System.getProperty("file.encoding") ですが、
    054 * 明示的に UTF-8 などを指定して統一しておいたほうが良いでしょう。
    055 *
    056 * 上流プロセスでは、Name 属性として、『File』を持ち、値は、Fileオブジェクト
    057 * である、Process_FileSearch を使用するのが、便利です。それ以外のクラスを
    058 * 使用する場合でも、Name属性と、File オブジェクトを持つ LineModel を受け渡し
    059 * できれば、使用可能です。
    060 *
    061 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
    062 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に
    063 * 繋げてください。
    064 *
    065 *  Process_GrepChangeExcel -keyword=検索文字列 -ignoreCase=true -outfile=OUTFILE -encode=UTF-8
    066 *
    067 *    -keywordFile=キーワード    ：置換する語句を含むキーと値のペアー(タブ区切り)
    068 *   [-ignoreCase=大文字小文字 ] ：検索時に大文字小文字を区別しない(true)かどうか(初期値:false[区別する])
    069 *   [-isChange=置換可否       ] ：置換処理を実施する(true)かどうか(初期値:true[置換する])
    070 *   [-inEncode=入力エンコード ] ：keywordFileのエンコード
    071 *   [-display=[false/true]    ] ：結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
    072 *   [-debug=[false/true]      ] ：デバッグ用に実行内容を表示するかどうかを指定(初期値:false[表示しない])
    073 *
    074 * @og.rev 5.5.1.7 (2012/04/16) 新規追加
    075 * @og.rev 6.0.2.0 (2014/09/19) fukurou.model.ExcelModel を使用するように変更
    076 * @version  4.0
    077 * @author   Kazuhiko Hasegawa
    078 * @since    JDK5.0,
    079 */
    080public class Process_GrepChangeExcel extends AbstractProcess implements ChainProcess {
    081        private String[]        keyword ;
    082        private String[]        change  ;
    083        private boolean         ignoreCase      ;
    084        private boolean         isChange        = true;         // 5.1.2.0 (2010/01/01) 置換するかどうかを指定可能にする
    085        private boolean         display         ;                       // false:表示しない
    086        private boolean         debug           ;                       // false:表示しない
    087
    088        private int             inCount         ;
    089        private int             findCount       ;
    090        private int             cngCount        ;
    091
    092        /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
    093        private static final Map<String,String> MUST_PROPARTY   ;               // ［プロパティ］必須チェック用 Map
    094        /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
    095        private static final Map<String,String> USABLE_PROPARTY ;               // ［プロパティ］整合性チェック Map
    096
    097        static {
    098                MUST_PROPARTY = new LinkedHashMap<>();
    099                MUST_PROPARTY.put( "keywordFile",       "置換する語句を含むキーと値のペアー(タブ区切り)(必須)" );
    100
    101                USABLE_PROPARTY = new LinkedHashMap<>();
    102                USABLE_PROPARTY.put( "ignoreCase",      "検索時に大文字小文字を区別しない(true)かどうか。" +
    103                                                                                CR + "(初期値:区別する[false])" );
    104                USABLE_PROPARTY.put( "isChange",                "置換処理を実施する(true)かどうか" +
    105                                                                                CR + "(初期値:置換する[true])" );
    106                USABLE_PROPARTY.put( "inEncode",                "keywordFileのエンコード" );
    107                USABLE_PROPARTY.put( "display",         "結果を標準出力に表示する(true)かしない(false)か" +
    108                                                                                CR + "(初期値:false:表示しない)" );
    109                USABLE_PROPARTY.put( "debug",           "デバッグ用に実行内容を表示するかどうかを指定" +
    110                                                                                CR + "(初期値:false:表示しない)" );
    111        }
    112
    113        /**
    114         * デフォルトコンストラクター。
    115         * このクラスは、動的作成されます。デフォルトコンストラクターで、
    116         * super クラスに対して、必要な初期化を行っておきます。
    117         *
    118         */
    119        public Process_GrepChangeExcel() {
    120                super( "org.opengion.fukurou.process.Process_GrepChangeExcel",MUST_PROPARTY,USABLE_PROPARTY );
    121        }
    122
    123        /**
    124         * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
    125         * 初期処理(ファイルオープン、ＤＢオープン等)に使用します。
    126         *
    127         * @og.rev 6.4.5.1 (2016/04/28) FileStringのコンストラクター変更
    128         * @og.rev 6.4.5.2 (2016/05/06) fukurou.util.FileString から、fukurou.util.FileUtil に移動。
    129         *
    130         * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
    131         */
    132        public void init( final ParamProcess paramProcess ) {
    133                final Argument arg = getArgument();
    134
    135                final String keywordFile = arg.getProparty("keywordFile" );
    136                ignoreCase              = arg.getProparty("ignoreCase",ignoreCase);
    137                isChange                = arg.getProparty("isChange",isChange);                 // 5.1.2.0 (2010/01/01)
    138                final String inEncode   = arg.getProparty("inEncode",System.getProperty("file.encoding"));
    139                display                 = arg.getProparty("display",display);
    140                debug                   = arg.getProparty("debug",debug);
    141
    142                // 6.4.5.1 (2016/04/28) FileStringのコンストラクター変更
    143                final List<String> list = FileUtil.getLineList( keywordFile , inEncode );               // 6.4.5.2 (2016/05/06)
    144                final int len = list.size();                                                            // 6.4.5.2 (2016/05/06)
    145                if( len == 0 ) {
    146                        final String errMsg = "keywordFile の内容が 読み取れませんでした。[" + keywordFile + "]" ;
    147                        throw new OgRuntimeException( errMsg );
    148                }
    149
    150                println( "keywordFile を、" + len + "件読み取りました。" );
    151                final List<String> keyList = new ArrayList<>( len );
    152                final List<String> cngList = new ArrayList<>( len );
    153
    154                for( final String line : list ) {
    155        //              String line = lines[i].trim();
    156                        final int indx = line.indexOf( '\t' );
    157                        if( indx <= 0 ) { continue ; }  // TAB が先頭や、存在しない行は読み飛ばす。
    158                        keyList.add( line.substring( 0,indx ).trim() );
    159                        String cng = line.substring( indx+1 ).trim();
    160                        cng = StringUtil.replace( cng,"\\n",CR );
    161                        cng = StringUtil.replace( cng,"\\t","\t" );
    162                        cngList.add( cng );
    163                }
    164                keyword = keyList.toArray( new String[keyList.size()] );
    165                change  = cngList.toArray( new String[cngList.size()] );
    166        }
    167
    168        /**
    169         * プロセスの終了を行います。最後に一度だけ、呼び出されます。
    170         * 終了処理(ファイルクローズ、ＤＢクローズ等)に使用します。
    171         *
    172         * @param   isOK トータルで、OKだったかどうか[true:成功/false:失敗]
    173         */
    174        public void end( final boolean isOK ) {
    175                // ここでは処理を行いません。
    176        }
    177
    178        /**
    179         * 引数の LineModel を処理するメソッドです。
    180         * 変換処理後の LineModel を返します。
    181         * 後続処理を行わない場合(データのフィルタリングを行う場合)は、
    182         * null データを返します。つまり、null データは、後続処理を行わない
    183         * フラグの代わりにも使用しています。
    184         * なお、変換処理後の LineModel と、オリジナルの LineModel が、
    185         * 同一か、コピー(クローン)かは、各処理メソッド内で決めています。
    186         * ドキュメントに明記されていない場合は、副作用が問題になる場合は、
    187         * 各処理ごとに自分でコピー(クローン)して下さい。
    188         *
    189         * @og.rev 5.7.2.2 (2014/01/24) エラー時にデータも出力します。
    190         * @og.rev 6.0.2.0 (2014/09/19) org.opengion.fukurou.model.ExcelModel を使うように変更。
    191         * @og.rev 6.3.9.0 (2015/11/06) ExcelModel#getValues(int) では、nullは返さない。
    192         *
    193         * @param   data        オリジナルのLineModel
    194         *
    195         * @return      処理変換後のLineModel
    196         */
    197        @Override       // ChainProcess
    198        public LineModel action( final LineModel data ) {
    199                inCount++ ;
    200                final FileLineModel fileData ;
    201                if( data instanceof FileLineModel ) {
    202                        fileData = (FileLineModel)data ;
    203                }
    204                else {
    205                        final String errMsg = "データが FileLineModel オブジェクトではありません。" + CR ;
    206                        throw new OgRuntimeException( errMsg );
    207                }
    208
    209                final File org = fileData.getFile() ;
    210                if( ! org.isFile() ) { return data; }
    211
    212                boolean nextFlag        = false;
    213                final File                      orgFile = org.getAbsoluteFile();                                // 6.2.0.0 (2015/02/27)
    214                final ExcelModel        excel   = new ExcelModel( orgFile );                    // 6.2.0.0 (2015/02/27)
    215
    216                final int maxStNo = excel.getNumberOfSheets();                                          // 6.0.2.0 (2014/09/19)
    217                for( int stNo=0; stNo<maxStNo; stNo++ ) {
    218                        final String sheetName = excel.getSheetName( stNo );            // ここで、ExcelModel内部に処理Sheetをセットします。
    219                        if( display ) { println( orgFile + ":" + sheetName ); }
    220                        final int nFirstRow = excel.getFirstRowNum();
    221                        final int nLastRow  = excel.getLastRowNum();
    222                        for( int rowNo=nFirstRow; rowNo<=nLastRow; rowNo++ ) {
    223                                final String[] vals = excel.getValues( rowNo );
    224                                // 6.3.9.0 (2015/11/06) ExcelModel#getValues(int) では、nullは返さない。
    225                                for( int colNo=0; colNo<vals.length; colNo++ ) {
    226                                        final String orgText = vals[colNo];
    227                                        if( orgText != null && orgText.length() > 0 ) {
    228                                                if( debug ) { println( "\tDATA: [" + rowNo + "," + colNo + "]=" + orgText ); }
    229                                                final String strText = changeString( orgText );         // 文字列変換。無変換の場合は、null が返る。
    230                                                if( strText != null ) {
    231                                                        if( display ) { println( "\tFIND: [" + rowNo + "," + colNo + "]=" + orgText + "→" + strText ); }
    232                                                        excel.setCellValue( strText,colNo );            // 書き戻し。行は先ほど getValues(int) した行
    233                                                        nextFlag = true;
    234                                                        findCount++;                    // 5.5.2.4 (2012/05/16)
    235                                                }
    236                                        }
    237                                }
    238                        }
    239
    240                        // シート名も変換対象とする。
    241                        final String newSheetName = changeString( sheetName );  // 無変換の場合は、null が返る。
    242                        if( newSheetName != null ) {
    243                                if( display ) { println( "\tFIND sheetName=" + sheetName + "→" + newSheetName ); }
    244                                excel.setSheetName( stNo,newSheetName );
    245                                nextFlag = true;
    246                                findCount++;                                    // 5.5.2.4 (2012/05/16)
    247                        }
    248                }
    249
    250                if( isChange && nextFlag ) {
    251                        excel.saveFile( orgFile ) ;                     // 6.2.0.0 (2015/02/27)
    252                        cngCount = findCount ;                          // 5.5.2.4 (2012/05/16) 置換時には、findCount を、cngCount にセットしておく。
    253                }
    254
    255                return nextFlag ? data : null ;
    256        }
    257
    258        /**
    259         * 引数の文字列から、keyword ファイルを元に文字列変換を行います。
    260         *
    261         * ここでは、変換が行われたかどうかを判定するため、変換された場合
    262         * のみ、値を返します。変換されない場合は、null を返しますので、
    263         * ご注意ください。
    264         *
    265         * @param       org     変換前の文字列
    266         *
    267         * @return   変換後の文字列(変換がなければ、null を返します。)
    268         */
    269        public String changeString( final String org ) {
    270                if( org == null || org.isEmpty() ) { return null; }
    271
    272                String tgt = org;
    273                for( int i=0; i<keyword.length; i++ ) {
    274                        tgt = tgt.replaceAll( keyword[i],change[i] );
    275                }
    276
    277                // 元と同じ場合は、null を返します。
    278                if( ignoreCase && org.equalsIgnoreCase( tgt ) || org.equals( tgt ) ) {          // 6.9.7.0 (2018/05/14) PMD Useless parentheses.
    279                        tgt = null;
    280                }
    281
    282                return tgt ;
    283        }
    284
    285        /**
    286         * プロセスの処理結果のレポート表現を返します。
    287         * 処理プログラム名、入力件数、出力件数などの情報です。
    288         * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
    289         * 形式で出してください。
    290         *
    291         * @return   処理結果のレポート
    292         */
    293        public String report() {
    294                // 7.2.9.5 (2020/11/28) PMD:Consider simply returning the value vs storing it in local variable 'XXXX'
    295                return "[" + getClass().getName() + "]" + CR
    296//              final String report = "[" + getClass().getName() + "]" + CR
    297                                + TAB + "Search File Count : " + inCount    + CR
    298                                + TAB + "Key Find    Count : " + findCount  + CR
    299                                + TAB + "Key Change  Count : " + cngCount ;
    300
    301//              return report ;
    302        }
    303
    304        /**
    305         * このクラスの使用方法を返します。
    306         *
    307         * @return      このクラスの使用方法
    308         * @og.rtnNotNull
    309         */
    310        public String usage() {
    311                final StringBuilder buf = new StringBuilder( 1200 )
    312                        .append( "Process_GrepChangeExcel は、上流から受け取った FileLineModelから、語句を"              ).append( CR )
    313                        .append( "置換する、ChainProcess インターフェースの実装クラスです。"                                  ).append( CR )
    314                        .append( "Process_GrepChange との違いは、入力元のファイルが、テキストファイルなのか、"      ).append( CR )
    315                        .append( "ネイティブEXCELファイルなのかの違いです。"                                                                      ).append( CR )
    316                        .append( CR )
    317                        .append( "keywordFile より、置換する語句を含むキーと値のペアー(タブ区切り)を読取り、" ).append( CR )
    318                        .append( "対象とする語句を置換します。"                                                                                                       ).append( CR )
    319                        .append( "keywordFile に、タブが含まれない行や、先頭にタブが存在している場合は、"            ).append( CR )
    320                        .append( "その行を読み飛ばします。また、区切りタブは何個存在しても構いません。"                   ).append( CR )
    321                        .append( "ただし、タブで区切った前(キー)と後ろ(値)は、trim() されますので、スペース"   ).append( CR )
    322                        .append( "が前後に存在している場合は、ご注意ください。"                                                                       ).append( CR )
    323                        .append( "置換文字(値)は、\t と \n の特殊文字が使用できます。"                                                       ).append( CR )
    324                        .append( "この GrepChangeExcel では、語句に、正規表現は使用できません。"                              ).append( CR )
    325                        .append( "正規表現のキーワードや文字列を複数行の文字列と置き換える場合は、Process_Grep" ).append( CR )
    326                        .append( "を使用して下さい。"                                                                                                                    ).append( CR )
    327                        .append( "このプログラムでは、上流から受け取った FileLineModel のファイルに対して、" ).append( CR )
    328                        .append( "置き換えた結果も、同じファイルにセーブします。"                                                              ).append( CR )
    329                        .append( "元のファイルを保存したい場合は、予めバックアップを取得しておいてください。"        ).append( CR )
    330                        .append( "-inEncode は、keywordFileのエンコード指定になります。"                                                ).append( CR )
    331                        .append( "初期値は、互換性を持つため、System.getProperty(\"file.encoding\") ですが、"     ).append( CR )
    332                        .append( "明示的に UTF-8 などを指定して統一しておいたほうが良いでしょう。"                          ).append( CR )
    333                        .append( CR )
    334                        .append( "上流プロセスでは、Name 属性として、『File』を持ち、値は、Fileオブジェクト"  ).append( CR )
    335                        .append( "である、Process_FileSearch を使用するのが、便利です。それ以外のクラスを"                ).append( CR )
    336                        .append( "使用する場合でも、Name属性と、File オブジェクトを持つ LineModel を受け渡し"      ).append( CR )
    337                        .append( "できれば、使用可能です。"                                                                                                         ).append( CR )
    338                        .append( CR )
    339                        .append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。"    ).append( CR )
    340                        .append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に"           ).append( CR )
    341                        .append( "繋げてください。"                                                                                                                             ).append( CR )
    342                        .append( CR ).append( CR )
    343                        .append( getArgument().usage() ).append( CR );
    344
    345                return buf.toString();
    346        }
    347
    348        /**
    349         * このクラスは、main メソッドから実行できません。
    350         *
    351         * @param       args    コマンド引数配列
    352         */
    353        public static void main( final String[] args ) {
    354                LogWriter.log( new Process_GrepChangeExcel().usage() );
    355        }
    356}