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         * 初期処理(ファイルオープン、DBオープン等)に使用します。
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         * 終了処理(ファイルクローズ、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        public LineModel action( final LineModel data ) {
198                inCount++ ;
199                final FileLineModel fileData ;
200                if( data instanceof FileLineModel ) {
201                        fileData = (FileLineModel)data ;
202                }
203                else {
204                        final String errMsg = "データが FileLineModel オブジェクトではありません。" + CR ;
205                        throw new OgRuntimeException( errMsg );
206                }
207
208                final File org = fileData.getFile() ;
209                if( ! org.isFile() ) { return data; }
210
211                boolean nextFlag        = false;
212                final File                      orgFile = org.getAbsoluteFile();                                // 6.2.0.0 (2015/02/27)
213                final ExcelModel        excel   = new ExcelModel( orgFile );                    // 6.2.0.0 (2015/02/27)
214
215                final int maxStNo = excel.getNumberOfSheets();                                          // 6.0.2.0 (2014/09/19)
216                for( int stNo=0; stNo<maxStNo; stNo++ ) {
217                        final String sheetName = excel.getSheetName( stNo );            // ここで、ExcelModel内部に処理Sheetをセットします。
218                        if( display ) { println( orgFile + ":" + sheetName ); }
219                        final int nFirstRow = excel.getFirstRowNum();
220                        final int nLastRow  = excel.getLastRowNum();
221                        for( int rowNo=nFirstRow; rowNo<=nLastRow; rowNo++ ) {
222                                final String[] vals = excel.getValues( rowNo );
223                                // 6.3.9.0 (2015/11/06) ExcelModel#getValues(int) では、nullは返さない。
224                                for( int colNo=0; colNo<vals.length; colNo++ ) {
225                                        final String orgText = vals[colNo];
226                                        if( orgText != null && orgText.length() > 0 ) {
227                                                if( debug ) { println( "\tDATA: [" + rowNo + "," + colNo + "]=" + orgText ); }
228                                                final String strText = changeString( orgText );         // 文字列変換。無変換の場合は、null が返る。
229                                                if( strText != null ) {
230                                                        if( display ) { println( "\tFIND: [" + rowNo + "," + colNo + "]=" + orgText + "→" + strText ); }
231                                                        excel.setCellValue( strText,colNo );            // 書き戻し。行は先ほど getValues(int) した行
232                                                        nextFlag = true;
233                                                        findCount++;                    // 5.5.2.4 (2012/05/16)
234                                                }
235                                        }
236                                }
237                        }
238
239                        // シート名も変換対象とする。
240                        final String newSheetName = changeString( sheetName );  // 無変換の場合は、null が返る。
241                        if( newSheetName != null ) {
242                                if( display ) { println( "\tFIND sheetName=" + sheetName + "→" + newSheetName ); }
243                                excel.setSheetName( stNo,newSheetName );
244                                nextFlag = true;
245                                findCount++;                                    // 5.5.2.4 (2012/05/16)
246                        }
247                }
248
249                if( isChange && nextFlag ) {
250                        excel.saveFile( orgFile ) ;                     // 6.2.0.0 (2015/02/27)
251                        cngCount = findCount ;                          // 5.5.2.4 (2012/05/16) 置換時には、findCount を、cngCount にセットしておく。
252                }
253
254                return nextFlag ? data : null ;
255        }
256
257        /**
258         * 引数の文字列から、keyword ファイルを元に文字列変換を行います。
259         *
260         * ここでは、変換が行われたかどうかを判定するため、変換された場合
261         * のみ、値を返します。変換されない場合は、null を返しますので、
262         * ご注意ください。
263         *
264         * @param       org     変換前の文字列
265         *
266         * @return   変換後の文字列(変換がなければ、null を返します。)
267         */
268        public String changeString( final String org ) {
269                if( org == null || org.isEmpty() ) { return null; }
270
271                String tgt = org;
272                for( int i=0; i<keyword.length; i++ ) {
273                        tgt = tgt.replaceAll( keyword[i],change[i] );
274                }
275
276                // 元と同じ場合は、null を返します。
277                if( ignoreCase && org.equalsIgnoreCase( tgt ) || org.equals( tgt ) ) {          // 6.9.7.0 (2018/05/14) PMD Useless parentheses.
278                        tgt = null;
279                }
280
281                return tgt ;
282        }
283
284        /**
285         * プロセスの処理結果のレポート表現を返します。
286         * 処理プログラム名、入力件数、出力件数などの情報です。
287         * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
288         * 形式で出してください。
289         *
290         * @return   処理結果のレポート
291         */
292        public String report() {
293                // 7.2.9.5 (2020/11/28) PMD:Consider simply returning the value vs storing it in local variable 'XXXX'
294                return "[" + getClass().getName() + "]" + CR
295//              final String report = "[" + getClass().getName() + "]" + CR
296                                + TAB + "Search File Count : " + inCount    + CR
297                                + TAB + "Key Find    Count : " + findCount  + CR
298                                + TAB + "Key Change  Count : " + cngCount ;
299
300//              return report ;
301        }
302
303        /**
304         * このクラスの使用方法を返します。
305         *
306         * @return      このクラスの使用方法
307         * @og.rtnNotNull
308         */
309        public String usage() {
310                final StringBuilder buf = new StringBuilder( 1200 )
311                        .append( "Process_GrepChangeExcel は、上流から受け取った FileLineModelから、語句を"              ).append( CR )
312                        .append( "置換する、ChainProcess インターフェースの実装クラスです。"                                  ).append( CR )
313                        .append( "Process_GrepChange との違いは、入力元のファイルが、テキストファイルなのか、"      ).append( CR )
314                        .append( "ネイティブEXCELファイルなのかの違いです。"                                                                      ).append( CR )
315                        .append( CR )
316                        .append( "keywordFile より、置換する語句を含むキーと値のペアー(タブ区切り)を読取り、" ).append( CR )
317                        .append( "対象とする語句を置換します。"                                                                                                       ).append( CR )
318                        .append( "keywordFile に、タブが含まれない行や、先頭にタブが存在している場合は、"            ).append( CR )
319                        .append( "その行を読み飛ばします。また、区切りタブは何個存在しても構いません。"                   ).append( CR )
320                        .append( "ただし、タブで区切った前(キー)と後ろ(値)は、trim() されますので、スペース"   ).append( CR )
321                        .append( "が前後に存在している場合は、ご注意ください。"                                                                       ).append( CR )
322                        .append( "置換文字(値)は、\t と \n の特殊文字が使用できます。"                                                       ).append( CR )
323                        .append( "この GrepChangeExcel では、語句に、正規表現は使用できません。"                              ).append( CR )
324                        .append( "正規表現のキーワードや文字列を複数行の文字列と置き換える場合は、Process_Grep" ).append( CR )
325                        .append( "を使用して下さい。"                                                                                                                    ).append( CR )
326                        .append( "このプログラムでは、上流から受け取った FileLineModel のファイルに対して、" ).append( CR )
327                        .append( "置き換えた結果も、同じファイルにセーブします。"                                                              ).append( CR )
328                        .append( "元のファイルを保存したい場合は、予めバックアップを取得しておいてください。"        ).append( CR )
329                        .append( "-inEncode は、keywordFileのエンコード指定になります。"                                                ).append( CR )
330                        .append( "初期値は、互換性を持つため、System.getProperty(\"file.encoding\") ですが、"     ).append( CR )
331                        .append( "明示的に UTF-8 などを指定して統一しておいたほうが良いでしょう。"                          ).append( CR )
332                        .append( CR )
333                        .append( "上流プロセスでは、Name 属性として、『File』を持ち、値は、Fileオブジェクト"  ).append( CR )
334                        .append( "である、Process_FileSearch を使用するのが、便利です。それ以外のクラスを"                ).append( CR )
335                        .append( "使用する場合でも、Name属性と、File オブジェクトを持つ LineModel を受け渡し"      ).append( CR )
336                        .append( "できれば、使用可能です。"                                                                                                         ).append( CR )
337                        .append( CR )
338                        .append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。"    ).append( CR )
339                        .append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に"           ).append( CR )
340                        .append( "繋げてください。"                                                                                                                             ).append( CR )
341                        .append( CR ).append( CR )
342                        .append( getArgument().usage() ).append( CR );
343
344                return buf.toString();
345        }
346
347        /**
348         * このクラスは、main メソッドから実行できません。
349         *
350         * @param       args    コマンド引数配列
351         */
352        public static void main( final String[] args ) {
353                LogWriter.log( new Process_GrepChangeExcel().usage() );
354        }
355}