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 java.util.Map ;
019import java.util.LinkedHashMap ;
020import java.util.List ;
021import java.util.ArrayList ;
022import java.io.File;
023
024import org.opengion.fukurou.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
025import org.opengion.fukurou.system.LogWriter;
026import org.opengion.fukurou.util.Argument;
027import org.opengion.fukurou.util.StringUtil ;
028import org.opengion.fukurou.util.FileUtil;                                      // 6.4.5.2 (2016/05/06)
029import org.opengion.fukurou.model.ExcelModel;                           // 6.0.2.0 (2014/09/19)
030
031/**
032 * Process_GrepChangeExcel は、上流から受け取った FileLineModelから、語句を
033 * 置換する、ChainProcess インターフェースの実装クラスです。
034 *
035 * Process_GrepChange との違いは、入力元のファイルが、テキストファイルなのか、
036 * ネイティブEXCELファイルなのかの違いです。
037 *
038 * keywordFile より、置換する語句を含むキーと値のペアー(タブ区切り)を読取り、
039 * 対象とする語句をセル単位に置換します。
040 * keywordFile に、タブが含まれない行や、先頭にタブが存在している場合は、
041 * その行を読み飛ばします。また、区切りタブは何個存在しても構いません。
042 * ただし、タブで区切った前(キー)と後ろ(値)は、trim() されますので、スペース
043 * が前後に存在している場合は、ご注意ください。
044 * 置換文字(値)は、\t と \n の特殊文字が使用できます。
045 * この GrepChangeExcel では、語句に、正規表現は使用できません。正規表現のキーワード
046 * や文字列を複数行の文字列と置き換える場合は、Process_Grep を使用してください。
047 * このプログラムでは、上流から受け取った FileLineModel のファイルに対して、
048 * 置き換えた結果も、同じファイルにセーブします。
049 * 元のファイルを保存したい場合は、予めバックアップを取得しておいてください。
050 * -inEncode は、keywordFileのエンコード指定になります。
051 * 初期値は、互換性を持つため、System.getProperty("file.encoding") ですが、
052 * 明示的に UTF-8 などを指定して統一しておいたほうが良いでしょう。
053 *
054 * 上流プロセスでは、Name 属性として、『File』を持ち、値は、Fileオブジェクト
055 * である、Process_FileSearch を使用するのが、便利です。それ以外のクラスを
056 * 使用する場合でも、Name属性と、File オブジェクトを持つ LineModel を受け渡し
057 * できれば、使用可能です。
058 *
059 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
060 * 引数文字列の 『=』 の前後には、スペースは挟めません。必ず、-key=value の様に
061 * 繋げてください。
062 *
063 *  Process_GrepChangeExcel -keyword=検索文字列 -ignoreCase=true -outfile=OUTFILE -encode=UTF-8
064 *
065 *    -keywordFile=キーワード    :置換する語句を含むキーと値のペアー(タブ区切り)
066 *   [-ignoreCase=大文字小文字 ] :検索時に大文字小文字を区別しない(true)かどうか(初期値:false[区別する])
067 *   [-isChange=置換可否       ] :置換処理を実施する(true)かどうか(初期値:true[置換する])
068 *   [-inEncode=入力エンコード ] :keywordFileのエンコード
069 *   [-display=[false/true]    ] :結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
070 *   [-debug=[false/true]      ] :デバッグ用に実行内容を表示するかどうかを指定(初期値:false[表示しない])
071 *
072 * @og.rev 5.5.1.7 (2012/04/16) 新規追加
073 * @og.rev 6.0.2.0 (2014/09/19) fukurou.model.ExcelModel を使用するように変更
074 * @version  4.0
075 * @author   Kazuhiko Hasegawa
076 * @since    JDK5.0,
077 */
078public class Process_GrepChangeExcel extends AbstractProcess implements ChainProcess {
079        private String[]        keyword ;
080        private String[]        change  ;
081        private boolean         ignoreCase      ;
082        private boolean         isChange        = true;         // 5.1.2.0 (2010/01/01) 置換するかどうかを指定可能にする
083        private boolean         display         ;                       // false:表示しない
084        private boolean         debug           ;                       // false:表示しない
085
086        private int             inCount         ;
087        private int             findCount       ;
088        private int             cngCount        ;
089
090        /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
091        private static final Map<String,String> MUST_PROPARTY   ;               // [プロパティ]必須チェック用 Map
092        /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
093        private static final Map<String,String> USABLE_PROPARTY ;               // [プロパティ]整合性チェック Map
094
095        static {
096                MUST_PROPARTY = new LinkedHashMap<>();
097                MUST_PROPARTY.put( "keywordFile",       "置換する語句を含むキーと値のペアー(タブ区切り)(必須)" );
098
099                USABLE_PROPARTY = new LinkedHashMap<>();
100                USABLE_PROPARTY.put( "ignoreCase",      "検索時に大文字小文字を区別しない(true)かどうか。" +
101                                                                                CR + "(初期値:区別する[false])" );
102                USABLE_PROPARTY.put( "isChange",                "置換処理を実施する(true)かどうか" +
103                                                                                CR + "(初期値:置換する[true])" );
104                USABLE_PROPARTY.put( "inEncode",                "keywordFileのエンコード" );
105                USABLE_PROPARTY.put( "display",         "結果を標準出力に表示する(true)かしない(false)か" +
106                                                                                CR + "(初期値:false:表示しない)" );
107                USABLE_PROPARTY.put( "debug",           "デバッグ用に実行内容を表示するかどうかを指定" +
108                                                                                CR + "(初期値:false:表示しない)" );
109        }
110
111        /**
112         * デフォルトコンストラクター。
113         * このクラスは、動的作成されます。デフォルトコンストラクターで、
114         * super クラスに対して、必要な初期化を行っておきます。
115         *
116         */
117        public Process_GrepChangeExcel() {
118                super( "org.opengion.fukurou.process.Process_GrepChangeExcel",MUST_PROPARTY,USABLE_PROPARTY );
119        }
120
121        /**
122         * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
123         * 初期処理(ファイルオープン、DBオープン等)に使用します。
124         *
125         * @og.rev 6.4.5.1 (2016/04/28) FileStringのコンストラクター変更
126         * @og.rev 6.4.5.2 (2016/05/06) fukurou.util.FileString から、fukurou.util.FileUtil に移動。
127         *
128         * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
129         */
130        public void init( final ParamProcess paramProcess ) {
131                final Argument arg = getArgument();
132
133                final String keywordFile = arg.getProparty("keywordFile" );
134                ignoreCase              = arg.getProparty("ignoreCase",ignoreCase);
135                isChange                = arg.getProparty("isChange",isChange);                 // 5.1.2.0 (2010/01/01)
136                final String inEncode   = arg.getProparty("inEncode",System.getProperty("file.encoding"));
137                display                 = arg.getProparty("display",display);
138                debug                   = arg.getProparty("debug",debug);
139
140                // 6.4.5.1 (2016/04/28) FileStringのコンストラクター変更
141                final List<String> list = FileUtil.getLineList( keywordFile , inEncode );               // 6.4.5.2 (2016/05/06)
142                final int len = list.size();                                                            // 6.4.5.2 (2016/05/06)
143                if( len == 0 ) {
144                        final String errMsg = "keywordFile の内容が 読み取れませんでした。[" + keywordFile + "]" ;
145                        throw new OgRuntimeException( errMsg );
146                }
147
148                println( "keywordFile を、" + len + "件読み取りました。" );
149                final List<String> keyList = new ArrayList<>( len );
150                final List<String> cngList = new ArrayList<>( len );
151
152                for( final String line : list ) {
153        //              String line = lines[i].trim();
154                        final int indx = line.indexOf( '\t' );
155                        if( indx <= 0 ) { continue ; }  // TAB が先頭や、存在しない行は読み飛ばす。
156                        keyList.add( line.substring( 0,indx ).trim() );
157                        String cng = line.substring( indx+1 ).trim();
158                        cng = StringUtil.replace( cng,"\\n",CR );
159                        cng = StringUtil.replace( cng,"\\t","\t" );
160                        cngList.add( cng );
161                }
162//              keyword = keyList.toArray( new String[keyList.size()] );
163//              change  = cngList.toArray( new String[cngList.size()] );
164                keyword = keyList.toArray( new String[0] );     // 8.5.4.2 (2024/01/12) PMD 7.0.0 OptimizableToArrayCall 対応
165                change  = cngList.toArray( new String[0] );     // 8.5.4.2 (2024/01/12) PMD 7.0.0 OptimizableToArrayCall 対応
166        }
167
168        /**
169         * プロセスの終了を行います。最後に一度だけ、呼び出されます。
170         * 終了処理(ファイルクローズ、DBクローズ等)に使用します。
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( CHAIN_FILE_USAGE )             // 8.5.6.1 (2024/03/29) 継承元使用
339                        .append( CR )
340//                      .append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。"    ).append( CR )
341//                      .append( "引数文字列の 『=』 の前後には、空白は挟めません。必ず、-key=value の様に"          ).append( CR )
342//                      .append( "繋げてください。"                                                                                                                             ).append( CR )
343                        .append( PROCESS_PARAM_USAGE )  // 8.5.6.1 (2024/03/29) 継承元使用
344                        .append( CR ).append( CR )
345                        .append( getArgument().usage() ).append( CR );
346
347                return buf.toString();
348        }
349
350        /**
351         * このクラスは、main メソッドから実行できません。
352         *
353         * @param       args    コマンド引数配列
354         */
355        public static void main( final String[] args ) {
356                LogWriter.log( new Process_GrepChangeExcel().usage() );
357        }
358}