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.hayabusa.taglib;
017
018import org.opengion.hayabusa.common.HybsSystemException;
019import org.opengion.fukurou.util.LogWriter;
020import static org.opengion.fukurou.util.StringUtil.nval ;
021import org.opengion.fukurou.util.FileUtil ;
022
023import org.opengion.fukurou.process.MainProcess;
024import org.opengion.fukurou.process.HybsProcess;
025import org.opengion.fukurou.process.LoggerProcess;
026import org.opengion.fukurou.process.Process_Logger;
027
028import javax.servlet.jsp.JspWriter ;
029import javax.servlet.http.HttpServletRequest ;
030import javax.servlet.http.HttpServletResponse;
031
032import java.util.List;
033import java.util.ArrayList;
034import java.util.Set;
035import java.util.HashSet;
036
037import java.io.PrintWriter ;
038import java.io.ObjectOutputStream;
039import java.io.ObjectInputStream;
040import java.io.IOException;
041
042/**
043 * HybsProcess を継承した、ParamProcess,FirstProcess,ChainProcess の実装クラスを
044 * 実行する MainProcess を起動するクラスです。
045 * LoggerProcess は、最初に定義するクラスで、画面ログ、ファイルログ、を定義します。
046 * また、エラー発生時に、指定のメールアドレスにメール送信できます。
047 * Process_Logger は、なくても構いませんが、指定する場合は、最も最初に指定しなければ
048 * なりません。
049 *
050 * ParamProcess は、一つだけ定義できるクラスで、データベース接続情報を定義します。
051 * (データベース接続しなければ)なくても構いません。
052 *
053 * FirstProcess は、処理を実行する最初のクラスで、このクラスでデータが作成されます。
054 * ループ処理は、この FirstProcess で順次作成された LineModel オブジェクトを
055 * 1行づつ下位の ChainProcess に流していきます。
056 * ChainProcess は、FirstProcess で作成されたデータを、受け取り、処理します。
057 * 処理対象から外れる場合は、LineModel を null に設定する為、下流には流れません。
058 * フィルタチェインの様に使用します。なくても構いませんし、複数存在しても構いません。
059 *
060 * @og.formSample
061 * ●形式:<og:mainProcess
062 *           useJspLog ="[true/false]"
063 *           useDisplay="[true/false]" >
064 *             <og:process processID="ZZZ" >
065 *                 <og:param key="AAA" value="111" />
066 *             </og:process >
067 *         </og:mainProcess >
068 * ●body:あり(EVAL_BODY_BUFFERED:BODYを評価し、{@XXXX} を解析します)
069 *
070 * ●Tag定義:
071 *   <og:mainProcess
072 *       command            【TAG】(通常使いません)処理の実行を指定する command を設定できます(初期値:NEW)
073 *       useJspLog          【TAG】ログ出力先に、JspWriter(つまり、HTML上の返り値)を使用するかどうか[true/false]を指定します(初期値:false)
074 *       useDisplay         【TAG】画面表示先に、JspWriter(つまり、HTML上の返り値)を使用するかどうか[true/false]を指定します(初期値:false)
075 *       useThread          【TAG】独立した別スレッドで実行するかどうか[true/false]を指定します(初期値:false)
076 *       delayTime          【TAG】要求に対して、処理の実行開始を遅延させる時間を指定します(初期値:0秒)
077 *       debug              【TAG】デバッグ情報を出力するかどうか[true/false]を指定します(初期値:false)
078 *   >   ... Body ...
079 *   </og:mainProcess>
080 *
081 * ●使用例
082 *   <og:mainProcess
083 *        useJspLog="true" >
084 *     <og:process processID="DBReader" >
085 *        <og:param key="dbid" value="FROM" />
086 *        <og:param key="sql"  value="select * from GE02" />
087 *     </og:process >
088 *     <og:process processID="DBWriter" >
089 *        <og:param key="dbid"  value="TO" />
090 *        <og:param key="table" value="GE02" />
091 *     </og:process >
092 *   </og:mainProcess >
093 *
094 * @og.group 画面表示
095 *
096 * @version  4.0
097 * @author       Kazuhiko Hasegawa
098 * @since    JDK5.0,
099 */
100public class MainProcessTag extends CommonTagSupport {
101        //* このプログラムのVERSION文字列を設定します。   {@value} */
102        private static final String VERSION = "4.0.0.0 (2006/09/31)" ;
103
104        private static final long serialVersionUID = 400020060931L ;
105
106        /** command 引数に渡す事の出来る コマンド  新規 {@value} */
107        public static final String CMD_NEW       = "NEW" ;
108
109        private List<HybsProcess> list = null;
110
111        private String  command         = CMD_NEW ;
112        private boolean isJspLog        = false;
113        private boolean isDisplay       = false;
114        private boolean useThread       = false;
115
116        private int                             delayTime = 0;  // 処理の遅延時間(秒)
117        private static final Set<String> lockSet = new HashSet<String>();
118        private String  urlKey   = null ;
119        private boolean skipFlag = false;
120
121        /**
122         * Taglibの開始タグが見つかったときに処理する doStartTag() を オーバーライドします。
123         *
124         * @return      後続処理の指示
125         */
126        @Override
127        public int doStartTag() {
128                HttpServletRequest request = (HttpServletRequest)pageContext.getRequest();
129                urlKey = getUrlKey( request );
130
131                synchronized( lockSet ) {
132                        // 新規追加は、true , すでに存在すれば、false を返します。
133                        boolean lock = lockSet.add( urlKey );
134                        skipFlag = !CMD_NEW.equalsIgnoreCase( command ) || !lock && delayTime > 0 ;
135                }
136
137                if( skipFlag ) {
138                        System.out.println( "Skip Process : " + urlKey );
139                        return SKIP_BODY ;              // 処理しません。
140                }
141                else {
142                        list = new ArrayList<HybsProcess>();
143                        return EVAL_BODY_BUFFERED ;             // Body を評価する
144                }
145        }
146
147        /**
148         * Taglibの終了タグが見つかったときに処理する doEndTag() を オーバーライドします。
149         *
150         * @return      後続処理の指示
151         */
152        @Override
153        public int doEndTag() {
154                debugPrint();           // 4.0.0 (2005/02/28)
155
156                if( skipFlag ) { return SKIP_PAGE ; }
157
158                // ログの出力先を切り替えます。
159                if( isJspLog || isDisplay ) {
160                        initLoggerProcess();
161                }
162
163                boolean isOK = true;
164                try {
165                        DelayedProcess process = new DelayedProcess( delayTime,urlKey,list );
166                        if( useThread ) {
167                                new Thread( process ).start();
168                        }
169                        else {
170                                process.run();
171                        }
172
173                        // 実行結果を、"DB.ERR_CODE" キーでリクエストにセットする。
174                        int errCode = process.getKekka();
175                        setRequestAttribute( "DB.ERR_CODE", String.valueOf( errCode ) );
176                }
177                catch( Throwable th ) {
178                        isOK = false;
179                        LogWriter.log( th );
180                        try {
181                                HttpServletResponse responce = (HttpServletResponse)pageContext.getResponse();
182                                responce.sendError( 304 , "ERROR:" + th.getMessage() );
183                        }
184                        catch( IOException ex ) {
185                                LogWriter.log( ex );
186                        }
187                }
188
189                if( isOK )      { return EVAL_PAGE ; }
190                else            { return SKIP_PAGE ; }
191        }
192
193        /**
194         * タグリブオブジェクトをリリースします。
195         * キャッシュされて再利用されるので、フィールドの初期設定を行います。
196         *
197         */
198        @Override
199        protected void release2() {
200                super.release2();
201                command         = CMD_NEW ;
202                isJspLog        = false;
203                isDisplay       = false;
204                useThread       = false;
205                delayTime       = 0;    // 処理の遅延時間(秒)
206                list            = null;
207        }
208
209        /**
210         * 親クラスに登録するプロセスをセットします。
211         *
212         * @param       process 登録するプロセス
213         */
214        protected void addProcess( final HybsProcess process ) {
215                if( ! list.isEmpty() && process instanceof LoggerProcess ) {
216                        String errMsg = "LoggerProcess は、最も最初に指定しなければなりません。";
217                        throw new HybsSystemException( errMsg );
218                }
219                list.add( process );
220        }
221
222        /**
223         * 【TAG】(通常使いません)処理の実行を指定する command を設定できます(初期値:NEW)。
224         *
225         * @og.tag
226         * この処理は、command="NEW" の場合のみ実行されます。RENEW時にはなにも行いません。
227         * 初期値は、NEW です。
228         *
229         * @param       cmd コマンド
230         * @see         <a href="../../../../constant-values.html#org.opengion.hayabusa.taglib.MainProcessTag.CMD_NEW">コマンド定数</a>
231         */
232        public void setCommand( final String cmd ) {
233                command = nval( getRequestParameter( cmd ),command );
234        }
235
236        /**
237         * 【TAG】ログ出力先に、JspWriter(つまり、HTML上の返り値)を使用するかどうか[true/false]を指定します(初期値:false)。
238         *
239         * @og.tag
240         * ログファイルは、processタグで、Logger を指定する場合に、パラメータ logFile にて
241         * ファイル名/System.out/System.err 形式で指定します。
242         * この場合、JSP 特有のWriterである、JspWriter(つまり、HTML上の返り値)は指定
243         * できません。
244         * ここでは、特別に ログの出力先を、JspWriter に切り替えるかどうかを指示
245         * できます。
246         * true を指定すると、画面出力(JspWriter) に切り替わります。
247         * 初期値は、false です。
248         *
249         * @param   flag JspWriter出力 [true:行う/false:行わない]
250         */
251        public void setUseJspLog( final String flag ) {
252                isJspLog = nval( getRequestParameter( flag ),isJspLog );
253        }
254
255        /**
256         * 【TAG】画面表示先に、JspWriter(つまり、HTML上の返り値)を使用するかどうか[true/false]を指定します(初期値:false)。
257         *
258         * @og.tag
259         * 画面表示は、processタグで、Logger を指定する場合に、パラメータ dispFile にて
260         * ファイル名/System.out/System.err 形式で指定します。
261         * この場合、JSP 特有のWriterである、JspWriter(つまり、HTML上の返り値)は指定
262         * できません。
263         * ここでは、特別に ログの出力先を、JspWriter に切り替えるかどうかを指示
264         * できます。
265         * true を指定すると、画面出力(JspWriter) に切り替わります。
266         * 初期値は、false です。
267         *
268         * @param   flag JspWriter出力 [true:行う/false:行わない]
269         */
270        public void setUseDisplay( final String flag ) {
271                isDisplay = nval( getRequestParameter( flag ),isDisplay );
272        }
273
274        /**
275         * 【TAG】独立した別スレッドで実行するかどうか[true/false]を指定します(初期値:false)。
276         *
277         * @og.tag
278         * MainProcess 処理を実行する場合、比較的実行時間が長いケースが考えられます。
279         * そこで、実行時に、スレッドを生成して処理を行えば、非同期に処理を行う
280         * 事が可能です。
281         * ただし、その場合の出力については、JspWriter 等で返すことは出来ません。
282         * 起動そのものを、URL指定の http で呼び出すのであれば、返り値を無視する
283         * ことで、アプリサーバー側のスレッドで処理できます。
284         * 初期値は、順次処理(false)です。
285         *
286         * @param   flag [true:スレッドを使う/false:順次処理で行う]
287         */
288        public void setUseThread( final String flag ) {
289                useThread = nval( getRequestParameter( flag ),useThread );
290        }
291
292        /**
293         * 【TAG】要求に対して、処理の実行開始を遅延させる時間を指定します(初期値:0秒)。
294         *
295         * @og.tag
296         * プロセス起動が、同時に大量に発生した場合に、すべての処理を行うのではなく、
297         * ある程度待って、複数の処理を1回だけで済ますことが出来る場合があります。
298         * 例えば、更新データ毎にトリガが起動されるケースなどです。
299         * それらの開始時刻を遅らせる事で、同時発生のトリガを1回のプロセス処理で
300         * 実行すれば、処理速度が向上します。
301         * ここでは、処理が開始されると、タイマーをスタートさせ、指定時間経過後に、
302         * 処理を開始するようにしますが、その間、受け取ったリクエストは、すべて
303         * 処理せず破棄されます。
304         * ここでは、リクエストのタイミングと処理の開始タイミングは厳密に制御して
305         * いませんので、処理が重複する可能性があります。よって、アプリケーション側で
306         * リクエストが複数処理されても問題ないように、制限をかける必要があります。
307         * 遅延は、リクエスト引数単位に制御されます。
308         *
309         * @param       time    処理開始する遅延時間(秒)
310         */
311        public void setDelayTime( final String time ) {
312                delayTime = nval( getRequestParameter( time ),delayTime );
313        }
314
315        /**
316         * ログの出力先を切り替えます。
317         *
318         * LoggerProcess が存在すれば、そのログに、PrintWriter を直接指定します。
319         * 存在しない場合は、デフォルト LoggerProcess を作成して、指定します。
320         */
321        private void initLoggerProcess() {
322                final LoggerProcess logger ;
323                HybsProcess process = list.get(0);
324                if( process instanceof LoggerProcess ) {
325                        logger = (LoggerProcess)process;
326                }
327                else {
328                        logger = new Process_Logger();
329                        list.add( 0,logger );
330                }
331
332                JspWriter out = pageContext.getOut();
333                PrintWriter writer = FileUtil.getNonFlushPrintWriter( out );
334                if( isJspLog ) {
335                        logger.setLoggingWriter( writer );
336                }
337
338                if( isDisplay ) {
339                        logger.setDisplayWriter( writer );
340                }
341        }
342
343        /**
344         * このリクエストの引数を返します。
345         *
346         * @param       request HttpServletRequestオブジェクト
347         *
348         * @return      request.getRequestURL() + "?" + request.getQueryString()
349         */
350        private String getUrlKey( final HttpServletRequest request ) {
351                StringBuffer address = request.getRequestURL();
352                String           query   = request.getQueryString();
353                if( query != null ) {
354                        address.append( '?' ).append( query );
355                }
356                return address.toString();
357        }
358
359        /**
360         * シリアライズ用のカスタムシリアライズ書き込みメソッド
361         *
362         * @og.rev 4.0.0.0 (2006/09/31) 新規追加
363         * @serialData 一部のオブジェクトは、シリアライズされません。
364         *
365         * @param       strm    ObjectOutputStreamオブジェクト
366         * @throws IOException  入出力エラーが発生した場合
367         */
368        private void writeObject( final ObjectOutputStream strm ) throws IOException {
369                strm.defaultWriteObject();
370        }
371
372        /**
373         * シリアライズ用のカスタムシリアライズ読み込みメソッド
374         *
375         * ここでは、transient 宣言された内部変数の内、初期化が必要なフィールドのみ設定します。
376         *
377         * @og.rev 4.0.0.0 (2006/09/31) 新規追加
378         * @serialData 一部のオブジェクトは、シリアライズされません。
379         *
380         * @param       strm    ObjectInputStreamオブジェクト
381         * @see #release2()
382         * @throws IOException  シリアライズに関する入出力エラーが発生した場合
383         * @throws ClassNotFoundException       クラスを見つけることができなかった場合
384         */
385        private void readObject( final ObjectInputStream strm ) throws IOException , ClassNotFoundException {
386                strm.defaultReadObject();
387        }
388
389        /**
390         * このオブジェクトの文字列表現を返します。
391         * 基本的にデバッグ目的に使用します。
392         *
393         * @return このクラスの文字列表現
394         */
395        @Override
396        public String toString() {
397                return org.opengion.fukurou.util.ToString.title( this.getClass().getName() )
398                                .println( "VERSION"                             ,VERSION                        )
399                                .println( "list"                                ,list                           )
400                                .fixForm().toString() ;
401        }
402
403        private static final class DelayedProcess implements Runnable {
404                private final int delayTime ;
405                private final String urlKey;
406                private final List<HybsProcess> list;
407                private int errCode = MainProcess.RETURN_INIT ;
408
409                public DelayedProcess( final int delayTime,final String urlKey,final List<HybsProcess> list ) {
410                        this.delayTime = delayTime;
411                        this.urlKey    = urlKey;
412                        this.list      = list;
413                }
414
415                public int getKekka() { return errCode; }
416
417                public void run() {
418                        if( delayTime > 0 ) {
419                                try {
420                                        Thread.sleep( delayTime * 1000L );
421                                }
422                                catch( InterruptedException ex2 ) {
423                                        System.out.println( "InterruptedException:" + ex2.getMessage() );
424                                }
425                        }
426                        synchronized( lockSet ) {
427                                lockSet.remove( urlKey );       // 処理の開始前に解除します。取りこぼし対策
428                        }
429
430                        try {
431                                MainProcess process = new MainProcess();
432                                process.setList( list );
433                                process.run();
434                                errCode = process.getKekka();
435                        }
436                        catch( Throwable th ) {
437                                errCode = MainProcess.RETURN_NG;
438                                LogWriter.log( th );
439                        }
440                }
441        }
442}