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.Closer;
021import org.opengion.fukurou.system.LogWriter;
022import org.opengion.fukurou.model.Formatter;
023import org.opengion.fukurou.db.ConnectionFactory;
024
025import java.util.Map ;
026import java.util.LinkedHashMap ;
027
028import java.sql.Connection;
029import java.sql.PreparedStatement;
030import java.sql.ParameterMetaData;
031import java.sql.ResultSet;
032import java.sql.SQLException;
033
034/**
035 * Process_DBCountFilter は、データベースの存在件数でフィルタリングする
036 * ChainProcess インターフェースの実装クラスです。
037 * 上流(プロセスチェインのデータは上流から下流へと渡されます。)から受け取った
038 * LineModel を元に、データベースの存在チェックを行い、下流への処理を振り分けます。
039 * 具体的には、指定する SELECT 文は、必ず、『select count(*) from ・・・』形式にして下さい。
040 * 検索カラムは、一つだけで、そこには数字が入ります。
041 *
042 * データベース接続先等は、ParamProcess のサブクラス(Process_DBParam)に
043 * 設定された接続(Connection)を使用します。
044 *
045 * 引数文字列中にスペースを含む場合は、ダブルコーテーション("") で括って下さい。
046 * 引数文字列の 『=』の前後には、スペースは挟めません。必ず、-key=value の様に
047 * 繋げてください。
048 *
049 * @og.formSample
050 *  Process_DBCountFilter -dbid=DBGE -sql="select count(*) from GEA03"
051 *
052 *   [ -dbid=DB接続ID           ] : -dbid=DBGE (例: Process_DBParam の -configFile で指定する DBConfig.xml ファイルで規定)
053 *   [ -sql=検索SQL文           ] : -sql="SELECT COUNT(*) FROM GEA03
054 *                                         WHERE SYSTEM_ID = [SYSTEM_ID]
055 *                                         AND CLM         = [CLM]
056 *                                         AND FGJ         = '1'"
057 *   [ -sqlFile=検索SQLファイル ] : -sqlFile=select.sql
058 *                                :   -sql や -sqlFile が指定されない場合は、エラーです。
059 *   [ -count=スルー条件        ] : -count=[0|1|2] は、検索値に応じたスルー条件。
060 *                                     0:0件時にスルー(処理を継続) つまり、なければ継続
061 *                                     1:1件時にスルー(処理を継続) つまり、あれば継続
062 *                                     2:2件以上ある場合にスルー   つまり、キー重複時に継続
063 *   [ -display=[false/true]    ] :結果を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
064 *   [ -debug=[false/true]      ] :デバッグ情報を標準出力に表示する(true)かしない(false)か(初期値:false[表示しない])
065 *
066 * @version  4.0
067 * @author   Kazuhiko Hasegawa
068 * @since    JDK5.0,
069 */
070public class Process_DBCountFilter extends AbstractProcess implements ChainProcess {
071        /** 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズ  {@value} */
072        private static final int DB_FETCH_SIZE = 1001 ;
073
074        private Connection      connection      ;
075        private PreparedStatement pstmt ;
076        private ParameterMetaData pMeta ;               // 5.1.1.0 (2009/11/11) setObject に、Type を渡す。(PostgreSQL対応)
077        private boolean useParamMetaData;               // 5.1.1.0 (2009/11/11) setObject に、Type を渡す。(PostgreSQL対応)
078
079        private String          dbid            ;
080        private String          sql                     ;
081        private int                     cntFlag         = -2;           // スルー条件 [0|1|2]
082        private boolean         display         ;                       // false:表示しない
083        private boolean         debug           ;                       // 5.7.3.0 (2014/02/07) デバッグ情報
084
085        private int[]           clmNos          ;                       // ファイルのヘッダーのカラム番号
086        private boolean         firstRow        = true;         // 最初の一行目
087        private int                     count           ;
088
089        /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
090        private static final Map<String,String> MUST_PROPARTY   ;               // [プロパティ]必須チェック用 Map
091        /** staticイニシャライザ後、読み取り専用にするので、ConcurrentHashMap を使用しません。 */
092        private static final Map<String,String> USABLE_PROPARTY ;               // [プロパティ]整合性チェック Map
093
094        static {
095                MUST_PROPARTY = new LinkedHashMap<>();
096
097                USABLE_PROPARTY = new LinkedHashMap<>();
098                USABLE_PROPARTY.put( "dbid",    "Process_DBParam の -configFile で指定する DBConfig.xml ファイルで規定" );
099                USABLE_PROPARTY.put( "sql",                     "カウントSQL文(sql or sqlFile 必須)" +
100                                                                        CR + "例: \"SELECT COUNT(*) FROM GEA03 " +
101                                                                        CR + "WHERE SYSTEM_ID = [SYSTEM_ID] " +
102                                                                        CR + "AND CLM = [CLM] AND FGJ = '1'\"" );
103                USABLE_PROPARTY.put( "sqlFile",         "検索SQLファイル(sql or sqlFile 必須)例: select.sql" );
104                USABLE_PROPARTY.put( "count",   "[0|1|2] は、検索値に応じたスルー条件" +
105                                                                        CR + "  0:0件時にスルー(処理を継続) つまり、なければ継続" +
106                                                                        CR + "  1:1件時にスルー(処理を継続) つまり、あれば継続" +
107                                                                        CR + "  2:2件以上ある場合にスルー   つまり、キー重複時に継続" );
108                USABLE_PROPARTY.put( "display", "結果を標準出力に表示する(true)かしない(false)か" +
109                                                                                CR + "(初期値:false:表示しない)" );
110                USABLE_PROPARTY.put( "debug",   "デバッグ情報を標準出力に表示する(true)かしない(false)か" +
111                                                                                CR + "(初期値:false:表示しない)" );             // 5.7.3.0 (2014/02/07) デバッグ情報
112        }
113
114        /**
115         * デフォルトコンストラクター。
116         * このクラスは、動的作成されます。デフォルトコンストラクターで、
117         * super クラスに対して、必要な初期化を行っておきます。
118         *
119         */
120        public Process_DBCountFilter() {
121                super( "org.opengion.fukurou.process.Process_DBCountFilter",MUST_PROPARTY,USABLE_PROPARTY );
122        }
123
124        /**
125         * プロセスの初期化を行います。初めに一度だけ、呼び出されます。
126         * 初期処理(ファイルオープン、DBオープン等)に使用します。
127         *
128         * @og.rev 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
129         * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData を ConnectionFactory経由で取得。(PostgreSQL対応)
130         *
131         * @param   paramProcess データベースの接続先情報などを持っているオブジェクト
132         */
133        public void init( final ParamProcess paramProcess ) {
134                final Argument arg = getArgument();
135
136                sql                     = arg.getFileProparty("sql","sqlFile",true);
137                cntFlag         = arg.getProparty("count",cntFlag);
138                display         = arg.getProparty("display",display);
139                debug           = arg.getProparty("debug",debug);                               // 5.7.3.0 (2014/02/07) デバッグ情報
140
141                dbid            = arg.getProparty("dbid");
142                connection      = paramProcess.getConnection( dbid );
143                useParamMetaData = ConnectionFactory.useParameterMetaData( dbid );      // 5.3.8.0 (2011/08/01)
144        }
145
146        /**
147         * プロセスの終了を行います。最後に一度だけ、呼び出されます。
148         * 終了処理(ファイルクローズ、DBクローズ等)に使用します。
149         *
150         * @og.rev 4.0.0.0 (2007/11/27) commit,rollback,remove 処理を追加
151         * @og.rev 5.1.2.0 (2010/01/01) pMeta のクリア
152         *
153         * @param   isOK トータルで、OKだったかどうか [true:成功/false:失敗]
154         */
155        public void end( final boolean isOK ) {
156                final boolean flag = Closer.stmtClose( pstmt );
157                pstmt = null;
158                pMeta = null;           // 5.1.1.0 (2009/11/11)
159
160                ConnectionFactory.remove( connection,dbid );
161
162                if( !flag ) {
163                        final String errMsg = "ステートメントをクローズ出来ません。";
164                        throw new OgRuntimeException( errMsg );
165                }
166        }
167
168        /**
169         * 引数の LineModel を処理するメソッドです。
170         * 変換処理後の LineModel を返します。
171         * 後続処理を行わない場合(データのフィルタリングを行う場合)は、
172         * null データを返します。つまり、null データは、後続処理を行わない
173         * フラグの代わりにも使用しています。
174         * なお、変換処理後の LineModel と、オリジナルの LineModel が、
175         * 同一か、コピー(クローン)かは、各処理メソッド内で決めています。
176         * ドキュメントに明記されていない場合は、副作用が問題になる場合は、
177         * 各処理ごとに自分でコピー(クローン)して下さい。
178         *
179         * @og.rev 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
180         * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData  setNull 対応(PostgreSQL対応)
181         * @og.rev 5.7.2.2 (2014/01/24) SQL実行エラーを少し詳細に出力します。
182         * @og.rev 6.4.2.1 (2016/02/05) try-with-resources 文で記述。
183         *
184         * @param       data オリジナルのLineModel
185         *
186         * @return      処理変換後のLineModel
187         */
188        public LineModel action( final LineModel data ) {
189                LineModel rtnData = data;
190
191                count++ ;
192                try {
193                        if( firstRow ) {
194                                pstmt = makePrepareStatement( data );                           // 最初に作成します。最後は、end( boolean ) メソッドで、close します。
195                                if( useParamMetaData ) {
196                                        pMeta = pstmt.getParameterMetaData();
197                                }
198                                firstRow = false;
199                                if( display ) { println( data.nameLine() ); }           // 5.7.3.0 (2014/02/07) デバッグ情報
200                        }
201
202                        // 5.1.1.0 (2009/11/11) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応)
203                        if( useParamMetaData ) {
204                                for( int i=0; i<clmNos.length; i++ ) {
205                                        final int type = pMeta.getParameterType( i+1 );
206                                        // 5.3.8.0 (2011/08/01) setNull 対応
207                                        final Object val = data.getValue(clmNos[i]);
208                                        if( val == null || ( val instanceof String && ((String)val).isEmpty() ) ) {
209                                                pstmt.setNull( i+1, type );
210                                        }
211                                        else {
212                                                pstmt.setObject( i+1, val, type );
213                                        }
214                                }
215                        }
216                        else {
217                                for( int i=0; i<clmNos.length; i++ ) {
218                                        pstmt.setObject( i+1,data.getValue(clmNos[i]) );
219                                }
220                        }
221
222                        int cnt = -1;
223                        // 6.4.2.1 (2016/02/05) try-with-resources 文
224                        try( final ResultSet result = pstmt.executeQuery() ) {
225                                if( result.next() ) {                           // 1行目固定
226                                        cnt = result.getInt( 1 );               // 1カラム目固定
227                                }
228                        }
229
230                        if( ( cnt > 2  && cntFlag != 2 ) ||
231                                ( cnt <= 2 && cnt != cntFlag ) ) {
232                                        rtnData = null;         // 不一致
233                        }
234                        if( display ) { println( data.dataLine() ); }           // 5.1.2.0 (2010/01/01) display の条件変更
235                }
236                catch( final SQLException ex) {
237                        // 5.7.2.2 (2014/01/24) SQL実行エラーを少し詳細に出力します。
238                        final String errMsg = "SQL を実行できませんでした。" + CR
239                                                + "errMsg=[" + ex.getMessage() + "]" + CR
240                                                + "errCode=[" + ex.getErrorCode() + "] State=[" + ex.getSQLState() + "]" + CR
241                                                + "dbid=[" + dbid + "]" + CR
242                                                + "sql =[" + sql + "]" + CR
243                                                + "data=[" + data.dataLine() + "]" + CR ;
244                        throw new OgRuntimeException( errMsg,ex );
245                }
246                return rtnData;
247        }
248
249        /**
250         * 内部で使用する PreparedStatement を作成します。
251         * 引数指定の SQL または、LineModel から作成した SQL より構築します。
252         *
253         * @og.rev 5.7.2.2 (2014/01/24) SQL実行エラーを少し詳細に出力します。
254         * @og.rev 6.4.3.4 (2016/03/11) Formatterに新しいコンストラクターを追加する。
255         * @og.rev 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズを設定。
256         *
257         * @param       data 処理対象のLineModel
258         *
259         * @return  PreparedStatementオブジェクト
260         */
261        private PreparedStatement makePrepareStatement( final LineModel data ) {
262
263                // カラム番号は、makeFormat の処理で設定しています。
264                final Formatter format = new Formatter( data,sql );             // 6.4.3.4 (2016/03/11)
265                sql = format.getQueryFormatString();
266                clmNos = format.getClmNos();
267
268                final PreparedStatement ps ;
269                try {
270                        ps = connection.prepareStatement( sql );
271                        ps.setFetchSize( DB_FETCH_SIZE );                                       // 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズ
272                }
273                catch( final SQLException ex) {
274                        // 5.7.2.2 (2014/01/24) SQL実行エラーを少し詳細に出力します。
275                        final String errMsg = "PreparedStatement を取得できませんでした。" + CR
276                                                        + "errMsg=[" + ex.getMessage() + "]" + CR
277                                                        + "errCode=[" + ex.getErrorCode() + "] State=[" + ex.getSQLState() + "]" + CR
278                                                        + "dbid=[" + dbid + "]" + CR
279                                                        + "sql =[" + sql + "]" + CR
280                                                        + "data=[" + data.dataLine() + "]" + CR ;
281                        throw new OgRuntimeException( errMsg,ex );
282                }
283
284                return ps;
285        }
286
287        /**
288         * プロセスの処理結果のレポート表現を返します。
289         * 処理プログラム名、入力件数、出力件数などの情報です。
290         * この文字列をそのまま、標準出力に出すことで、結果レポートと出来るような
291         * 形式で出してください。
292         *
293         * @return   処理結果のレポート
294         */
295        public String report() {
296                final String report = "[" + getClass().getName() + "]" + CR
297                                                        + TAB + "DBID         : " + dbid + CR
298                                                        + TAB + "Output Count : " + count ;
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( BUFFER_LARGE )
311                        .append( "Process_DBCountFilter は、データベースの存在件数でフィルタリングする"                        ).append( CR )
312                        .append( "ChainProcess インターフェースの実装クラスです。"                                                               ).append( CR )
313                        .append( "上流(プロセスチェインのデータは上流から下流へと渡されます。)から"                            ).append( CR )
314                        .append( "受け取った LineModel を元に、データベースの存在チェックを行い、"                                ).append( CR )
315                        .append( "下流への処理を振り分けます。"                                                                                                       ).append( CR )
316                        .append( "存在チェックで指定する SELECT 文は、必ず、『select count(*) from ・・・』"          ).append( CR )
317                        .append( "形式にして下さい。検索カラムは、一つだけで、そこには数字が入ります。"                   ).append( CR )
318                        .append( CR )
319                        .append( "データベース接続先等は、ParamProcess のサブクラス(Process_DBParam)に"                    ).append( CR )
320                        .append( "設定された接続(Connection)を使用します。"                                                                           ).append( CR )
321                        .append( CR )
322                        .append( "引数文字列中に空白を含む場合は、ダブルコーテーション(\"\") で括って下さい。"    ).append( CR )
323                        .append( "引数文字列の 『=』の前後には、空白は挟めません。必ず、-key=value の様に"           ).append( CR )
324                        .append( "繋げてください。"                                                                                                                             ).append( CR )
325                        .append( CR ).append( CR )
326                        .append( getArgument().usage() ).append( CR );
327
328                return buf.toString();
329        }
330
331        /**
332         * このクラスは、main メソッドから実行できません。
333         *
334         * @param       args    コマンド引数配列
335         */
336        public static void main( final String[] args ) {
337                LogWriter.log( new Process_DBCountFilter().usage() );
338        }
339}