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