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