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;
019
020import org.opengion.fukurou.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
021
022/**
023 * エレメントをあらわす、OGElement クラスを定義します。
024 *
025 * エレメントは、OGNode クラスを継承し、名称、属性、ノードリストを持つオブジェクトです。
026 * 通常で言うところの、タグになります。
027 * 属性は、OGAttributes クラスで管理します。ノードリスト に関する操作は、OGNodeクラスの実装です。
028 *
029 * OGNode は、enum OGNodeType で区別される状態を持っています。
030 * OGNodeType は、それぞれ、再設定が可能です。
031 * 例えば、既存のエレメントやノードに対して、コメントタイプ(Comment)を指定すると、
032 * ファイル等への出力時にコメントとして出力されます。
033 *
034 * @og.rev 5.1.8.0 (2010/07/01) 新規作成
035 *
036 * @version  5.0
037 * @author   Kazuhiko Hasegawa
038 * @since    JDK6.0,
039 */
040// 8.5.5.1 (2024/02/29) spotbugs CT_CONSTRUCTOR_THROW(コンストラクタで、Excweptionを出さない) class を final にすれば、警告は消える。
041// public class OGElement extends OGNode {
042public final class OGElement extends OGNode {
043
044        private final String            qName   ;               // このタグの名前(nameSpace も含むエレメントの名前)
045        private           OGAttributes  attri   ;               // 属性オブジェクト
046
047        /** 階層に応じたスペースの設定 */
048        private static final int      PARA_LEN  = 8;
049        private static final String   PARA_CHAR = "\t";
050        private static final String[] PARA = new String[PARA_LEN];
051        static {
052                PARA[0] = CR;
053                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
054                buf.append( CR );
055                for( int i=1; i<PARA_LEN; i++ ) {
056                        buf.append( PARA_CHAR );
057                        PARA[i] = buf.toString();
058                }
059        }
060
061        /**
062         * ノード名を指定してのトコンストラクター
063         *
064         * ノード名のみ指定するため、属性と、ノードリストが空のエレメントを構築します。
065         *
066         * @param       qName   ノード名
067         */
068        public OGElement( final String qName ) {
069                this( qName,null );
070        }
071
072        /**
073         * ノード名、属性タブ、属性リストを指定してのトコンストラクター
074         *
075         * 注意 属性値の正規化は必ず行われます。
076         * 属性値に含まれるCR(復帰), LF(改行), TAB(タブ)は、半角スペースに置き換えられます。
077         * XMLの規定では、属性の並び順は保障されませんが、SAXのAttributesは、XMLに記述された順番で
078         * 取得できていますので、このクラスでの属性リストも、記述順での並び順になります。
079         *
080         * @og.rev 5.2.1.0 (2010/10/01) タグ属性の改行処理を、Set からString[] に変更。
081         * @og.rev 5.6.1.2 (2013/02/22) CR_SET を配列から文字列に変更
082         * @og.rev 8.5.3.2 (2023/10/13) JDK21対応。警告: [this-escape] サブクラスが初期化される前の'this'エスケープの可能性があります
083         *
084         * @param       qName   ノード名
085         * @param       atts    属性リスト
086         */
087        public OGElement( final String qName , final Attributes atts ) {
088                super( OGNodeType.Element );
089//              super();
090//              setNodeType( OGNodeType.Element );
091
092                if( qName == null ) {
093                        final String errMsg = "エレメントには、ノード名は必須です。";
094                        throw new OgRuntimeException( errMsg );
095                }
096
097                this.qName = qName;
098                this.attri = new OGAttributes( atts ) ;
099        }
100
101        /**
102         * ノード名を返します。
103         *
104         * @return      ノード名
105         */
106        public String getTagName() {
107                return qName;
108        }
109
110        /**
111         * 属性オブジェクトを返します。
112         *
113         * これは、org.xml.sax.Attributes ではなく、OGAttributes オブジェクトを返します。
114         * 内部オブジェクトそのものを返しますので、この OGAttributes の変更は、この
115         * エレメントが持つ内部属性も変更されます。
116         *
117         * @return      属性オブジェクト
118         */
119        public OGAttributes getOGAttributes() {
120                return attri;
121        }
122
123        /**
124         * 属性オブジェクトをセットします。
125         *
126         * 属性オブジェクトのセットは、このメソッドからのみできるようにします。
127         * 内部オブジェクトそのものにセットしますので、異なる OGAttributes をセットしたい場合は、
128         * 外部で、コピーしてからセットしてください。
129         *
130         * @og.rev 5.6.1.2 (2013/02/22) 新規追加
131         *
132         * @param       attri 属性オブジェクト(org.opengion.fukurou.xml.OGAttributes)
133         */
134        public void setOGAttributes( final OGAttributes attri ) {
135                this.attri = attri;
136        }
137
138        /**
139         * 属性リストから、id属性の、属性値を取得します。
140         *
141         * id属性 は、内部的にキャッシュしており、すぐに取り出せます。
142         * タグを特定する場合、一般属性のキーと値で選別するのではなく、
143         * id属性を付与して選別するようにすれば、高速に見つけることが可能になります。
144         *
145         * @og.rev 5.1.9.0 (2010/08/01) 新規追加
146         *
147         * @return      id属性値
148         */
149        public String getId() {
150                // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
151                return attri == null ? null : attri.getId() ;
152        }
153
154        /**
155         * 属性リストから、指定の属性キーの、属性値を取得します。
156         *
157         * この処理は、属性リストをすべてスキャンして、キーにマッチする
158         * 属性オブジェクトを見つけ、そこから、属性値を取り出すので、
159         * パフォーマンスに問題があります。
160         * 基本的には、アドレス指定で、属性値を取り出すようにしてください。
161         *
162         * @og.rev 5.6.1.2 (2013/02/22) 新規追加
163         *
164         * @param       key     属性キー
165         *
166         * @return      属性値
167         */
168        public String getVal( final String key ) {
169                // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
170                return attri == null ? null : attri.getVal( key ) ;
171        }
172
173        /**
174         * 属性リストに、属性(キー、値のセット)を設定します。
175         *
176         * 属性リストの一番最後に、属性(キー、値のセット)を設定します。
177         *
178         * @param       key     属性リストのキー
179         * @param       val     属性リストの値
180         */
181        public void addAttr( final String key , final String val ) {
182                if( attri == null ) { attri = new OGAttributes() ; }
183                attri.add( key,val ) ;
184        }
185
186        /**
187         * 自分自身の状態が、指定の条件に合致しているかどうか、判定します。
188         *
189         * 合致している場合は、true を、合致していない場合は、false を返します。
190         *
191         * 指定の属性が null の場合は、すべてに合致すると判断します。
192         * 例えば、kye のみ指定すると、その属性名を持っているエレメントすべてで
193         * true が返されます。
194         * 実行速度を考えると、ノード名は指定すべきです。
195         *
196         * @param       name    ノード名 null の場合は、すべての ノード名 に合致
197         * @param       key     属性名 null の場合は、すべての 属性名 に合致
198         * @param       val     属性値 null の場合は、すべての 属性値 に合致
199         *
200         * @return      条件がこのエレメントに合致した場合 true
201         */
202        public boolean match( final String name , final String key , final String val ) {
203                // name が存在するが、不一致の場合は、false
204                if( name != null && ! name.equals( qName ) ) { return false; }
205
206                // attri が null なのに、key か val が、null でない場合は合致しないので、false と判断
207                if( attri == null && ( key != null || val != null ) ) { return false; }
208
209                // キーが存在し、値も存在する場合は、その値の合致と同じ結果となる。
210                if( key != null ) {
211                        // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
212                        return val == null
213                                                ? attri.getAdrs( key ) >= 0                                     // 値がなければ、存在チェック
214                                                : val.equals( attri.getVal( key ) ) ;           // 値があれば、比較する。
215                }
216
217                // 値が存在する場合は、その値が含まれるかチェックし、あれば、true, なければ false
218                if( val != null ) {
219                        boolean flag = false;
220                        final int len = attri.size();
221                        for( int i=0; i<len; i++ ) {
222                                if( val.equals( attri.getVal(i) ) ) { flag = true; break; }
223                        }
224                        return flag;
225                }
226
227                // 上記の条件以外は、すべてが null なので、true
228                return true;
229        }
230
231        /**
232         * 段落文字列を返します。
233         *
234         * 段落文字列は、階層を表す文字列です。
235         * 通常は TAB ですが、XMLの階層が、PARA_LEN を超えても、段落を増やしません。
236         * 段落の最初の文字は、改行です。
237         *
238         * @og.rev 5.6.1.2 (2013/02/22) 内部テキストがない場合のタグの終了時にスペースは入れない。
239         * @og.rev 5.6.4.4 (2013/05/31) PARA_LEN を超えても、段落を増やしません。
240         *
241         * @param       cnt     階層(-1:なし。
242         * @return      段落文字列
243         * @og.rtnNotNull
244         * @see OGNodeType
245         */
246        private String getPara( final int cnt ) {
247                // 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
248                return cnt < 0
249                                        ? ""
250                                        : ( cnt < PARA_LEN
251                                                        ? PARA[cnt]
252                                                        : PARA[PARA_LEN-1] ) ;                  // 5.6.4.4 (2013/05/31) PARA_LEN を超えても、段落を増やしません。
253        }
254
255        /**
256         * オブジェクトの文字列表現を返します。
257         *
258         * 文字列は、OGNodeType により異なります。
259         * Comment ノードの場合は、コメント記号を、Cdata ノードの場合は、CDATA を
260         * つけて出力します。
261         *
262         * @og.rev 5.6.1.2 (2013/02/22) 内部テキストがない場合のタグの終了時にスペースは入れない。
263         * @og.rev 5.6.4.4 (2013/05/31) 改行3つを改行2つに置換します。
264         * @og.rev 8.5.5.1 (2024/02/29) switch文にアロー構文を使用
265         *
266         * @param       cnt             Nodeの階層(-1:なし、0:改行のみ、1:改行+"  "・・・・)
267         * @return      このオブジェクトの文字列表現
268         * @og.rtnNotNull
269         * @see OGNode#toString()
270         */
271        @Override
272        public String getText( final int cnt ) {
273
274                // 6.0.2.5 (2014/10/31) char を append する。
275                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
276                        .append( getPara(cnt) )
277                        .append( '<' ).append( qName )
278                        .append( attri.getText( getPara(cnt+1) ) );
279
280                final String text = super.getText(cnt+1);
281
282//              if( text.trim().isEmpty() ) {
283                if( text.isBlank() ) {                                  // 8.5.4.2 (2024/01/12) PMD 7.0.0 InefficientEmptyStringCheck 対応
284                        buf.append( "/>" );                                     // 5.6.1.2 (2013/02/22) タグの終了時にスペースは入れない。       XML なので、このまま。
285                }
286                else {
287                        buf.append( '>' ).append( text )
288                                .append( getPara(cnt) )
289                                .append( "</" ).append( qName ).append( '>' );
290                        //      .append( CR );
291                }
292
293                // 6.4.2.1 (2016/02/05) PMD refactoring. Prefer StringBuffer over += for concatenating strings
294                // 8.5.5.1 (2024/02/29) switch文にアロー構文を使用
295//              switch( getNodeType() ) {
296//                      case Comment:   buf.insert( 0,"<!-- "      ).append( "-->"  ); break;
297//                      case Cdata:             buf.insert( 0,"<![CDATA[ " ).append( " ]]>" ); break;
298//      //              case Text:
299//      //              case List:
300//                      default:                break;
301//              }
302                switch( getNodeType() ) {
303                        case Comment -> buf.insert( 0,"<!-- "      ).append( "-->"  );
304                        case Cdata ->           buf.insert( 0,"<![CDATA[ " ).append( " ]]>" );
305        //              case Text ->
306        //              case List ->
307                        default -> { /* 何もしない */ }
308                }
309
310                return buf.toString().replaceAll( CR+CR+CR , CR+CR ) ;                  // 改行3つを改行2つに置換します。
311        }
312}