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 java.util.List;
019import java.util.ArrayList;
020import org.xml.sax.Attributes;
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   ;                       // 属性の改行出力を行うかどうか。個数制限が1と同じ
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         * タグによって、属性が多くなったり、意味が重要な場合は、属性1つづつに改行を
098         * 行いたいケースがあります。
099         * 属性改行をtrue に設定すると、属性一つづつで、改行を行います。
100         *
101         * false の場合は、自動的な改行処理が行われます。
102         * これは、属性の個数制限(CR_CNT)を超える場合は、改行を行います。
103         *
104         * @param       useCR   属性改行の有無(true:1属性単位の改行)
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         * 属性は、3つのパターンで文字列化を行います。
309         *  ・useCR=true の場合
310         *     この場合は、属性を1行ずつ改行しながら作成します。属性キーは、
311         *     最大長+1 でスペース埋めされて、整形されます。
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                                // 8.5.4.2 (2024/01/12) PMD 7.0.0 ConsecutiveAppendsShouldReuse 対応
337                                buf.append( crTab )
338                                // 6.0.2.5 (2014/10/31) char を append する。
339                                        .append( atts.getAlignKey( maxValLen ) ).append( '=' ).append( atts.QRT_VAL );
340                        }
341                        // 5.6.1.2 (2013/02/22) 改行処理の修正。最後の属性処理の後にも改行を入れる。
342                        buf.append( CR );
343                }
344                else {
345                        int crCnt = 0;
346                        int crLen = 0;
347                        for( int i=0; i<size(); i++ ) {
348                                final OGAtts atts = attList.get(i);
349                                crCnt++ ;
350                                crLen += atts.LEN;
351                                // 6.0.2.5 (2014/10/31) char を append する。
352                                if( i>0 && (crCnt > CR_CNT || crLen > CR_LEN) ) {
353                                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 ConsecutiveAppendsShouldReuse 対応
354                                        buf.append( crTab )
355                                                .append( atts.KEY ).append( '=' ).append( atts.QRT_VAL );
356                                        crCnt = 0;
357                                        crLen = 0;
358                                }
359                                else {
360                                        buf.append( ' ' ).append( atts.KEY ).append( '=' ).append( atts.QRT_VAL );
361                                }
362                        }
363                }
364
365                return buf.toString();
366        }
367
368        /**
369         * オブジェクトの文字列表現を返します。
370         *
371         * @og.rev 5.6.1.2 (2013/02/22) 改行処理の修正。最後の属性処理の後にも改行を入れる。
372         *
373         * @return      このオブジェクトの文字列表現
374         * @og.rtnNotNull
375         * @see OGNode#toString()
376         */
377        @Override
378        public String toString() {
379                return getText( " " );
380        }
381
382        /**
383         * 属性キーと属性値を管理する クラス
384         *
385         * 属性自身は、属性キーと属性値のみで十分ですが、改行処理や文字列の長さ設定で、
386         * 予め内部処理をしておきたいため、クラス化しています。
387         *
388         * 内部変数は、final することで定数化し、アクセスメソッド経由ではなく、直接内部変数を
389         * 参照させることで、見易さを優先しています。
390         *
391         * @og.rev 6.3.9.1 (2015/11/27) private static final class に変更。
392         *
393         */
394        private static final class OGAtts {
395                /** 属性の長さをそろえるための空白文字の情報 **/
396                private static final String     SPACE  = "                    ";        // 5.1.9.0 (2010/09/01) public ⇒ private へ変更
397
398                /** 属性キー **/
399                private         final String    KEY ;
400                /** 属性値 **/
401                private         final String    VAL ;
402                private         final int               KLEN ;
403                private         final int               LEN ;
404                private         final String    QRT_VAL;
405
406                /**
407                 * 引数を指定して構築する、コンストラクター
408                 *
409                 * 属性キーと、属性値 を指定して、オブジェクトを構築します。
410                 *
411                 *
412                 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、public → なし(パッケージプライベート)に変更。
413                 * @param       key     属性キー
414                 * @param       val     属性値
415                 */
416                /* default */ OGAtts( final String key , final String val ) {
417                        KEY  = key;
418                        VAL  = val == null ? "" : htmlFilter(val) ;
419                        KLEN = key.length();
420                        LEN  = KLEN + VAL.length();
421
422                        QRT_VAL = VAL.indexOf( '"' ) >= 0 ? ("'" + VAL + "'") : ("\"" + VAL + "\"") ;
423                }
424
425                /**
426                 * キーの文字長さの比較で、大きい数字を返します。
427                 *
428                 * 属性キーの最大の文字列長を求めるため、引数の長さと、属性キーの長さを比較して、
429                 * 大きな値の方を返します。
430                 * この処理を、属性すべてに行えば、最終的に最も大きな値が残ることになります。
431                 *
432                 * @param       maxLen  属性キーの最大長さ
433                 *
434                 * @return      属性リスト群の長さ補正が行われた、属性キー+空白文字列
435                 */
436                /* default */ int maxKeyLen( final int maxLen ) {
437                        return maxLen > KLEN ? maxLen : KLEN ;
438                }
439
440                /**
441                 * 長さ補正が行われた属性キーを取得します。
442                 *
443                 * useCR=true の場合に、属性の改行が行われますが、そのときに、キーが縦に並びます。
444                 * そして、値も縦に並ぶため、間の 「=」記号の位置をそろえて、表示します。
445                 * 属性リストの最大長さ+1 になるように、キーの文字列にスペースを埋めます。
446                 * これにより、属性を改行して表示しても、値の表示位置がそろいます。
447                 *
448                 * @param       maxLen  属性キーの最大長さ
449                 *
450                 * @return      属性リスト群の長さ補正が行われた、属性キー+空白文字列
451                 * @og.rtnNotNull
452                 */
453                /* default */ String getAlignKey( final int maxLen ) {
454                        return KEY + SPACE.substring( KLEN,maxLen ) ;
455                }
456
457                /**
458                 * HTML上のエスケープ文字を変換します。
459                 *
460                 * HTMLで表示する場合にきちんとエスケープ文字に変換しておかないと
461                 * Script を実行されたり、不要なHTMLコマンドを潜り込まされたりするため、
462                 * セキュリティーホールになる可能性があるので、注意してください。
463                 *
464                 * ※ オリジナルは、org.opengion.fukurou.util.StringUtil#htmlFilter( String )
465                 * ですが、ダブルクオート、シングルクオートの変換処理を省いています。
466                 *
467                 * @og.rev 8.5.5.1 (2024/02/29) switch文にアロー構文を使用
468                 *
469                 * @param       input HTMLエスケープ前の文字列
470                 *
471                 * @return      エスケープ文字に変換後の文字列
472                 * @og.rtnNotNull
473                 * @see  org.opengion.fukurou.util.StringUtil#htmlFilter( String )
474                 */
475                private String htmlFilter( final String input ) {
476                        if( input == null || input.isEmpty() ) { return ""; }
477
478                        final StringBuilder rtn = new StringBuilder( BUFFER_MIDDLE );
479//                      char ch;
480                        for( int i=0; i<input.length(); i++ ) {
481//                              ch = input.charAt(i);
482                                final char ch = input.charAt(i);
483                                // 8.5.5.1 (2024/02/29) switch文にアロー構文を使用
484//                              switch( ch ) {
485//                                      case '<'  : rtn.append("&lt;");         break;
486//                                      case '>'  : rtn.append("&gt;");         break;
487//                      //              case '"'  : rtn.append("&quot;");       break;
488//                      //              case '\'' : rtn.append("&#39;");        break;
489//                                      case '&'  : rtn.append("&amp;");        break;
490//                                      default   : rtn.append(ch);                     break;          // 6.0.2.5 (2014/10/31) break追記
491//                              }
492                                switch( ch ) {
493                                        case '<' -> rtn.append("&lt;");
494                                        case '>' -> rtn.append("&gt;");
495                        //              case '"' -> rtn.append("&quot;")
496                        //              case '\'' -> rtn.append("&#39;");
497                                        case '&' -> rtn.append("&amp;");
498                                        default  -> rtn.append(ch);                             // 6.0.2.5 (2014/10/31) break追記
499                                }
500                        }
501                        return rtn.toString() ;
502                }
503        }
504}