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;
020
021import org.opengion.fukurou.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
022import org.opengion.fukurou.system.HybsConst;                                                   // 6.1.0.0 (2014/12/26) refactoring
023
024/**
025 * ノードの基底クラスとなる、OGNode クラスを定義します。
026 *
027 * OGElement、OGDocument は、この、OGNode クラスを継承します。
028 * ただし、OGAttributes は、独立しているため、このクラスは継承していません。
029 *
030 * 最も一般的なノードは、テキストノードであり、
031 *
032 * OGNode は、enum OGNodeType で区別される状態を持っています。
033 * その内、OGElement と OGDocument は、サブクラスになっています。
034 * OGNodeType は、それぞれ、再設定が可能です。
035 * 例えば、既存のエレメントやノードに対して、コメントタイプ(Comment)を指定すると、
036 * ファイル等への出力時にコメントとして出力されます。
037 *
038 *   List   :内部に、OGNode の ArrayList を持つ
039 *   Text   :内部は、文字列の BODY 部分を持つ
040 *   Comment  :内部は、文字列であるが、toString() 時には、コメント記号を前後に出力する。
041 *   Cdata   :内部は、TextNodeのArrayList を持つ、toString() 時には、Cdataを前後に出力する。
042 *   Element  :タグ名、属性、OGNode の ArrayList の入れ子状態をもつ
043 *   Document :トップのElement として、read/write するときに使用。構造は、唯一の OGElement を持つ List タイプ
044 *
045 * @og.rev 5.1.8.0 (2010/07/01) 新規作成
046 * @og.rev 5.6.1.2 (2013/02/22) 構想からやり直し
047 *
048 * @version  5.0
049 * @author   Kazuhiko Hasegawa
050 * @since    JDK6.0,
051 */
052public class OGNode {
053        /** システムの改行コードを設定します。*/
054        protected static final String CR                 = HybsConst.CR;                        // 6.1.0.0 (2014/12/26) refactoring
055        /** StringBilderなどの初期値を設定します。   {@value} */
056        protected static final int BUFFER_MIDDLE = HybsConst.BUFFER_MIDDLE;     // 6.1.0.0 (2014/12/26) refactoring
057
058        private final List<OGNode> nodes = new ArrayList<>();   // ノードリスト
059        private final String    text            ;                                               // テキストノード用の文字列ノード値
060        private OGNodeType              nodeType        ;                                               // List,Text,Comment,Cdata,Element,Document
061        private OGNode                  parentNode      ;                                               // 自身の親ノード(ただし、最終セットされたノード)
062
063        /**
064         * デフォルトコンストラクター
065         *
066         * ここでは、NodeType は、List に設定されます。
067         */
068        public OGNode() {
069                this.text = null;
070                nodeType  = OGNodeType.List;
071        }
072
073        /**
074         * OGNodeTypeを指定したコンストラクター
075         *
076         * 基本的には、Element かDocument のケースです。
077         *
078         * @og.rev 8.5.3.2 (2023/10/13) JDK21対応。警告: [this-escape] サブクラスが初期化される前の'this'エスケープの可能性があります
079         *
080         * @param       type    OGNodeTypeのenum(List,Text,Comment,Cdata,Element,Document)
081         */
082        public OGNode( final OGNodeType type ) {
083                this.text = null;
084                nodeType  = type;
085        }
086
087        /**
088         * テキストノードを構築するためのコンストラクター
089         *
090         * テキストノードは、簡易的に、内部には、ノードリストではなく文字列を持っています。
091         *
092         * @og.rev 5.6.1.2 (2013/02/22) 内部テキストがない場合のタグの終了時にスペースは入れない。
093         *
094         * ここでは、NodeType は、Text に設定されます。
095         * ただし、引数のテキストが null のNodeType は、List に設定されます。
096         *
097         * @param       txt     テキストノードの設定値
098         */
099        public OGNode( final String txt ) {
100                text = txt ;
101                // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
102                nodeType = text == null ? OGNodeType.List : OGNodeType.Text;
103        }
104
105        /**
106         * テキストノードをノードリストに追加します。
107         *
108         * 内部的にテキストノードを構築して、リストに追加しています。
109         * 戻り値は、StringBuilder#append(String) の様に、連結登録できるように
110         * 自分自身を返しています。
111         * テキストノードに、この処理を行うと、エラーになります。
112         * 一旦、テキストノードとして作成したノードには、ノードを追加できません。
113         *
114         * @param       txt     テキストノードの設定値
115         *
116         * @return      自分自身(this)のノード
117         * @og.rtnNotNull
118         */
119        public OGNode addNode( final String txt ) {
120                if( txt != null ) {
121                        if( nodeType == OGNodeType.Text ) {
122                                // テキストノードにノードは追加できません。
123                                final String errMsg = "一旦、テキストノードとして作成したノードには、ノードを追加できません。";
124                                throw new OgRuntimeException( errMsg );
125                        }
126
127                        final OGNode node = new OGNode( txt );
128                        node.parentNode = this;
129                        nodes.add( node );
130                }
131                return this;
132        }
133
134        /**
135         * ノードをノードリストに追加します。
136         *
137         * 追加するノードの親として、自分自身を登録します。
138         * なお、同じオブジェクトを、複数の親に追加する場合(ノードリストには追加可能)は、
139         * 親ノードは、最後に登録されたノードのみが設定されます。
140         * テキストノードに、この処理を行うと、エラーになります。
141         * 一旦、テキストノードとして作成したノードには、ノードを追加できません。
142         *
143         * @param       node    ノード
144         *
145         * @return      自分自身(this)のノード
146         * @og.rtnNotNull
147         */
148        public OGNode addNode( final OGNode node ) {
149                if( node != null ) {
150                        if( nodeType == OGNodeType.Text ) {
151                                // テキストノードにノードは追加できません。
152                                final String errMsg = "一旦、テキストノードとして作成したノードには、ノードを追加できません。";
153                                throw new OgRuntimeException( errMsg );
154                        }
155
156                        node.parentNode = this;
157                        nodes.add( node );
158                }
159                return this;
160        }
161
162        /**
163         * ノードリストに追加されている、ノードの個数を返します。
164         *
165         * @return      ノードリストの数
166         */
167        public int nodeSize() {
168                return nodes.size();
169        }
170
171        /**
172         * ノードリストに追加されている、ノードを返します。
173         *
174         * ノードの指定には、配列番号を使用します。
175         * ノードの個数は、事前に、nodeSize() で調べて置いてください。
176         * 当然、テキストノードの場合は、nodeSize()==0 なので、
177         * このメソッドでは取得できません。
178         *
179         * @param       adrs    ノードリストの位置
180         *
181         * @return      指定の配列番号のノード
182         */
183        public OGNode getNode( final int adrs ) {
184                return nodes.get(adrs);
185        }
186
187        /**
188         * ノードリストに、ノードをセットします。
189         *
190         * ノードリストの指定のアドレスに、ノードをセットします。
191         * これは、追加ではなく置換えになります。
192         * ノードの指定には、配列番号を使用します。
193         * ノードの個数は、事前に、nodeSize() で調べて置いてください。
194         *
195         * @param       adrs    ノードリストの位置
196         * @param       node    セットするノード
197         */
198        public void setNode( final int adrs , final OGNode node ) {
199                nodes.set(adrs,node);
200        }
201
202        /**
203         * 自身にセットされている、親ノードを返します。
204         *
205         * 親ノードは、自身のオブジェクトに、一つしか設定できません。
206         * これは、オブジェクトとして、同一ノードを、複数の親ノードに
207         * 追加した場合(これは、ノードリストへの追加なので可能)最後に追加した
208         * 親ノードのみ、保持していることになります。
209         * XML を構築するときは、同一のノードであっても、毎回、作成しなおさないと、
210         * 親ノードを見つけて、何かを行う場合には、おかしな動きをすることになります。
211         * なお、ノードオブジェクト自体が、親ノードから削除されても、自身の
212         * 親ノード情報は保持し続けています。
213         * ある Element から削除したノードを別のElementに追加すると、その時点で、
214         * 親ノードも更新されます。
215         *
216         * @return      親ノード
217         */
218        public OGNode getParentNode() {
219                return parentNode;
220        }
221
222        /**
223         * 自身にセットされている、親ノードの階層数を返します。
224         *
225         * 自身のオブジェクトに設定されている親ノードを順番にさかのぼって、
226         * 何階層あるか返します。
227         * これは、getText(int) の引数に使えます。
228         * 親ノードがひとつもない場合、つまり自身が最上位の場合は、0 が返されます。
229         *
230         * @return      自身の階層
231         */
232        public int getParentCount() {
233                int para = 0;
234                OGNode node = getParentNode();
235                while( node != null ) {
236                        para++ ;
237                        node = node.getParentNode();
238                }
239                return para;
240        }
241
242        /**
243         * ノードリストから、指定の配列番号の、ノードを削除します。
244         *
245         * ノードの指定には、配列番号を使用します。
246         * ノードの個数は、事前に、nodeSize() で調べて置いてください。
247         *
248         * @param       adrs    ノードリストの位置
249         *
250         * @return      削除されたノード
251         */
252        public OGNode removeNode( final int adrs ) {
253                return nodes.remove(adrs);
254        }
255
256        /**
257         * ノードリストから、すべてのノードを削除します。
258         *
259         * これは、ノードリストをクリアします。
260         *
261         */
262        public void clearNode() {
263                nodes.clear();
264        }
265
266        /**
267         * ノードリストから、指定のノード(orgNode)を新しいノード(newNode)に置き換えます。
268         *
269         * ノードは、それぞれ、ノードが作成された順番で、ユニークな番号を持っています。
270         * その番号を元に、ノードを探し出して、置き換えます。
271         * 通常の、XMLパースから作成されたノードは、すべて一意にユニーク番号が振られますが、
272         * 新しくつったノードを複数のノードと置き換える場合、置き換えられた後のノードは、
273         * オブジェクトそのものが、同一になるため、注意が必要です。
274         *
275         * @param       orgNode 置換元のオリジナルノード
276         * @param       newNode 置換する新しいノード
277         */
278        public void changeNode( final OGNode orgNode , final OGNode newNode ) {
279                final int size = nodes.size();
280                for( int i=0; i<size; i++ ) {
281                        final OGNode node = nodes.get(i);
282                        if( node.equals( orgNode ) ) {          // Object.equals なので、オブジェクトそのものの一致判定
283                                nodes.set( i,newNode );
284                        }
285                        else {
286                                node.changeNode( orgNode,newNode );
287                        }
288                }
289        }
290
291        /**
292         * ノードリストから、直下(メンバー)のエレメントのみをリストにして返します。
293         *
294         * ノードリストの第一レベルで、エレメントのみを返します。
295         * 通常は、あるエレメントを、getElementList( String ) 等で検索した後、その子要素を
296         * 取り出す場合に使用します。
297         * 該当するエレメントが、なにも存在しない場合は、空のリストオブジェクトが返されます。
298         *
299         * @return      直下(メンバー)のエレメントのリスト
300         */
301        public List<OGElement> getChildElementList() {
302                final List<OGElement> eles = new ArrayList<>();
303
304                for( final OGNode node : nodes ) {
305                        if( node.nodeType == OGNodeType.Element ) {
306                                eles.add( (OGElement)node );
307                        }
308                }
309
310                return eles;
311        }
312
313        /**
314         * ノードリストから、下位の階層に存在するすべてのエレメントをリストにして返します。
315         *
316         * エレメントは、名前を指定して検索します。
317         * 該当するエレメントが、なにも存在しない場合は、空のリストオブジェクトが返されます。
318         *
319         * @param       qName   エレメントの名前
320         *
321         * @return      下位の階層に存在するすべてのエレメントのリスト
322         */
323        public List<OGElement> getElementList( final String qName ) {
324                final List<OGElement> eles = new ArrayList<>();
325
326                if( qName != null ) {
327                        for( final OGNode node : nodes ) {
328                                if( node.nodeType == OGNodeType.Element ) {
329                                        final OGElement ele = (OGElement)node;
330                                        if( qName.equals( ele.getTagName() ) ) {
331                                                eles.add( ele );
332                                        }
333                                        eles.addAll( ele.getElementList( qName ) );
334                                }
335                        }
336                }
337
338                return eles;
339        }
340
341        /**
342         * ノードタイプを設定します。
343         *
344         * ノードタイプとは、List , Text , Comment , Cdata , Element , Document などの
345         * ノードの種別を表す enum タイプです。
346         * 基本的には、オブジェクトの取得時に、ファクトリメソッド経由であれば、自動的に設定
347         * されています。
348         * ここでは、可変設定できます。
349         * 例えば、既存のエレメントやノードに対して、コメントタイプ(Comment)を指定すると、
350         * ファイル等への出力時にコメントとして出力されます。
351         * null を指定すると、なにも処理されません。
352         *
353         * @param       type    enumのOGNodeType
354         * @see OGNodeType
355         */
356        public void setNodeType( final OGNodeType type ) {
357                if( type != null ) {
358                        if( type != OGNodeType.Text && nodeType == OGNodeType.Text ) {
359                                final OGNode node = new OGNode( text );
360                                node.parentNode = this;
361                                nodes.add( node );
362                        }
363
364                        nodeType = type ;
365                }
366        }
367
368        /**
369         * ノードタイプを取得します。
370         *
371         * ノードタイプとは、List , Text , Comment , Cdata , Element , Document などの
372         * ノードの種別を表す enum タイプです。
373         * 基本的には、オブジェクトの取得時に、ファクトリメソッド経由であれば、自動的に設定
374         * されています。
375         *
376         * @return      ノードタイプ
377         * @see OGNodeType
378         */
379        public OGNodeType getNodeType() {
380                return nodeType;
381        }
382
383        /**
384         * ノードリストの文字列を返します。
385         *
386         * これは、タグで言うところのBODY部に書かれた文字列に相当します。
387         * 該当する文字列が、存在しない場合は、空の文字列(ゼロストリング)が返されます。
388         *
389         * @og.rev 8.5.5.1 (2024/02/29) switch文にアロー構文を使用
390         *
391         * @param       cnt             Nodeの階層
392         * @return      ノードリストの文字列(BODY部に書かれた文字列)
393         */
394        public String getText( final int cnt ) {
395                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
396
397                if( nodeType == OGNodeType.Text ) {
398                        buf.append( text );
399                }
400                else {
401                        for( final OGNode node : nodes ) {
402                                buf.append( node.getText( cnt ) );
403                        }
404                }
405
406                // 6.4.2.1 (2016/02/05) PMD refactoring. Prefer StringBuffer over += for concatenating strings
407                // 8.5.5.1 (2024/02/29) switch文にアロー構文を使用
408//              switch( nodeType ) {
409//                      case Comment:   buf.insert( 0,"<!-- "      ).append( "-->"  ); break;
410//                      case Cdata:             buf.insert( 0,"<![CDATA[ " ).append( " ]]>" ); break;
411//      //              case Document:
412//      //              case Text:
413//      //              case DTD:
414//      //              case List:
415//                      default:                break;
416//              }
417                switch( nodeType ) {
418                        case Comment -> buf.insert( 0,"<!-- "      ).append( "-->"  );
419                        case Cdata   -> buf.insert( 0,"<![CDATA[ " ).append( " ]]>" );
420        //              case Document ->
421        //              case Text ->
422        //              case DTD ->
423        //              case List ->
424                        default -> { /* 何もしない */ }
425                }
426
427                return buf.toString() ;
428        }
429
430        /**
431         * オブジェクトの文字列表現を返します。
432         *
433         * 文字列は、OGNodeType により異なります。
434         * Comment ノードの場合は、コメント記号を、Cdata ノードの場合は、CDATA を
435         * つけて出力します。
436         *
437         * @return      このオブジェクトの文字列表現
438         * @og.rtnNotNull
439         * @see Object#toString()
440         */
441        @Override
442        public String toString() {
443                return getText( -10 );
444        }
445}