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 * 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 * これは、<ROW> タグの 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}