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.io.Reader;
019import java.io.IOException;
020import java.util.Map;
021import javax.xml.parsers.SAXParserFactory;
022import javax.xml.parsers.SAXParser;
023import javax.xml.parsers.ParserConfigurationException;
024
025import org.xml.sax.InputSource;
026import org.xml.sax.SAXException;
027import org.xml.sax.Attributes;
028import org.xml.sax.helpers.DefaultHandler;
029
030import org.opengion.fukurou.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
031import static org.opengion.fukurou.system.HybsConst.CR;                         // 6.1.0.0 (2014/12/26) refactoring
032import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;      // 6.1.0.0 (2014/12/26) refactoring
033
034/**
035 * このクラスは、拡張オラクル XDK形式のXMLファイルを処理するハンドラです。
036 * オラクルXDK形式のXMLとは、下記のような ROWSET をトップとする ROW の
037 * 集まりで1レコードを表し、各ROWには、カラム名をキーとするXMLになっています。
038 *
039 *   <ROWSET>
040 *       <ROW num="1">
041 *           <カラム1>値1</カラム1>
042 *             ・・・
043 *           <カラムn>値n</カラムn>
044 *       </ROW>
045 *        ・・・
046 *       <ROW num="n">
047 *          ・・・
048 *       </ROW>
049 *   <ROWSET>
050 *
051 * この形式であれば、XDK(Oracle XML Developer's Kit)を利用すれば、非常に簡単に
052 * データベースとXMLファイルとの交換が可能です。
053 * <a href="https://docs.oracle.com/cd/F19136_01/adxdk/introduction-to-XDK.html" target="_blank" >
054 * XDK(Oracle XML Developer's Kit)</a>
055 *
056 * 拡張XDK形式とは、ROW 以外に、SQL処理用タグ(EXEC_SQL)を持つ XML ファイルです。
057 * また、登録するテーブル(table)を ROWSETタグの属性情報として付与することができます。
058 * (大文字小文字に注意)
059 * これは、オラクルXDKで処理する場合、無視されますので、同様に扱うことが出来ます。
060 * この、EXEC_SQL は、それそれの XMLデータをデータベースに登録する際に、
061 * SQL処理を自動的に流す為の、SQL文を記載します。
062 * この処理は、イベント毎に実行される為、その配置順は重要です。
063 * このタグは、複数記述することも出来ますが、BODY部には、1つのSQL文のみ記述します。
064 *
065 *   &lt;ROWSET tableName="XX" &gt;
066 *       &lt;EXEC_SQL&gt;                    最初に記載して、初期処理(データクリア等)を実行させる。
067 *           delete from GEXX where YYYYY
068 *       &lt;/EXEC_SQL&gt;
069 *       &lt;MERGE_SQL&gt;                   このSQL文で UPDATEして、結果が0件ならINSERTを行います。
070 *           update GEXX set AA=[AA] , BB=[BB] where CC=[CC]
071 *       &lt;/MERGE_SQL&gt;
072 *       &lt;ROW num="1"&gt;
073 *           &lt;カラム1&gt;値1&lt;/カラム1&gt;
074 *             ・・・
075 *           &lt;カラムn&gt;値n&lt;/カラムn&gt;
076 *       &lt;/ROW&gt;
077 *        ・・・
078 *       &lt;ROW num="n"&gt;
079 *          ・・・
080 *       &lt;/ROW&gt;
081 *       &lt;EXEC_SQL&gt;                    最後に記載して、項目の設定(整合性登録)を行う。
082 *           update GEXX set AA='XX' , BB='YY' where CC='ZZ'
083 *       &lt;/EXEC_SQL&gt;
084 *   &lt;ROWSET&gt;
085 *
086 * DefaultHandler クラスを拡張している為、通常の処理と同様に、使用できます。
087 *
088 *      InputSource input = new InputSource( reader );
089 *      HybsXMLHandler hndler = new HybsXMLHandler();
090 *
091 *      SAXParserFactory f = SAXParserFactory.newInstance();
092 *      SAXParser parser = f.newSAXParser();
093 *      parser.parse( input,hndler );
094 *
095 * また、上記の処理そのものを簡略化したメソッド:parse( Reader ) を持っているため、
096 * 通常そのメソッドを使用します。
097 *
098 * 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。
099 *   EXEC_SQL は、『;』で複数SQLを実行できます。
100 *   これに、属性 exists="0" があれば、最初のSQLを実行し、結果が 0 の場合のみ、
101 *   以下のSQLを実行します。
102 *
103 *       &lt;EXEC_SQL exists="0"&gt;
104 *           select count(*) from user_tables where table_name=upper('BONUS');
105 *           CREATE TABLE BONUS ( ・・・・ )
106 *       &lt;/EXEC_SQL&gt;
107 *
108 *   exists="0" があるため、1行目を実行後、結果が一致した場合(=0)は、CREATE TABLE文を実行します。
109 *   exists="1" を指定した場合は、(!=0)と同じで、0以外という意味になります。
110 *   値の判定は、検索処理後の1行目1列目の値で判定します。
111 *
112 * HybsXMLHandler には、TagElementListener をセットすることができます。
113 * これは、ROW 毎に 内部情報を TagElement オブジェクト化し、action( TagElement )
114 * が呼び出されます。この Listener を介して、1レコードずつ処理することが
115 * 可能です。
116 *
117 * @version     4.0
118 * @author      Kazuhiko Hasegawa
119 * @since       JDK5.0,
120 */
121public class HybsXMLHandler extends DefaultHandler {
122
123        /** このハンドラのトップタグ名 {@value} */
124        public static final     String ROWSET           = "ROWSET";
125        /** このハンドラで取り扱える ROWSETタグの属性 */
126        public static final     String ROWSET_TABLE = "tableName";
127
128        /** このハンドラで取り扱えるタグ名 {@value} */
129        public static final     String ROW                      = "ROW";
130        /** このハンドラで取り扱える ROWタグの属性 {@value} */
131        public static final     String ROW_NUM          = "num";
132
133        /** このハンドラで取り扱えるタグ名 {@value} */
134        public static final     String EXEC_SQL         = "EXEC_SQL";
135        /** このハンドラで取り扱える EXEC_SQLタグの属性 {@value} */
136        public static final     String EXEC_EXISTS      = "exists";             // 8.1.0.3 (2022/01/21)
137
138        /** このハンドラで取り扱えるタグ名 {@value} */
139        public static final     String MERGE_SQL        = "MERGE_SQL";
140
141        /** 6.4.3.1 (2016/02/12) 作成元のMapを、HashMap から ConcurrentHashMap に置き換え。 */
142        private Map<String,String>      defaultMap;
143        private TagElementListener listener     ;
144        private TagElement              element         ;
145        private String                  key                     ;
146        private boolean                 bodyIn          ;
147        private int                             level           ;
148
149        private final StringBuilder     body = new StringBuilder( BUFFER_MIDDLE );                      // 6.4.2.1 (2016/02/05) PMD refactoring.
150
151        /**
152         * デフォルトコンストラクター
153         *
154         * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
155         */
156        public HybsXMLHandler() { super(); }            // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
157
158        /**
159         * パース処理を行います。
160         * 通常のパース処理の簡易メソッドになっています。
161         *
162         * @og.rev 8.5.4.2 (2024/01/12) PMD 7.0.0 ExceptionAsFlowControl 対応
163         *
164         * @param       reader  パース処理用のReaderオブジェクト
165         */
166        public void parse( final Reader reader ) {
167                try {
168                        final SAXParserFactory fact = SAXParserFactory.newInstance();
169                        final SAXParser parser = fact.newSAXParser();
170
171                        final InputSource input = new InputSource( reader );
172
173                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 ExceptionAsFlowControl 対応
174//                      try {
175                                parser.parse( input,this );
176//                      }
177//                      catch( final SAXException ex ) {
178//                              if( ! "END".equals( ex.getMessage() ) ) {
179//                                      // 6.4.2.1 (2016/02/05) PMD refactoring.
180//                                      final String errMsg = "XMLパースエラー key=" + key + CR
181//                                                              + "element=" + element + CR
182//                                                              + ex.getMessage() + CR
183//                                                              + body.toString();
184//                                      throw new OgRuntimeException( errMsg,ex );
185//                              }
186//                      }
187                }
188                catch( final ParserConfigurationException ex1 ) {
189                        final String errMsg = "SAXParser のコンフィグレーションが構築できません。"
190                                                + "key=" + key + CR + ex1.getMessage();
191                        throw new OgRuntimeException( errMsg,ex1 );
192                }
193                catch( final SAXException ex ) {
194                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 ExceptionAsFlowControl 対応
195                        if( ! "END".equals( ex.getMessage() ) ) {
196                                // 6.4.2.1 (2016/02/05) PMD refactoring.
197                                final String errMsg = "XMLパースエラー key=" + key + CR
198                                                        + "element=" + element + CR
199                                                        + ex.getMessage() + CR
200                                                        + body.toString();
201                                throw new OgRuntimeException( errMsg,ex );
202                        }
203
204                        final String errMsg = "SAXParser が構築できません。"
205                                                + "key=" + key + CR + ex.getMessage();
206                        throw new OgRuntimeException( errMsg,ex );
207                }
208                catch( final IOException ex3 ) {
209                        final String errMsg = "InputSource の読み取り時にエラーが発生しました。"
210                                                + "key=" + key + CR + ex3.getMessage();
211                        throw new OgRuntimeException( errMsg,ex3 );
212                }
213        }
214
215        /**
216         * 内部に TagElementListener を登録します。
217         * これは、&lt;ROW&gt; タグの endElement 処理毎に呼び出されます。
218         * つまり、行データを取得都度、TagElement オブジェクトを作成し、
219         * この TagElementListener の action( TagElement ) メソッドを呼び出します。
220         * 何もセットしない、または、null がセットされた場合は、何もしません。
221         *
222         * @param       listener        TagElementListenerオブジェクト
223         */
224        public void setTagElementListener( final TagElementListener listener ) {
225                this.listener = listener;
226        }
227
228        /**
229         * TagElement オブジェクトを作成する時の 初期カラム/値を設定します。
230         * TagElements オブジェクトは、XMLファイルより作成する為、項目(カラム)も
231         * XMLファイルのROW属性に持っている項目と値で作成されます。
232         * このカラム名を、外部から初期設定することが可能です。
233         * その場合、ここで登録したカラム順(Mapに、LinkedHashMap を使用した場合)
234         * が保持されます。また、ROW属性に存在しないカラムがあれば、値とともに
235         * 初期値として設定しておくことが可能です。
236         * なお、ここでのMapは、直接設定していますので、ご注意ください。
237         *
238         * @param       map     初期カラムマップ
239         */
240        public void setDefaultMap( final Map<String,String> map ) {
241                defaultMap = map;
242        }
243
244        /**
245         * 要素内の文字データの通知を受け取ります。
246         * インタフェース ContentHandler 内の characters メソッドをオーバーライドしています。
247         * 各文字データチャンクに対して特殊なアクション (ノードまたはバッファへのデータの追加、
248         * データのファイルへの出力など) を実行することができます。
249         *
250         * @param       buffer  文字データ配列
251         * @param       start   配列内の開始位置
252         * @param       length  配列から読み取られる文字数
253         * @see org.xml.sax.helpers.DefaultHandler#characters(char[] , int , int )
254         */
255        @Override
256        public void characters( final char[] buffer, final int start, final int length ) throws SAXException {
257                if( ! ROW.equals( key ) && ! ROWSET.equals( key ) && length > 0 ) {
258                        body.append( buffer,start,length );
259                        bodyIn = true;
260                }
261        }
262
263        /**
264         * 要素の開始通知を受け取ります。
265         * インタフェース ContentHandler 内の startElement メソッドをオーバーライドしています。
266         * パーサは XML 文書内の各要素の前でこのメソッドを呼び出します。
267         * 各 startElement イベントには対応する endElement イベントがあります。
268         * これは、要素が空である場合も変わりません。対応する endElement イベントの前に、
269         * 要素のコンテンツ全部が順番に報告されます。
270         * ここでは、タグがレベル3以上の場合は、上位タグの内容として取り扱います。よって、
271         * タグに名前空間が定義されている場合、その属性は削除します。
272         *
273         * @og.rev 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。
274         *
275         * @param       namespace       名前空間 URI
276         * @param       localName       前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
277         * @param       qname           前置修飾子を持つ修飾名。修飾名を使用できない場合は空文字列
278         * @param       attributes      要素に付加された属性。属性が存在しない場合、空の Attributesオブジェクト
279         * @see org.xml.sax.helpers.DefaultHandler#startElement(String , String , String , Attributes )
280         */
281        @Override
282        public void startElement(final String namespace, final String localName,
283                                                         final String qname, final Attributes attributes) throws SAXException {
284                if( ROWSET.equals( qname ) ) {
285                        if( listener != null ) {
286                                element = new TagElement( ROWSET,defaultMap );
287                                element.put( ROWSET_TABLE,attributes.getValue( ROWSET_TABLE ) );
288                                listener.actionInit( element );
289                        }
290                        element = null;
291                }
292                else if( ROW.equals( qname ) ) {
293                        element = new TagElement( ROW,defaultMap );
294                        final String num = attributes.getValue( ROW_NUM );
295                        element.setRowNo( num );
296                }
297                else if( EXEC_SQL.equals( qname ) ) {
298                        element = new TagElement( EXEC_SQL );
299                        // 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。
300                        element.put( EXEC_EXISTS,attributes.getValue( EXEC_EXISTS ) );
301                }
302                else if( MERGE_SQL.equals( qname ) ) {
303                        element = new TagElement( MERGE_SQL );
304                }
305
306                if( level <= 2 ) {
307                        key = qname;
308                        body.setLength(0);              // StringBuilder の初期化
309                }
310                else {
311                        // レベル3 以上のタグは上位タグの内容として扱います。
312                        // 6.0.2.5 (2014/10/31) char を append する。
313                        body.append( '<' ).append( qname );
314                        final int len = attributes.getLength();
315                        for( int i=0; i<len; i++ ) {
316                                // 名前空間の宣言は、削除しておきます。あくまでデータとして取り扱う為です。
317                                final String attr = attributes.getQName(i);
318                                if( ! attr.startsWith( "xmlns:" ) ) {
319                                        body.append( ' ' )
320                                                .append( attr ).append( "=\"" )
321                                                .append( attributes.getValue(i) ).append( '"' );
322                                }
323                        }
324                        body.append( '>' );
325                }
326
327                bodyIn = false;         // 入れ子状のタグのBODY部の有無
328                level ++ ;
329        }
330
331        /**
332         * 要素の終了通知を受け取ります。
333         * インタフェース ContentHandler 内の endElement メソッドをオーバーライドしています。
334         * SAX パーサは、XML 文書内の各要素の終わりにこのメソッドを呼び出します。
335         * 各 endElement イベントには対応する startElement イベントがあります。
336         * これは、要素が空である場合も変わりません。
337         *
338         * @og.rev 6.4.3.2 (2016/02/19) findBugs. element は、コンストラクタで初期化されません。
339         * @og.rev 6.9.9.0 (2018/08/20) body の最後の処理の修正。
340         *
341         * @param       namespace       名前空間 URI
342         * @param       localName       前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
343         * @param       qname   前置修飾子を持つ XML 1.0 修飾名。修飾名を使用できない場合は空文字列
344         * @see org.xml.sax.helpers.DefaultHandler#endElement(String , String , String )
345         */
346        @Override
347        public void endElement(final String namespace, final String localName, final String qname) throws SAXException {
348                level -- ;
349                if( ROW.equals( qname ) ) {
350                        if( listener != null ) {
351                                listener.actionRow( element );
352                        }
353                        element = null;
354                }
355                // 6.4.3.2 (2016/02/19) findBugs. element は、コンストラクタで初期化されません。
356                else if( EXEC_SQL.equals( qname ) && element != null ) {
357                        element.setBody( body.toString().trim() );
358                        if( listener != null ) {
359                                listener.actionExecSQL( element );
360                        }
361                        element = null;
362                }
363                // 6.4.3.2 (2016/02/19) findBugs. element は、コンストラクタで初期化されません。
364                else if( MERGE_SQL.equals( qname ) && element != null ) {
365                        element.setBody( body.toString().trim() );
366                        if( listener != null ) {
367                                listener.actionMergeSQL( element );
368                        }
369                        element = null;
370                }
371                else if( level <= 2 && element != null ) {
372                                element.put( key , body.toString().trim() );
373                }
374                else {
375                        if( bodyIn ) {
376                                body.append( "</" ).append( qname ).append( '>' );              // 6.0.2.5 (2014/10/31) char を append する。
377                        }
378                        else {
379                                // 6.9.9.0 (2018/08/20) body の最後の処理の修正。
380//                              body.insert( body.length()-1, " /" );           // タグの最後を " />" とする。
381                                final int len = body.length();
382                                if( len > 0 && body.charAt( len-1 ) == '>' ) {
383                                        body.insert( len-1, " /" );                             // タグの最後を " />" とする。
384                                }
385                        }
386                }
387        }
388}