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 ;
020
021import java.io.File;
022import java.io.PrintWriter;
023
024import org.opengion.fukurou.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
025import org.opengion.fukurou.util.Argument;
026import org.opengion.fukurou.util.HybsEntry ;
027import org.opengion.fukurou.util.FileUtil;
028import org.opengion.fukurou.system.Closer ;
029import org.opengion.fukurou.system.LogWriter;
030import org.opengion.fukurou.model.Formatter;                            // 6.3.2.0 (2015/07/10)
031
032/**
033 * Process_TableWriter は、上流から受け取ったデータをファイルに書き込む
034 * CainProcess インターフェースの実装クラスです。
035 *
036 * 上流(プロセスチェインのデータは上流から下流へと渡されます。)から
037 * 受け取ったLineModel を元に、DBTableModel 形式ファイルを出力します。
038 *
039 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
040 * 引数文字列の 『=』 の前後には、スペースは挟めません。必ず、-key=value の様に
041 * 繋げてください。
042 *
043 * @og.formSample
044 *  Process_TableWriter -outfile=OUTFILE -sep=, -encode=UTF-8 -append=true
045 *
046 *    -outfile=出力ファイル名         :出力ファイル名
047 *   [-sep=セパレータ文字           ] :区切り文字(初期値:タブ)
048 *   [-encode=文字エンコード        ] :出力ファイルのエンコードタイプ
049 *   [-append=[false/true]      ] :出力ファイルを、追記する(true)か新規作成する(false)か。
050 *   [-useHeader=[true/false]   ] :ヘッダー情報(#NAME行)を出力する(true)か出力しない(false)か。
051 *   [-useNumber=[true/false]   ] :行番号を出力する(true)か出力しない(false)か。
052 *   [-useWquot=[false/true]    ] :出力データをダブルクオーテーションで括る(true)かそのまま(false)か。
053 *   [-useDataWquot=[true/false]] :出力データ上のダブルクオーテーションを2重にする(true)かそのまま(false)か。
054 *   [-omitCTRL=[false/true]    ] :コントロール文字を削除する(true)かそのまま(false)か。
055 *   [-const_XXXX=固定値        ] :-const_FGJ=1
056 *                                      LineModel のキー(const_ に続く文字列)の値に、固定値を設定します。
057 *                                     キーが異なれば、複数のカラム名を指定できます。
058 *   [-lineFormat=出力形式      ] :1行分のフォーマットを指定します。
059 *   [-display=[false/true]     ] :結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
060 *   [-debug=[false/true]       ] :デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
061 *
062 * @version  4.0
063 * @author   Kazuhiko Hasegawa
064 * @since    JDK5.0,
065 */
066public class Process_TableWriter extends AbstractProcess implements ChainProcess {
067        private static final String CNST_KEY = "const_" ;
068
069        private String  outfile         ;
070        private PrintWriter writer      ;
071        private char    separator       = TAB;                  // 6.0.2.5 (2014/10/31) TAB を char 化
072
073        private String[]        cnstClm         ;                       // 固定値を設定するカラム名
074        private int[]           cnstClmNos      ;                       // 固定値を設定するのカラム番号
075        private String[]        constVal        ;                       // カラム番号に対応した固定値
076        private File            file            ;                       // 出力ファイル
077        private String          encode          = System.getProperty("file.encoding");  // 出力ファイルエンコード
078        private boolean         append          ;                       // ファイル追加(true:追加/false:通常)
079        private boolean         useHeader       = true;         // ヘッダー情報(#NAME行)を出力する(true)か出力しない(false)か。
080        private boolean         useNumber       = true;         // 行番号を出力する(true)か出力しない(false)か。
081        private boolean         useWquot        ;                       // 出力データをダブルクオーテーションで括る(true)かそのまま(false)か。
082        private boolean         useDataWquot= true;             // 5.9.10.3 (2016/07/15) データ上のダブルクオーテーションを重ねる(true)かそのまま(false)か。
083        private boolean         omitCTRL        ;                       // コントロール文字を削除する(true)かそのまま(false)か。
084        private String          lineFormat      ;                       // 6.3.2.0 (2015/07/10) 1行分のフォーマット
085        private boolean         display         ;                       // 表示しない
086        private boolean         debug           ;                       // 5.7.3.0 (2014/02/07) デバッグ情報
087
088        private boolean firstRow        = true;                 // 最初の一行目
089        private int             count           ;
090
091        private Formatter format        ;                               // 6.3.2.0 (2015/07/10)
092
093        /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
094        private static final Map<String,String> MUST_PROPARTY   ;               // [プロパティ]必須チェック用 Map
095        /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
096        private static final Map<String,String> USABLE_PROPARTY ;               // [プロパティ]整合性チェック Map
097
098        static {
099                MUST_PROPARTY = new LinkedHashMap<>();
100                MUST_PROPARTY.put( "outfile",   "出力ファイル名 (必須)" );
101
102                USABLE_PROPARTY = new LinkedHashMap<>();
103                USABLE_PROPARTY.put( "sep",                     "区切り文字(初期値:タブ)" );
104                USABLE_PROPARTY.put( "encode",          "出力ファイルのエンコードタイプ" );
105                USABLE_PROPARTY.put( "append",          "出力ファイルを、追記する(true)か新規作成する(false)か。" );
106                USABLE_PROPARTY.put( "useHeader",       "ヘッダー情報(#NAME行)を出力する(true)か出力しない(false)か。" );
107                USABLE_PROPARTY.put( "useNumber",       "行番号を出力する(true)か出力しない(false)か。" );
108                USABLE_PROPARTY.put( "useWquot",        "出力データをダブルクオーテーションで括る(true)かそのまま(false)か。" );
109                USABLE_PROPARTY.put( "useDataWquot","出力データ中のダブルクオーテーションを重ねる(true)かそのまま(false)か。" );  // 5.9.10.3 (2016/07/15)
110                USABLE_PROPARTY.put( "omitCTRL",        "コントロール文字を削除する(true)かそのまま(false)か。" );
111                USABLE_PROPARTY.put( "const_",          "LineModel のキー(const_ に続く文字列)の値に、固定値を" +
112                                                                                CR + "設定します。キーが異なれば、複数のカラム名を指定できます。" +
113                                                                                CR + "例: -const_FGJ=1" );
114                USABLE_PROPARTY.put( "lineFormat","1行分のフォーマットを指定します。" );                // 6.3.2.0 (2015/07/10)
115                USABLE_PROPARTY.put( "display",         "結果を標準出力に表示する(true)かしない(false)か" +
116                                                                                CR + " (初期値:false:表示しない)" );
117                USABLE_PROPARTY.put( "debug",           "デバッグ情報を標準出力に表示する(true)かしない(false)か" +
118                                                                                CR + "(初期値:false:表示しない)" );             // 5.7.3.0 (2014/02/07) デバッグ情報
119        }
120
121        /**
122         * デフォルトコンストラクター。
123         * このクラスは、動的作成されます。デフォルトコンストラクターで、
124         * super クラスに対して、必要な初期化を行っておきます。
125         *
126         */
127        public Process_TableWriter() {
128                super( "org.opengion.fukurou.process.Process_TableWriter",MUST_PROPARTY,USABLE_PROPARTY );
129        }
130
131        /**
132         * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
133         * 初期処理(ファイルオープン、DBオープン等)に使用します。
134         *
135         * @og.rev 6.3.2.0 (2015/07/10) 1行分のフォーマット(lineFormat属性)対応
136         * @og.rev 5.9.10.3 (2016/07/15) ダブルクオートを重ねるかどうかの判定useDataWquot追加
137         *
138         * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
139         */
140        public void init( final ParamProcess paramProcess ) {
141                final Argument arg = getArgument();
142
143                outfile                 = arg.getProparty("outfile");
144                encode                  = arg.getProparty("encode",encode);
145                append                  = arg.getProparty("append",append);
146                useHeader               = arg.getProparty("useHeader",useHeader);
147                useNumber               = arg.getProparty("useNumber",useNumber);
148                useWquot                = arg.getProparty("useWquot",useWquot);
149                useDataWquot    = arg.getProparty("useDataWquot",useDataWquot); // 5.9.10.3 (2016/07/15)
150                omitCTRL                = arg.getProparty("omitCTRL",omitCTRL);
151                final HybsEntry[] cnstKey = arg.getEntrys( CNST_KEY );                  // 配列
152                lineFormat              = arg.getProparty("lineFormat",lineFormat);             // 6.3.2.0 (2015/07/10)
153                display                 = arg.getProparty("display",display);
154                debug                   = arg.getProparty("debug",debug);                               // 5.7.3.0 (2014/02/07) デバッグ情報
155
156                // 6.0.2.5 (2014/10/31) TAB を char 化
157                final String sep = arg.getProparty( "sep",null );
158                if( sep != null && sep.length() > 0 ) { separator = sep.charAt(0); }
159
160                final int size   = cnstKey.length;
161                cnstClm    = new String[size];
162                constVal   = new String[size];
163                for( int i=0; i<size; i++ ) {
164                        cnstClm[i]  = cnstKey[i].getKey();
165                        constVal[i] = cnstKey[i].getValue();
166                }
167
168                if( outfile == null ) {
169                        final String errMsg = "ファイル名が指定されていません。" ;
170                        throw new OgRuntimeException( errMsg );
171                }
172
173                file = new File( outfile );
174                final File dir = file.getParentFile() ;
175
176                // ディレクトリが存在しない場合の処理
177                if( ! dir.exists() && ! dir.mkdirs() ) {
178                        final String errMsg = "ディレクトリが作成できませんでした。[" + dir + "]" ;
179                        throw new OgRuntimeException( errMsg );
180                }
181        }
182
183        /**
184         * プロセスの終了を行います。最後に一度だけ、呼び出されます。
185         * 終了処理(ファイルクローズ、DBクローズ等)に使用します。
186         *
187         * @param   isOK トータルで、OKだったかどうか[true:成功/false:失敗]
188         */
189        public void end( final boolean isOK ) {
190                if( writer != null ) {
191                        writer.flush();
192                        Closer.ioClose( writer );
193                        writer = null;
194                }
195        }
196
197        /**
198         * 引数の LineModel を処理するメソッドです。
199         * 変換処理後の LineModel を返します。
200         * 後続処理を行わない場合(データのフィルタリングを行う場合)は、
201         * null データを返します。つまり、null データは、後続処理を行わない
202         * フラグの代わりにも使用しています。
203         * なお、変換処理後の LineModel と、オリジナルの LineModel が、
204         * 同一か、コピー(クローン)かは、各処理メソッド内で決めています。
205         * ドキュメントに明記されていない場合は、副作用が問題になる場合は、
206         * 各処理ごとに自分でコピー(クローン)して下さい。
207         *
208         * @og.rev 6.3.2.0 (2015/07/10) 1行分のフォーマット(lineFormat属性)対応
209         * @og.rev 6.4.3.4 (2016/03/11) Formatterに新しいコンストラクターを追加する。
210         *
211         * @param   data        オリジナルのLineModel
212         *
213         * @return      処理変換後のLineModel
214         */
215        @Override       // ChainProcess
216        public LineModel action( final LineModel data ) {
217                count++ ;
218        //      if( display ) { println( data.dataLine() ); }
219                if( firstRow ) {
220                        writer = FileUtil.getPrintWriter( file,encode,append );
221                        if( useHeader && useNumber ) { writeName( data ); }
222
223                        final int size   = cnstClm.length;
224                        cnstClmNos = new int[size];
225                        for( int i=0; i<size; i++ ) {
226                                cnstClmNos[i] = data.getColumnNo( cnstClm[i] );
227                        }
228
229                        // 6.3.2.0 (2015/07/10) 1行分のフォーマット(lineFormat属性)対応
230                        if( lineFormat != null ) {
231                                format = new Formatter( data,lineFormat );              // 6.4.3.4 (2016/03/11)
232                        }
233
234                        firstRow = false;
235                        if( display ) { println( data.nameLine() ); }           // 5.7.3.0 (2014/02/07) デバッグ情報
236                }
237
238                // 固定値置き換え処理
239                for( int j=0; j<cnstClmNos.length; j++ ) {
240                        data.setValue( cnstClmNos[j],constVal[j] );
241                }
242
243                // 6.3.2.0 (2015/07/10) 1行分のフォーマット(lineFormat属性)対応
244                if( lineFormat == null ) {
245                        writeData( data );
246                }
247                else {
248                        final String fmtVal = format.getLineFormatString( data );
249                        writer.println( fmtVal );
250                }
251
252                if( display ) { println( data.dataLine() ); }   // 5.1.2.0 (2010/01/01) display の条件変更
253                return data;
254        }
255
256        /**
257         * PrintWriter に LineModelの項目名情報を書き込みます。
258         * 第一カラム目は、項目名情報を示す "#Name" を書き込みます。
259         *
260         * seHeader=true &amp;&amp; useNumber=true の場合のみ、このメソッドが呼ばれます。
261         *
262         * @og.rev 6.0.4.0 (2014/11/28) #NAME 行の区切り文字は、指定の区切り文字を利用する。
263         *
264         * @param       data ラインモデル
265         */
266        private void writeName( final LineModel data ) {
267                final int size = data.size();
268                writer.print( "#Name" );
269                for( int clm=0; clm<size; clm++ ) {
270                        writer.print( separator );                              // 6.0.4.0 (2014/11/28) #NAME 行の区切り文字
271                        writer.print( data.getName(clm) );
272                }
273                writer.println();
274        }
275
276        /**
277         * PrintWriter に LineModelのテーブル情報を書き込みます。
278         *
279         * @og.rev 5.2.2.0 (2010/11/01) 改行を含む場合は、ダブルクオートを強制的に前後に追加する。
280         * @og.rev 5.2.2.0 (2010/11/01) ダブルクオートを含む場合は、その直前にダブルクオートを強制的に追加する。
281         * @og.rev 5.9.10.3 (2016/07/15) ダブルクオートを重ねるかどうかの判定useDataWquot追加
282         *
283         * @param       data ラインモデル
284         */
285        private void writeData( final LineModel data ) {
286                final int size = data.size();
287
288                if( useNumber ) { writer.print( data.getRowNo() ); }            // 行番号
289                for( int clm=0; clm<size; clm++ ) {
290                        if( useNumber || clm!=0 ) { writer.print( separator ); }
291                        Object val = data.getValue(clm);
292                        if( val == null ) { val = ""; }
293
294                        String sval = String.valueOf( val );
295                        // 5.2.2.0 (2010/11/01) ダブルクオートを含む場合は、その直前にダブルクオートを強制的に追加する。
296                        if( useDataWquot && sval.indexOf( '"' ) >= 0 ) { sval = sval.replaceAll( "\"" ,"\"\"" ) ; }     // 5.9.10.3
297                        if( omitCTRL ) { sval = sval.replaceAll( "\\s" ," " ) ; }
298                        // 5.2.2.0 (2010/11/01) 改行を含む場合は、ダブルクオートを強制的に前後に追加する。
299                        if( !omitCTRL && sval.indexOf( CR ) >= 0 || useWquot ) {                // 6.9.7.0 (2018/05/14) PMD Useless parentheses.
300                                // 8.5.4.2 (2024/01/12) PMD 7.0.0 UseStringBufferForStringAppends 対応
301//                              sval = "\"" + sval + "\"" ;
302                                writer.print( '"' );
303                                writer.print( sval );
304                                writer.print( '"' );
305                        }
306                        else {
307                                writer.print( sval );
308                        }
309                }
310                writer.println();
311        }
312
313        /**
314         * プロセスの処理結果のレポート表現を返します。
315         * 処理プログラム名、入力件数、出力件数などの情報です。
316         * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
317         * 形式で出してください。
318         *
319         * @return   処理結果のレポート
320         */
321        public String report() {
322                // 7.2.9.5 (2020/11/28) PMD:Consider simply returning the value vs storing it in local variable 'XXXX'
323                return "[" + getClass().getName() + "]" + CR
324//              final String report = "[" + getClass().getName() + "]" + CR
325                                + TAB + "Output File  : " + outfile + CR
326                                + TAB + "Output Count : " + count ;
327
328//              return report ;
329        }
330
331        /**
332         * このクラスの使用方法を返します。
333         *
334         * @return      このクラスの使用方法
335         * @og.rtnNotNull
336         */
337        public String usage() {
338                final StringBuilder buf = new StringBuilder( BUFFER_LARGE )
339                .append( "Process_TableWriter は、上流から受け取ったデータをファイルに書き込む"                 ).append( CR )
340                .append( "CainProcess インターフェースの実装クラスです。"                                                                ).append( CR )
341                .append( CR )
342                .append( "上流(プロセスチェインのデータは上流から下流へと渡されます。)から"                            ).append( CR )
343                .append( "受け取ったLineModel を元に、DBTableModel 形式ファイルを出力します。"                        ).append( CR )
344                .append( CR )
345//              .append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。"    ).append( CR )
346//              .append( "引数文字列の 『=』 の前後には、空白は挟めません。必ず、-key=value の様に"          ).append( CR )
347//              .append( "繋げてください。"                                                                                                                             ).append( CR )
348                .append( PROCESS_PARAM_USAGE )  // 8.5.6.1 (2024/03/29) 継承元使用
349                .append( CR ).append( CR )
350                .append( getArgument().usage() ).append( CR );
351
352                return buf.toString();
353        }
354
355        /**
356         * このクラスは、main メソッドから実行できません。
357         *
358         * @param       args    コマンド引数配列
359         */
360        public static void main( final String[] args ) {
361                LogWriter.log( new Process_TableWriter().usage() );
362        }
363}