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 * <ROWSET tableName="XX" > 063 * <EXEC_SQL> 最初に記載して、初期処理(データクリア等)を実行させる。 064 * delete from GEXX where YYYYY 065 * </EXEC_SQL> 066 * <MERGE_SQL> このSQL文で UPDATEして、結果が0件ならINSERTを行います。 067 * update GEXX set AA=[AA] , BB=[BB] where CC=[CC] 068 * </MERGE_SQL> 069 * <ROW num="1"> 070 * <カラム1>値1</カラム1> 071 * ・・・ 072 * <カラムn>値n</カラムn> 073 * </ROW> 074 * ・・・ 075 * <ROW num="n"> 076 * ・・・ 077 * </ROW> 078 * <EXEC_SQL> 最後に記載して、項目の設定(整合性登録)を行う。 079 * update GEXX set AA='XX' , BB='YY' where CC='ZZ' 080 * </EXEC_SQL> 081 * <ROWSET> 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 * これは、<ROW> タグの 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}