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;                        // 属性の改行出力を行うかどうか。個数制限が1と同じ
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         * タグによって、属性が多くなったり、意味が重要な場合は、属性1つづつに改行を
097         * 行いたいケースがあります。
098         * 属性改行をtrue に設定すると、属性一つづつで、改行を行います。
099         *
100         * false の場合は、自動的な改行処理が行われます。
101         * これは、属性の個数制限(CR_CNT)を超える場合は、改行を行います。
102         *
103         * @param       useCR   属性改行の有無(true:1属性単位の改行)
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         * 属性は、3つのパターンで文字列化を行います。
308         *  ・useCR=true の場合
309         *     この場合は、属性を1行ずつ改行しながら作成します。属性キーは、
310         *     最大長+1 でスペース埋めされて、整形されます。
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         * 属性リストの最大長さ+1 になるように、キーの文字列にスペースを埋めます。
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}