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.fukurou.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
019import org.opengion.hayabusa.common.HybsSystem;
020import org.opengion.hayabusa.common.HybsSystemException;
021import org.opengion.fukurou.util.ToString;                                              // 6.1.1.0 (2015/01/17)
022import org.opengion.fukurou.util.ArraySet;                                              // 6.4.3.4 (2016/03/11)
023import static org.opengion.fukurou.util.StringUtil.nval ;
024
025import org.apache.commons.codec.binary.Base64 ;
026import java.nio.charset.Charset;                                                                // 5.5.2.6 (2012/05/25)
027import java.util.Set;                                                                                   // 6.4.3.4 (2016/03/11)
028
029/**
030 * Cookie を読み書きするタグです。
031 *
032 * Cookie は少量の情報を Servlet から Web ブラウザに送り、 ブラウザにそれを
033 * 維持しもらい、以降のアクセスでサーバに送り返してもらう仕組です。
034 * Cookie の値はクライアントを一意に識別できるようになっているので一般に
035 * セッション管理に用いられています。
036 *
037 * Cookie には名前と値が一つありますが、他にコメントやパス、ドメイン、
038 * 最長存続期間、バージョンといったオプショナルな属性もあります。
039 * Web ブラウザの中にはオプショナルな属性の扱いにバグがあるものがあります。
040 * このため、Servlet の相互運用性を高めるためにはあまり使わないほうがいいでしょう。
041 *
042 * 標準の JavaScript で登録機能はサポートしていましたが、メモリのみで、かつ
043 * 画面単位の書き込みのみでした。
044 * 今回の cookie タグでは、永続化(maxAge)の設定や、システム内(CONTEXT_NAME以下)
045 * での共有(デフォルト)や、その変更、ドメインを指定しての共有(domain)などの
046 * 機能を持っています。
047 * また、漢字コードでの読み書き(useBase64)にも対応しています。
048 * 読み込みに関しては、漢字を指定しなければ、{@SYS.COOKIE.カラム名}で、使用可能です。
049 * 複数の読み込み、また、漢字コードを含むクッキーの場合は、読み込み(action="LOAD")
050 * してください。指定のキー以外に、別名に読み込む(aliasNames)事も可能です。
051 *
052 * @og.formSample
053 * ●形式:
054 *    <og:cookie
055 *        action    = "SAVE"       Cookie に対するアクションを指定します。(SAVE|LOAD|DELETE)
056 *        keys      = "AAA,BBB"    キーをCSV形式で複数指定できます。
057 *        vals      = "VAL1,VAL2"  値をCSV形式で複数指定できます。
058 *        path      = "/ge"        クライアントがこの Cookie を返さなくてはいけないパスを指定します。
059 *        domain    = ".foo.com"   この Cookie がどこで生成されたかを表すドメインを指定します。
060 *        maxAge    = "3600"       Cookie の最長存続期間を秒単位で設定します。
061 *        useBase64 = "false"      漢字等の2Byte文字を使用する場合に、BASE64で処理します。[true/false]
062 *    >
063 * ●body:なし
064 *
065 * ●Tag定義:
066 *   <og:cookie
067 *       action           ○【TAG】アクション(SAVE,LOAD,DELETE)をセットします(必須)。
068 *       keys             ○【TAG】クッキーのキーをCSV形式で複数指定します(必須)。
069 *       vals               【TAG】keys属性に対応する値をCSV形式で複数指定します
070 *       aliasNames         【TAG】クッキーのキーの別名をCSV形式で複数指定します
071 *       path               【TAG】クライアントがこの Cookie を返さなくてはいけないパスを指定します(初期値:/+CONTEXT_NAME)
072 *       domain             【TAG】この Cookie がどこで生成されたかを表すドメインを指定します(初期値:付与したサーバ)
073 *       maxAge             【TAG】Cookie の最長存続期間を秒単位で設定します(初期値: -1 )
074 *       useBase64          【TAG】漢字等の文字を扱う場合に、BASE64で処理を行うかどうか[true/false]を設定します(初期値:false )
075 *       debug              【TAG】デバッグ情報を出力するかどうか[true/false]を指定します(初期値:false)
076 *   />
077 *
078 * ●使用例
079 *    例1)設定:複数キーを同時に書き込むことが可能です。
080 *        <og:cookie
081 *            action="SAVE" keys="CDJ,FG_NAME" vals="{@CDJ},{@NAME}"
082 *        />
083 *
084 *    例2)取得:cookieタグで取得すると、それ以降では {@CDJ} や {@NAME} で扱えます。
085 *          aliasNames 属性を使わない場合は、keys に指定した変数にセットされます。
086 *        <og:cookie
087 *            action="LOAD" keys="CDJ,FG_NAME" aliasNames="CDJ,NAME"
088 *        />
089 *
090 *    例3)取得:SYS パラメータでの取得も可能です。
091 *        {@SYS.COOKIE.CDJ}
092 *
093 *    例4) QUERY画面では値の表示(LOAD)を行い、RESULT画面で値の設定(SAVE)を行うケース
094 *
095 *        QUERY画面
096 *        <og:cookie action="LOAD" useBase64="true"
097 *                        keys="CLM,NAME" aliasNames="CLM,LABEL_NAME" />
098 *
099 *        <og:column name="CLM"        defaultVal="{@CLM}" />
100 *        <og:column name="LABEL_NAME" defaultVal="{@LABEL_NAME}"/>
101 *
102 *        RESULT画面
103 *        <og:cookie action="SAVE" maxAge="360000" useBase64="true"
104 *                        keys="CLM,NAME" vals="{@CLM},{@LABEL_NAME}" />
105 *
106 *    例5) QUERY画面では、{@SYS.COOKIE.カラム名} で取得。
107 *        RESULT画面では、ムラタ内全システム共通に使える値をセット。
108 *
109 *        QUERY画面
110 *        <og:column name="SYSTEM_ID" defaultVal="{@SYS.COOKIE.SYSTEM_ID}" />
111 *
112 *        RESULT画面
113 *        <og:cookie action="SAVE" maxAge="360000" domain=".opengion.org"
114 *                        keys="SYSTEM_ID" vals="{@SYSTEM_ID}" />
115 *
116 * @og.rev 3.8.0.2 (2005/06/30) 新規作成
117 * @og.group 画面制御
118 *
119 * @version  0.9.0  2000/10/17
120 * @author   Kazuhiko Hasegawa
121 * @since    JDK5.0,
122 */
123public class CookieTag extends CommonTagSupport {
124        /** このプログラムのVERSION文字列を設定します。   {@value} */
125        private static final String VERSION = "6.4.3.4 (2016/03/11)" ;
126        private static final long serialVersionUID = 643420160311L ;
127
128        /**
129         * プラットフォーム依存のデフォルトの Charset です。
130         * プラットフォーム依存性を考慮する場合、エンコード指定で作成しておく事をお勧めします。
131         *
132         * @og.rev 5.5.2.6 (2012/05/25) findbugs対応
133         */
134        private static final Charset DEFAULT_CHARSET = Charset.defaultCharset() ;
135
136        /** action 引数に渡す事の出来る アクション  設定 {@value} */
137        public static final String ACT_SAVE = "SAVE" ;
138        /** action 引数に渡す事の出来る アクション  取得 {@value} */
139        public static final String ACT_LOAD = "LOAD" ;
140        /** action 引数に渡す事の出来る アクション  削除 {@value} */
141        public static final String ACT_DELETE = "DELETE" ;
142
143        // 6.4.3.4 (2016/03/11) String配列 から、Setに置き換えます。
144        private static final Set<String> ACTION_SET = new ArraySet<>( ACT_SAVE , ACT_LOAD , ACT_DELETE );
145
146        private String   action         ;
147        private String[] keys           ;
148        private String[] vals           ;
149        private String[] aliasNames     ;
150        private String   path           = "/" + HybsSystem.sys( "CONTEXT_NAME" );
151        private String   domain         ;
152        private int      maxAge         = -1;
153        private boolean  useBase64      ;
154
155        /**
156         * デフォルトコンストラクター
157         *
158         * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
159         */
160        public CookieTag() { super(); }         // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
161
162        /**
163         * Taglibの終了タグが見つかったときに処理する doEndTag() を オーバーライドします。
164         *
165         * @return      後続処理の指示(EVAL_PAGE)
166         */
167        @Override
168        public int doEndTag() {
169                debugPrint();           // 4.0.0 (2005/02/28)
170                actionExec( action );
171
172                return EVAL_PAGE ;
173        }
174
175        /**
176         * タグリブオブジェクトをリリースします。
177         * キャッシュされて再利用されるので、フィールドの初期設定を行います。
178         *
179         */
180        @Override
181        protected void release2() {
182                super.release2();
183                action          = null;
184                keys            = null;
185                vals            = null;
186                aliasNames      = null;
187                path            = "/" + HybsSystem.sys( "CONTEXT_NAME" );
188                domain          = null;
189                maxAge          = -1;
190                useBase64       = false;
191        }
192
193        /**
194         * アクションを実行します。
195         *
196         * アクションは action 属性で指定します。
197         *
198         * @og.rev 6.3.4.0 (2015/08/01) Arrays.toString から String.join に置き換え。
199         * @og.rev 6.4.3.4 (2016/03/11) String配列 から、Setに置き換えます。
200         *
201         * @param       action  アクション (public static final 宣言されている文字列)
202         */
203        private void actionExec( final String action ) {
204
205                if( ACT_SAVE.equals( action ) ) {
206                        saveCookies( maxAge );
207                }
208                else if( ACT_LOAD.equals( action ) ) {
209                        loadCookies();
210                }
211                else if( ACT_DELETE.equals( action ) ) {
212                        saveCookies( 0 );       // maxAge にゼロをセットすると削除されます。
213                }
214                else {
215                        final String errMsg = "指定のアクションは実行できません。アクションエラー"       + CR
216                                                        + "action=[" + action + "] "                                                            + CR
217                                                        + "actionList=" + String.join( ", " , ACTION_SET ) ;
218                        throw new HybsSystemException( errMsg );
219                }
220        }
221
222        /**
223         * SAVE アクションを実行します。
224         *
225         * クッキーに書き込みを行います。
226         *
227         * @og.rev 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
228         *
229         * @param       maxAge  最長存続期間 ( 0 なら削除 )
230         */
231        private void saveCookies( final int maxAge ) {
232                // 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
233                if( keys == null || vals == null || keys.length != vals.length ) {
234                        final String errMsg = "キーが未登録か、値が未登録か、キーと値の個数が異なります。" ;
235                        throw new OgRuntimeException( errMsg );
236                }
237
238        //      BASE64Encoder encoder = null;
239                for( int i=0; i<keys.length; i++ ) {
240                        String value = nval (vals[i],"" );
241                        if( useBase64 && value.length() > 0 ) {
242        //                      if( encoder == null ) { encoder = new BASE64Encoder(); }
243        //                      value = encoder.encode( value.getBytes() ) ;
244
245                                final byte[] encoded = Base64.encodeBase64( value.getBytes( DEFAULT_CHARSET ) );                // 5.5.2.6 (2012/05/25) findbugs対応
246                                value = new String( encoded,DEFAULT_CHARSET );                                                                  // 5.5.2.6 (2012/05/25) findbugs対応
247                        }
248                        setCookie( keys[i], value, maxAge );
249                }
250        }
251
252        /**
253         * LOAD アクションを実行します。
254         *
255         * クッキーから読み込みを行います。
256         *
257         * @og.rev 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
258         *
259         */
260        private void loadCookies() {
261                // 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
262                if( keys == null ) {
263                        final String errMsg = "キーが未登録です。" ;
264                        throw new OgRuntimeException( errMsg );
265                }
266
267        //      BASE64Decoder decoder = null;
268                if( aliasNames == null ) { aliasNames = keys; }
269
270        //      try {
271                        for( int i=0; i<keys.length; i++ ) {
272                                String value = getCookie( keys[i] );
273                                if( useBase64 && value != null && value.length() > 0 ) {
274        //                              if( decoder == null ) { decoder = new BASE64Decoder(); }
275        //                              byte[] decoded = decoder.decodeBuffer( value );
276                                        final byte[] decoded = Base64.decodeBase64( value.getBytes( DEFAULT_CHARSET ) );                // 5.5.2.6 (2012/05/25) findbugs対応
277                                        value = new String( decoded,DEFAULT_CHARSET );                                                                  // 5.5.2.6 (2012/05/25) findbugs対応
278                                }
279                                if( value != null ) {
280                                        setRequestAttribute( aliasNames[i],value );
281                                }
282                        }
283        //      }
284        //      catch( final IOException ex ) {
285        //      final String errMsg = "BASE64Decoder エラー " + ex.toString();
286        //              throw new HybsSystemException( errMsg );
287        //      }
288        }
289
290        /**
291         * 【TAG】アクション(SAVE,LOAD,DELETE)をセットします。
292         *
293         * @og.tag
294         * アクションは,HTMLから(get/post)指定されますので,ACT_xxx で設定される
295         * フィールド定数値のいづれかを、指定できます。
296         * 無指定の場合は、なにもしません。
297         *
298         * <table class="plain">
299         *   <caption>アクションの説明</caption>
300         *   <tr><th>action </th><th>名称</th><th>機能</th></tr>
301         *   <tr><td>SAVE   </td><td>登録</td><td>指定の keys のキーに vals の値をセットします。</td></tr>
302         *   <tr><td>LOAD   </td><td>取得</td><td>指定の keys のクッキーを(リクエスト中に)取得します。</td></tr>
303         *   <tr><td>DELETE </td><td>削除</td><td>指定の keys のクッキーを削除します。</td></tr>
304         * </table>
305         *
306         * @og.rev 6.3.4.0 (2015/08/01) Arrays.toString から String.join に置き換え。
307         * @og.rev 6.4.3.4 (2016/03/11) String配列 から、Setに置き換えます。
308         *
309         * @param       act アクション (public static final 宣言されている文字列)
310         * @see         <a href="../../../../constant-values.html#org.opengion.hayabusa.taglib.CookieTag.ACT_DELETE">アクション定数</a>
311         */
312        public void setAction( final String act ) {
313                action = nval( getRequestParameter( act ),action );
314
315                if( action != null && !check( action, ACTION_SET ) ) {
316                        final String errMsg = "指定のアクションは実行できません。アクションエラー"       + CR
317                                                        + "action=[" + action + "] "                                                            + CR
318                                                        + "actionList=" + String.join( ", " , ACTION_SET ) ;
319                        throw new HybsSystemException( errMsg );
320                }
321        }
322
323        /**
324         * 【TAG】クッキーのキーをCSV形式で複数指定します。
325         *
326         * @og.tag
327         * クッキーにセットするときのキーを指定します。CSV形式で複数指定できます。
328         * vals 属性には、キーに対応する値を、設定してください。
329         * 分解方法は、CSV変数を先に分解してから、getRequestParameter で値を取得します。
330         * こうしないとデータ自身にカンマを持っている場合に分解をミスる為です。
331         *
332         * @param       key     クッキーのキー
333         */
334        public void setKeys( final String key ) {
335                keys = getCSVParameter( key );
336        }
337
338        /**
339         * 【TAG】クッキーのキーの別名をCSV形式で複数指定します。
340         *
341         * @og.tag
342         * クッキーから値を取得する(action="LOAD")場合に、読み込みキー(keys)に対応する
343         * 別名を指定することで、別名の変数に読み込んだ値を登録することが出来ます。
344         * 別名を指定しない場合は、keys に指定された名前が、使用されます。
345         * 分解方法は、CSV変数を先に分解してから、getRequestParameter で値を取得します。
346         * こうしないとデータ自身にカンマを持っている場合に分解をミスる為です。
347         *
348         * @param       names   クッキーの別名
349         */
350        public void setAliasNames( final String names ) {
351                aliasNames = getCSVParameter( names );
352        }
353
354        /**
355         * 【TAG】keys属性に対応する値をCSV形式で複数指定します。
356         *
357         * @og.tag
358         * キーに設定した値を、CSV形式で複数して出来ます。
359         * 指定順序は、キーと同じにしておいて下さい。
360         * 分解方法は、CSV変数を先に分解してから、getRequestParameter で値を取得します。
361         * こうしないとデータ自身にカンマを持っている場合に分解をミスる為です。
362         *
363         * @param       val     keys属性に対応する値
364         */
365        public void setVals( final String val ) {
366                vals = getCSVParameter( val );
367        }
368
369        /**
370         * 【TAG】クライアントがこの Cookie を返さなくてはいけないパスを指定します(初期値:/+CONTEXT_NAME)。
371         *
372         * @og.tag
373         * この値を指定すると Cookie が該当するディレクトリ内、さらに、
374         * サブディレクトリに存在する全てのページから参照できるようになります。
375         * Cookie のパスには Cookie をセットした Servlet が含まれていなければなりません。
376         * 例えば、/catalog を指定したとします。このとき、 サーバの /catalog 以下の全ての
377         * ディレクトリから Cookie が見えるようになります。
378         * 初期値は、"/" + CONTEXT_NAME です。
379         *
380         * Cookie のパス名指定についての詳細は RFC2109 を参照してください。
381         *
382         * @param       uri パスを表すURL
383         */
384        public void setPath( final String uri ) {
385                path = nval( getRequestParameter( uri ),path );
386        }
387
388        /**
389         * 【TAG】この Cookie がどこで生成されたかを表すドメインを指定します(初期値:付与したサーバ)。
390         *
391         * @og.tag
392         * ドメイン名の形式は RFC2109 で指定されています。
393         * ドメイン名は (.foo.com のように) ドットで始まります。 このように設定すると、
394         * Cookie は指定された Domain Name System (DNS) のゾーン内のサーバから見える
395         * ようになります(例えば、www.foo.com) からは見えるけれど、a.b.foo.com からは
396         * 見えないというようにです)。 デフォルトでは Cookie を付与したサーバにしか送り返しません。
397         *
398         * @param       pattern 生成ドメイン名
399         */
400        public void setDomain( final String pattern ) {
401                domain = nval( getRequestParameter( pattern ),domain );
402        }
403
404        /**
405         * 【TAG】Cookie の最長存続期間を秒単位で設定します(初期値: -1 )。
406         *
407         * @og.tag
408         * 正の値が指定されると Cookie はある秒数が過ぎた後、削除されます。
409         * この値は、Cookie の有効期限が切れる 最長 存続期間であることに注意してください。
410         * Cookie の現在までの存続期間ではありません。
411         *
412         * 負の値は Cookie が永続的に保存されないことを意味しています。 この場合、
413         * Web ブラウザが終了すると Cookie も削除されます。 0 という値を指定すると
414         * Cookie が削除されることになります。
415         * 初期値は、-1(永続的に保存されない)です。
416         *
417         * @param       expiry 最長存続期間(秒) (負の値は Cookie を保存しない、 0 なら Cookie を削除する意味となる)
418         */
419        public void setMaxAge( final String expiry ) {
420                maxAge = nval( getRequestParameter( expiry ),maxAge );
421        }
422
423        /**
424         * 【TAG】漢字等の文字を扱う場合に、BASE64で処理を行うかどうか[true/false]を設定します(初期値:false )。
425         *
426         * @og.tag
427         * クッキーへの読み書きは、ASCII に限られます。漢字等のコードを書き込む場合は、
428         * BASE64でエンコードして書き込む必要があります。読み込む場合も同様です。
429         * ただし、一般のASCIIは、BASE64 ではエンコードしないため、外部で指定する必要があります。
430         * BASE64 で書き込んだ場合ば、{&#064;SYS.COOKIE.CDJ} での取得はできませんので、
431         * action="LOAD" で、取得してください。
432         * 初期値は、false(使用しない)です。
433         *
434         * @param       flag BASE64処理可否 [true:する/false:しない]
435         */
436        public void setUseBase64( final String flag ) {
437                useBase64 = nval( getRequestParameter( flag ),useBase64 );
438        }
439
440        /**
441         * このオブジェクトの文字列表現を返します。
442         * 基本的にデバッグ目的に使用します。
443         *
444         * @return このクラスの文字列表現
445         * @og.rtnNotNull
446         */
447        @Override
448        public String toString() {
449                return ToString.title( this.getClass().getName() )
450                                .println( "VERSION"             ,VERSION        )
451                                .println( "action"              ,action         )
452                                .println( "keys"                ,keys           )
453                                .println( "vals"                ,vals           )
454                                .println( "aliasNames"  ,aliasNames     )
455                                .println( "path"                ,path           )
456                                .println( "domain"              ,domain         )
457                                .println( "maxAge"              ,maxAge         )
458                                .println( "useBase64"   ,useBase64      )
459                                .println( "Other..."    ,getAttributes().getAttribute() )
460                                .fixForm().toString() ;
461        }
462}