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 * HybsXMLHandler には、TagElementListener をセットすることができます。
100 * これは、ROW 毎に 内部情報を TagElement オブジェクト化し、action( TagElement )
101 * が呼び出されます。この Listener を介して、1レコードずつ処理することが
102 * 可能です。
103 *
104 * @version  4.0
105 * @author   Kazuhiko Hasegawa
106 * @since    JDK5.0,
107 */
108public class HybsXMLHandler extends DefaultHandler {
109
110        /** このハンドラのトップタグ名       {@value}        */
111        public static final     String ROWSET           = "ROWSET";
112        /** このハンドラで取り扱える ROWSETタグの属性    */
113        public static final     String ROWSET_TABLE = "tableName";
114
115        /** このハンドラで取り扱えるタグ名     {@value}        */
116        public static final     String ROW                      = "ROW";
117        /** このハンドラで取り扱える ROWタグの属性       {@value}        */
118        public static final     String ROW_NUM          = "num";
119        /** このハンドラで取り扱えるタグ名     {@value}        */
120        public static final     String EXEC_SQL         = "EXEC_SQL";
121        /** このハンドラで取り扱えるタグ名     {@value}        */
122        public static final     String MERGE_SQL        = "MERGE_SQL";
123
124        /** 6.4.3.1 (2016/02/12) 作成元のMapを、HashMap から ConcurrentHashMap に置き換え。  */
125        private Map<String,String>      defaultMap;
126        private TagElementListener listener     ;
127        private TagElement              element         ;
128        private String                  key                     ;
129        private boolean                 bodyIn          ;
130        private int                             level           ;
131
132        private final StringBuilder     body = new StringBuilder( BUFFER_MIDDLE );                      // 6.4.2.1 (2016/02/05) PMD refactoring.
133
134        /**
135         * デフォルトコンストラクター
136         *
137         * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
138         */
139        public HybsXMLHandler() { super(); }            // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
140
141        /**
142         *      パース処理を行います。
143         *      通常のパース処理の簡易メソッドになっています。
144         *
145         * @param reader パース処理用のReaderオブジェクト
146         */
147        public void parse( final Reader reader ) {
148                try {
149                        final SAXParserFactory fact = SAXParserFactory.newInstance();
150                        final SAXParser parser = fact.newSAXParser();
151
152                        final InputSource input = new InputSource( reader );
153
154                        try {
155                                parser.parse( input,this );
156                        }
157                        catch( final SAXException ex ) {
158                                if( ! "END".equals( ex.getMessage() ) ) {
159                                        // 6.4.2.1 (2016/02/05) PMD refactoring.
160                                        final String errMsg = "XMLパースエラー key=" + key + CR
161                                                                + "element=" + element + CR
162                                                                + ex.getMessage() + CR
163                                                                + body.toString();
164                                        throw new OgRuntimeException( errMsg,ex );
165                                }
166                        }
167                }
168                catch( final ParserConfigurationException ex1 ) {
169                        final String errMsg = "SAXParser のコンフィグレーションが構築できません。"
170                                                + "key=" + key + CR + ex1.getMessage();
171                        throw new OgRuntimeException( errMsg,ex1 );
172                }
173                catch( final SAXException ex2 ) {
174                        final String errMsg = "SAXParser が構築できません。"
175                                                + "key=" + key + CR + ex2.getMessage();
176                        throw new OgRuntimeException( errMsg,ex2 );
177                }
178                catch( final IOException ex3 ) {
179                        final String errMsg = "InputSource の読み取り時にエラーが発生しました。"
180                                                + "key=" + key + CR + ex3.getMessage();
181                        throw new OgRuntimeException( errMsg,ex3 );
182                }
183        }
184
185        /**
186         * 内部に TagElementListener を登録します。
187         * これは、&lt;ROW&gt; タグの endElement 処理毎に呼び出されます。
188         * つまり、行データを取得都度、TagElement オブジェクトを作成し、
189         * この TagElementListener の action( TagElement ) メソッドを呼び出します。
190         * 何もセットしない、または、null がセットされた場合は、何もしません。
191         *
192         * @param listener TagElementListenerオブジェクト
193         */
194        public void setTagElementListener( final TagElementListener listener ) {
195                this.listener = listener;
196        }
197
198        /**
199         * TagElement オブジェクトを作成する時の 初期カラム/値を設定します。
200         * TagElements オブジェクトは、XMLファイルより作成する為、項目(カラム)も
201         * XMLファイルのROW属性に持っている項目と値で作成されます。
202         * このカラム名を、外部から初期設定することが可能です。
203         * その場合、ここで登録したカラム順(Mapに、LinkedHashMap を使用した場合)
204         * が保持されます。また、ROW属性に存在しないカラムがあれば、値とともに
205         * 初期値として設定しておくことが可能です。
206         * なお、ここでのMapは、直接設定していますので、ご注意ください。
207         *
208         * @param       map     初期カラムマップ
209         */
210        public void setDefaultMap( final Map<String,String> map ) {
211                defaultMap = map;
212        }
213
214        /**
215         * 要素内の文字データの通知を受け取ります。
216         * インタフェース ContentHandler 内の characters メソッドをオーバーライドしています。
217         * 各文字データチャンクに対して特殊なアクション (ノードまたはバッファへのデータの追加、
218         * データのファイルへの出力など) を実行することができます。
219         *
220         * @param       buffer  文字データ配列
221         * @param       start   配列内の開始位置
222         * @param       length  配列から読み取られる文字数
223         * @see org.xml.sax.helpers.DefaultHandler#characters(char[] , int , int )
224         */
225        @Override
226        public void characters( final char[] buffer, final int start, final int length ) throws SAXException {
227                if( ! ROW.equals( key ) && ! ROWSET.equals( key ) && length > 0 ) {
228                        body.append( buffer,start,length );
229                        bodyIn = true;
230                }
231        }
232
233        /**
234         * 要素の開始通知を受け取ります。
235         * インタフェース ContentHandler 内の startElement メソッドをオーバーライドしています。
236         * パーサは XML 文書内の各要素の前でこのメソッドを呼び出します。
237         * 各 startElement イベントには対応する endElement イベントがあります。
238         * これは、要素が空である場合も変わりません。対応する endElement イベントの前に、
239         * 要素のコンテンツ全部が順番に報告されます。
240         * ここでは、タグがレベル3以上の場合は、上位タグの内容として取り扱います。よって、
241         * タグに名前空間が定義されている場合、その属性は削除します。
242         *
243         * @param       namespace       名前空間 URI
244         * @param       localName       前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
245         * @param       qname           前置修飾子を持つ修飾名。修飾名を使用できない場合は空文字列
246         * @param       attributes      要素に付加された属性。属性が存在しない場合、空の Attributesオブジェクト
247         * @see org.xml.sax.helpers.DefaultHandler#startElement(String , String , String , Attributes )
248         */
249        @Override
250        public void startElement(final String namespace, final String localName,
251                                                         final String qname, final Attributes attributes) throws SAXException {
252                if( ROWSET.equals( qname ) ) {
253                        if( listener != null ) {
254                                element = new TagElement( ROWSET,defaultMap );
255                                element.put( ROWSET_TABLE,attributes.getValue( ROWSET_TABLE ) );
256                                listener.actionInit( element );
257                        }
258                        element = null;
259                }
260                else if( ROW.equals( qname ) ) {
261                        element = new TagElement( ROW,defaultMap );
262                        final String num = attributes.getValue( ROW_NUM );
263                        element.setRowNo( num );
264                }
265                else if( EXEC_SQL.equals( qname ) ) {
266                        element = new TagElement( EXEC_SQL );
267                }
268                else if( MERGE_SQL.equals( qname ) ) {
269                        element = new TagElement( MERGE_SQL );
270                }
271
272                if( level <= 2 ) {
273                        key = qname;
274                        body.setLength(0);              // StringBuilder の初期化
275                }
276                else {
277                        // レベル3 以上のタグは上位タグの内容として扱います。
278                        // 6.0.2.5 (2014/10/31) char を append する。
279                        body.append( '<' ).append( qname );
280                        final int len = attributes.getLength();
281                        for( int i=0; i<len; i++ ) {
282                                // 名前空間の宣言は、削除しておきます。あくまでデータとして取り扱う為です。
283                                final String attr = attributes.getQName(i);
284                                if( ! attr.startsWith( "xmlns:" ) ) {
285                                        body.append( ' ' );
286                                        body.append( attr ).append( "=\"" );
287                                        body.append( attributes.getValue(i) ).append( '"' );
288                                }
289                        }
290                        body.append( '>' );
291                }
292
293                bodyIn = false;         // 入れ子状のタグのBODY部の有無
294                level ++ ;
295        }
296
297        /**
298         * 要素の終了通知を受け取ります。
299         * インタフェース ContentHandler 内の endElement メソッドをオーバーライドしています。
300         * SAX パーサは、XML 文書内の各要素の終わりにこのメソッドを呼び出します。
301         * 各 endElement イベントには対応する startElement イベントがあります。
302         * これは、要素が空である場合も変わりません。
303         *
304         * @og.rev 6.4.3.2 (2016/02/19) findBugs. element は、コンストラクタで初期化されません。
305         *
306         * @param       namespace       名前空間 URI
307         * @param       localName       前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
308         * @param       qname   前置修飾子を持つ XML 1.0 修飾名。修飾名を使用できない場合は空文字列
309         * @see org.xml.sax.helpers.DefaultHandler#endElement(String , String , String )
310         */
311        @Override
312        public void endElement(final String namespace, final String localName, final String qname) throws SAXException {
313                level -- ;
314                if( ROW.equals( qname ) ) {
315                        if( listener != null ) {
316                                listener.actionRow( element );
317                        }
318                        element = null;
319                }
320                // 6.4.3.2 (2016/02/19) findBugs. element は、コンストラクタで初期化されません。
321                else if( EXEC_SQL.equals( qname ) && element != null ) {
322                        element.setBody( body.toString().trim() );
323                        if( listener != null ) {
324                                listener.actionExecSQL( element );
325                        }
326                        element = null;
327                }
328                // 6.4.3.2 (2016/02/19) findBugs. element は、コンストラクタで初期化されません。
329                else if( MERGE_SQL.equals( qname ) && element != null ) {
330                        element.setBody( body.toString().trim() );
331                        if( listener != null ) {
332                                listener.actionMergeSQL( element );
333                        }
334                        element = null;
335                }
336                else if( level <= 2 && element != null ) {
337                                element.put( key , body.toString().trim() );
338                }
339                else {
340                        if( bodyIn ) {
341                                body.append( "</" ).append( qname ).append( '>' );              // 6.0.2.5 (2014/10/31) char を append する。
342                        }
343                        else {
344                                body.insert( body.length()-1, " /" );           // タグの最後を " />" とする。
345                        }
346                }
347        }
348}