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