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