    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.xml;
    017
    018import org.xml.sax.Attributes;
    019import java.util.List;
    020import java.util.ArrayList;
    021
    022import static org.opengion.fukurou.system.HybsConst.CR;                         // 6.1.0.0 (2014/12/26) refactoring
    023import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;      // 6.1.0.0 (2014/12/26) refactoring
    024
    025/**
    026 * 属性リストをあらわす、OGAttributes クラスを定義します。
    027 *
    028 * 属性リストは、キーと値のペアを、並び順で管理しているリストを保持しています。
    029 * 内部的には、 org.xml.sax.Attributes からの値の設定と、タブの属性の整列を行うための
    030 * 属性タブ、属性の改行の制御、属性の長さの制御 などを行います。
    031 *
    032 * @og.rev 5.1.8.0 (2010/07/01) 新規作成
    033 *
    034 * @version  5.0
    035 * @author   Kazuhiko Hasegawa
    036 * @since    JDK6.0,
    037 */
    038public class OGAttributes {
    039
    040        /** 属性の個数制限。この個数で改行を行う。 {@value} */
    041        public static final int         CR_CNT = 4;
    042        /** 属性の長さ制限。これ以上の場合は、改行を行う。 {@value} */
    043        public static final int         CR_LEN = 80;
    044
    045        private final List<OGAtts>      attList = new ArrayList<>();
    046
    047        private boolean useCR   ;                       // 属性の改行出力を行うかどうか。個数制限が１と同じ
    048        private int     maxValLen       ;                       // 属性の名前の最大文字数
    049        private String  id              ;                       // 特別な属性。id で検索を高速化するため。
    050
    051        /**
    052         * デフォルトトコンストラクター
    053         *
    054         * 取りあえず、属性オブジェクトを構築する場合に使用します。
    055         * 属性タブは、改行＋タブ 、属性リストは、空のリスト、属性改行は、false を初期設定します。
    056         *
    057         */
    058        public OGAttributes() {
    059                // Document empty method チェック対策
    060        }
    061
    062        /**
    063         * 属性タブ、属性リスト、属性改行の有無を指定してのトコンストラクター
    064         *
    065         * 属性タブ、属性リストに null を指定すると、デフォルトトコンストラクターの設定と
    066         * 同じ状態になります。
    067         *
    068         * 注意 属性値の正規化は必ず行われます。
    069         * 属性値に含まれるCR(復帰), LF(改行), TAB(タブ)は、 半角スペースに置き換えられます。
    070         * XMLの規定では、属性の並び順は保障されませんが、SAXのAttributesは、XMLに記述された順番で
    071         * 取得できていますので、このクラスでの属性リストも、記述順での並び順になります。
    072         *
    073         * @og.rev 5.1.9.0 (2010/08/01) id 属性のみ特別にキャッシュしておく。
    074         *
    075         * @param attri         属性リスト
    076         */
    077        public OGAttributes( final Attributes attri ) {
    078
    079                final int num = (attri == null)? 0 : attri.getLength();
    080                int maxLen = 0;
    081                for( int i=0; i<num; i++ ) {
    082                        final String key = attri.getQName(i);
    083                        final String val = attri.getValue(i);
    084                        final OGAtts atts = new OGAtts( key,val );
    085                        attList.add( atts );
    086                        maxLen = atts.maxKeyLen( maxLen );
    087
    088                        if( "id".equals( key ) ) { id = val; }          // 5.1.9.0 (2010/08/01)
    089                }
    090
    091                maxValLen = maxLen;
    092        }
    093
    094        /**
    095         * 属性改行の有無を設定します。
    096         *
    097         * タグによって、属性が多くなったり、意味が重要な場合は、属性１つづつに改行を
    098         * 行いたいケースがあります。
    099         * 属性改行をtrue に設定すると、属性一つづつで、改行を行います。
    100         *
    101         * false の場合は、自動的な改行処理が行われます。
    102         * これは、属性の個数制限(CR_CNT)を超える場合は、改行を行います。
    103         *
    104         * @param       useCR   属性改行の有無(true:１属性単位の改行)
    105         * @see #CR_CNT
    106         * @see #CR_LEN
    107         */
    108        public void setUseCR( final boolean useCR ) {
    109                this.useCR  = useCR;
    110        }
    111
    112        /**
    113         * 属性リストの個数を取得します。
    114         *
    115         * @return      属性リストの個数
    116         */
    117        public int size() {
    118                return attList.size();
    119        }
    120
    121        /**
    122         * 属性リストから、指定の配列番号の、属性キーを取得します。
    123         *
    124         * @param       adrs    配列番号
    125         *
    126         * @return      属性キー
    127         */
    128        public String getKey( final int adrs ) {
    129                return attList.get(adrs).KEY ;
    130        }
    131
    132        /**
    133         * 属性リストから、指定の配列番号の、属性値を取得します。
    134         *
    135         * @param       adrs    配列番号
    136         *
    137         * @return      属性値
    138         */
    139        public String getVal( final int adrs ) {
    140                return attList.get(adrs).VAL ;
    141        }
    142
    143        /**
    144         * 属性リストから、指定の属性キーの、属性値を取得します。
    145         *
    146         * この処理は、属性リストをすべてスキャンして、キーにマッチする
    147         * 属性オブジェクトを見つけ、そこから、属性値を取り出すので、
    148         * パフォーマンスに問題があります。
    149         * 基本的には、アドレス指定で、属性値を取り出すようにしてください。
    150         *
    151         * @og.rev 5.1.9.0 (2010/08/01) 新規追加
    152         *
    153         * @param       key     属性キー
    154         *
    155         * @return      属性値
    156         */
    157        public String getVal( final String key ) {
    158                String val = null;
    159
    160                if( key != null ) {
    161                        for( final OGAtts atts : attList ) {
    162                                if( key.equals( atts.KEY ) ) {
    163                                        val = atts.VAL;
    164                                        break;
    165                                }
    166                        }
    167                }
    168
    169                return val;
    170        }
    171
    172        /**
    173         * 属性リストから、指定の属性キーの、アドレスを取得します。
    174         *
    175         * どちらかというと、キーの存在チェックに近い処理を行います。
    176         * この処理は、属性リストをすべてスキャンして、キーにマッチする
    177         * 属性オブジェクトを見つけ、そこから、属性値を取り出すので、
    178         * パフォーマンスに問題があります。
    179         *
    180         * @og.rev 5.1.9.0 (2010/08/01) 新規追加
    181         *
    182         * @param       key     属性キー
    183         *
    184         * @return      アドレス キーが存在しない場合は、-1 を返す。
    185         */
    186        public int getAdrs( final String key ) {
    187                int adrs = -1;
    188
    189                if( key != null ) {
    190                        for( int i=0; i<attList.size(); i++ ) {
    191                                if( key.equals( attList.get(i).KEY ) ) {
    192                                        adrs = i;
    193                                        break;
    194                                }
    195                        }
    196                }
    197
    198                return adrs;
    199        }
    200
    201        /**
    202         * 属性リストから、id属性の、属性値を取得します。
    203         *
    204         * id属性 は、内部的にキャッシュしており、すぐに取り出せます。
    205         * タグを特定する場合、一般属性のキーと値で選別するのではなく、
    206         * id属性を付与して選別するようにすれば、高速に見つけることが可能になります。
    207         *
    208         * @og.rev 5.1.9.0 (2010/08/01) 新規追加
    209         *
    210         * @return      id属性値
    211         */
    212        public String getId() { return id; }
    213
    214        /**
    215         * 属性リストの、指定の配列番号に、属性値を設定します。
    216         *
    217         * @og.rev 5.1.9.0 (2010/08/01) id 属性のみ特別にキャッシュしておく。
    218         *
    219         * @param       adrs    配列番号
    220         * @param       val     属性値
    221         */
    222        public void setVal( final int adrs , final String val ) {
    223                final OGAtts atts = attList.remove(adrs) ;
    224                attList.add( adrs , new OGAtts( atts.KEY,val ) );
    225
    226                if( "id".equals( atts.KEY ) ) { id = val; }             // 5.1.9.0 (2010/08/01)
    227        }
    228
    229        /**
    230         * 属性リストに、指定のキー、属性値を設定します。
    231         * もし、属性リストに、指定のキーがあれば、属性値を変更します。
    232         * なければ、最後に追加します。
    233         *
    234         * @og.rev 5.6.1.2 (2013/02/22) 新規追加
    235         *
    236         * @param       key     属性キー
    237         * @param       val     属性値
    238         */
    239        public void setVal( final String key , final String val ) {
    240                final int adrs = getAdrs( key );
    241                if( adrs < 0 ) { add( key,val ); }
    242                else           { setVal( adrs,val ); }
    243        }
    244
    245        /**
    246         * 属性リストに、属性(キー、値のセット)を設定します。
    247         *
    248         * 属性リストの一番最後に、属性(キー、値のセット)を設定します。
    249         *
    250         * @og.rev 5.1.9.0 (2010/08/01) id 属性のみ特別にキャッシュしておく。
    251         *
    252         * @param       key     属性リストのキー
    253         * @param       val     属性リストの値
    254         */
    255        public void add( final String key , final String val ) {
    256
    257                final OGAtts atts = new OGAtts( key,val );
    258                attList.add( atts );
    259                maxValLen = atts.maxKeyLen( maxValLen );
    260
    261                if( "id".equals( key ) ) { id = val; }          // 5.1.9.0 (2010/08/01)
    262        }
    263
    264        /**
    265         * 指定のアドレスの属性リストに、属性(キー、値のセット)を設定します。
    266         *
    267         * 指定のアドレスの属性を置き換えるのではなく追加します。
    268         *
    269         * @og.rev 5.1.9.0 (2010/08/01) id 属性のみ特別にキャッシュしておく。
    270         *
    271         * @param       adrs    属性リストのアドレス
    272         * @param       key     属性リストのキー
    273         * @param       val     属性リストの値
    274         */
    275        public void add( final int adrs , final String key , final String val ) {
    276
    277                final OGAtts atts = new OGAtts( key,val );
    278                attList.add( adrs , atts );
    279                maxValLen = atts.maxKeyLen( maxValLen );
    280
    281                if( "id".equals( key ) ) { id = val; }          // 5.1.9.0 (2010/08/01)
    282        }
    283
    284        /**
    285         * 指定のアドレスの属性リストから、属性情報を削除します。
    286         *
    287         * 指定のアドレスの属性を置き換えるのではなく追加します。
    288         *
    289         * @og.rev 5.1.9.0 (2010/08/01) id 属性のみ特別にキャッシュしておく。
    290         *
    291         * @param       adrs    属性リストのアドレス
    292         */
    293        public void remove( final int adrs ) {
    294                final OGAtts atts = attList.remove(adrs) ;
    295
    296                if( "id".equals( atts.KEY ) ) { id = null; }            // 5.1.9.0 (2010/08/01)
    297
    298                // 削除したキーが maxValLen だったとしても、再計算は、行いません。
    299                // 再計算とは、次の長さを見つける必要があるので、すべての OGAtts をもう一度
    300                // チェックする必要が出てくるためです。
    301        }
    302
    303        /**
    304         * オブジェクトの文字列表現を返します。
    305         *
    306         * 属性については、並び順は、登録順が保障されます。
    307         *
    308         * 属性は、３つのパターンで文字列化を行います。
    309         *  ・useCR=true の場合
    310         *     この場合は、属性を１行ずつ改行しながら作成します。属性キーは、
    311         *     最大長＋１ でスペース埋めされて、整形されます。
    312         *  ・useCR=false の場合
    313         *     ・属性の個数制限(CR_CNT)単位に、改行が行われます。
    314         *       これは、属性が右に多く並びすぎるのを防ぎます。
    315         *     ・属性の長さ制限(CR_LEN)単位で、改行が行われます。
    316         *       これは、たとえ、属性の個数が少なくても、文字列として長い場合は、
    317         *       改行させます。
    318         *
    319         * @og.rev 5.6.1.2 (2013/02/22) 改行処理の修正。最後の属性処理の後にも改行を入れる。
    320         * @og.rev 5.6.4.4 (2013/05/31) 改行処理の修正。attTabが、ゼロ文字列の場合の対応。
    321         *
    322         * @param       attTab  Nodeの階層を表す文字列。
    323         * @return      このオブジェクトの文字列表現
    324         * @og.rtnNotNull
    325         * @see OGNode#toString()
    326         * @see #setUseCR( boolean )
    327         */
    328        public String getText( final String attTab ) {
    329                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
    330
    331                final String crTab = attTab.length() > 0 ? attTab : CR + "\t" ;
    332
    333                if( useCR ) {
    334                        for( int i=0; i<size(); i++ ) {
    335                                final OGAtts atts = attList.get(i);
    336                                buf.append( crTab );
    337                                // 6.0.2.5 (2014/10/31) char を append する。
    338                                buf.append( atts.getAlignKey( maxValLen ) ).append( '=' ).append( atts.QRT_VAL );
    339                        }
    340                        // 5.6.1.2 (2013/02/22) 改行処理の修正。最後の属性処理の後にも改行を入れる。
    341                        buf.append( CR );
    342                }
    343                else {
    344                        int crCnt = 0;
    345                        int crLen = 0;
    346                        for( int i=0; i<size(); i++ ) {
    347                                final OGAtts atts = attList.get(i);
    348                                crCnt++ ;
    349                                crLen += atts.LEN;
    350                                // 6.0.2.5 (2014/10/31) char を append する。
    351                                if( i>0 && (crCnt > CR_CNT || crLen > CR_LEN) ) {
    352                                        buf.append( crTab );
    353                                        buf.append( atts.KEY ).append( '=' ).append( atts.QRT_VAL );
    354                                        crCnt = 0;
    355                                        crLen = 0;
    356                                }
    357                                else {
    358                                        buf.append( ' ' ).append( atts.KEY ).append( '=' ).append( atts.QRT_VAL );
    359                                }
    360                        }
    361                }
    362
    363                return buf.toString();
    364        }
    365
    366        /**
    367         * オブジェクトの文字列表現を返します。
    368         *
    369         * @og.rev 5.6.1.2 (2013/02/22) 改行処理の修正。最後の属性処理の後にも改行を入れる。
    370         *
    371         * @return      このオブジェクトの文字列表現
    372         * @og.rtnNotNull
    373         * @see OGNode#toString()
    374         */
    375        @Override
    376        public String toString() {
    377                return getText( " " );
    378        }
    379
    380        /**
    381         * 属性キーと属性値を管理する クラス
    382         *
    383         * 属性自身は、属性キーと属性値のみで十分ですが、改行処理や文字列の長さ設定で、
    384         * 予め内部処理をしておきたいため、クラス化しています。
    385         *
    386         * 内部変数は、final することで定数化し、アクセスメソッド経由ではなく、直接内部変数を
    387         * 参照させることで、見易さを優先しています。
    388         *
    389         * @og.rev 6.3.9.1 (2015/11/27) private static final class に変更。
    390         *
    391         */
    392        private static final class OGAtts {
    393                /** 属性の長さをそろえるための空白文字の情報 **/
    394                private static final String     SPACE  = "                    ";        // 5.1.9.0 (2010/09/01) public ⇒ private へ変更
    395
    396                /** 属性キー **/
    397                private         final String    KEY ;
    398                /** 属性値 **/
    399                private         final String    VAL ;
    400                private         final int               KLEN ;
    401                private         final int               LEN ;
    402                private         final String    QRT_VAL;
    403
    404                /**
    405                 * 引数を指定して構築する、コンストラクター
    406                 *
    407                 * 属性キーと、属性値 を指定して、オブジェクトを構築します。
    408                 *
    409                 *
    410                 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、public → なし(パッケージプライベート)に変更。
    411                 * @param       key     属性キー
    412                 * @param       val     属性値
    413                 */
    414                OGAtts( final String key , final String val ) {
    415                        KEY  = key;
    416                        VAL  = (val == null) ? "" : htmlFilter(val) ;
    417                        KLEN = key.length();
    418                        LEN  = KLEN + VAL.length();
    419
    420                        QRT_VAL = VAL.indexOf( '"' ) >= 0 ? "'" + VAL + "'" : "\"" + VAL + "\"" ;
    421                }
    422
    423                /**
    424                 * キーの文字長さの比較で、大きい数字を返します。
    425                 *
    426                 * 属性キーの最大の文字列長を求めるため、引数の長さと、属性キーの長さを比較して、
    427                 * 大きな値の方を返します。
    428                 * この処理を、属性すべてに行えば、最終的に最も大きな値が残ることになります。
    429                 *
    430                 * @param       maxLen  属性キーの最大長さ
    431                 *
    432                 * @return      属性リスト群の長さ補正が行われた、属性キー＋空白文字列
    433                 */
    434                /* default */ int maxKeyLen( final int maxLen ) {
    435                        return maxLen > KLEN ? maxLen : KLEN ;
    436                }
    437
    438                /**
    439                 * 長さ補正が行われた属性キーを取得します。
    440                 *
    441                 * useCR=true の場合に、属性の改行が行われますが、そのときに、キーが縦に並びます。
    442                 * そして、値も縦に並ぶため、間の ｢＝｣記号の位置をそろえて、表示します。
    443                 * 属性リストの最大長さ＋１ になるように、キーの文字列にスペースを埋めます。
    444                 * これにより、属性を改行して表示しても、値の表示位置がそろいます。
    445                 *
    446                 * @param       maxLen  属性キーの最大長さ
    447                 *
    448                 * @return      属性リスト群の長さ補正が行われた、属性キー＋空白文字列
    449                 * @og.rtnNotNull
    450                 */
    451                /* default */ String getAlignKey( final int maxLen ) {
    452                        return KEY + SPACE.substring( KLEN,maxLen ) ;
    453                }
    454
    455                /**
    456                 * HTML上のエスケープ文字を変換します。
    457                 *
    458                 * HTMLで表示する場合にきちんとエスケープ文字に変換しておかないと
    459                 * Script を実行されたり、不要なHTMLコマンドを潜り込まされたりするため、
    460                 * セキュリティーホールになる可能性があるので、注意してください。
    461                 *
    462                 * ※ オリジナルは、org.opengion.fukurou.util.StringUtil#htmlFilter( String )
    463                 * ですが、ダブルクオート、シングルクオートの変換処理を省いています。
    464                 *
    465                 * @param       input HTMLエスケープ前の文字列
    466                 *
    467                 * @return      エスケープ文字に変換後の文字列
    468                 * @og.rtnNotNull
    469                 * @see  org.opengion.fukurou.util.StringUtil#htmlFilter( String )
    470                 */
    471                private String htmlFilter( final String input ) {
    472                        if( input == null || input.isEmpty() ) { return ""; }
    473                        final StringBuilder rtn = new StringBuilder( BUFFER_MIDDLE );
    474                        char ch;
    475                        for( int i=0; i<input.length(); i++ ) {
    476                                ch = input.charAt(i);
    477                                switch( ch ) {
    478                                        case '<'  : rtn.append("&lt;");         break;
    479                                        case '>'  : rtn.append("&gt;");         break;
    480                        //              case '"'  : rtn.append("&quot;");       break;
    481                        //              case '\'' : rtn.append("&#39;");        break;
    482                                        case '&'  : rtn.append("&amp;");        break;
    483                                        default   : rtn.append(ch);                     break;          // 6.0.2.5 (2014/10/31) break追記
    484                                }
    485                        }
    486                        return rtn.toString() ;
    487                }
    488        }
    489}