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