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