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.ByteArrayInputStream; 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.UnsupportedEncodingException; 022import java.util.ArrayList; 023import java.util.HashMap; 024import java.util.List; 025import java.util.Locale; 026import java.util.Map; 027 028import javax.xml.parsers.ParserConfigurationException; 029import javax.xml.parsers.SAXParser; 030import javax.xml.parsers.SAXParserFactory; 031 032import org.opengion.fukurou.util.StringUtil; 033import org.xml.sax.Attributes; 034import org.xml.sax.SAXException; 035import org.xml.sax.helpers.DefaultHandler; 036 037/** 038 * XML2TableParser は、XMLを表形式に変換するためのXMLパーサーです。 039 * XMLのパースには、SAXを採用しています。 040 * 041 * このクラスでは、XMLデータを分解し、2次元配列の表データ、及び、指定されたキーに対応する 042 * 属性データのマップを生成します。 043 * 044 * これらの配列を生成するためには、以下のパラメータを指定する必要があります。 045 * 046 * ①2次元配列データ(表データ)の取り出し 047 * 行のキー(タグ名)と、項目のキー一覧(タグ名)を指定することで、表データを取り出します。 048 * 具体的には、行キーのタグセットを"行"とみなし、その中に含まれる項目キーをその列の"値"と 049 * して分解されます。(行キーがN回出現すれば、N行が生成されます。) 050 * もし、行キーの外で、項目キーのタグが出現した場合、その項目キーのタグは無視されます。 051 * 052 * また、colKeysにPARENT_TAG、PARENT_FULL_TAGを指定することで、rowKeyで指定されたタグの 053 * 直近の親タグ、及びフルの親タグ名(親タグの階層を">[タグA]>[タグB]>[タグC]>"で表現)を 054 * 取得することができます。 055 * 056 * 行キー及び項目キーは、{@link #setTableCols(String, String[])}で指定します。 057 * 058 * ②属性データのマップの取り出し 059 * 属性キー(タグ名)を指定することで、そのタグ名に対応した値をマップとして生成します。 060 * 同じタグ名が複数回にわたって出現した場合、値はアペンドされます。 061 * 062 * 属性キーは、{@link #setReturnCols(String[])}で指定します。 063 * 064 * ※それぞれのキー指定は、大文字、小文字を区別した形で指定することができます。 065 * 但し、XMLのタグ名とマッチングする際は、大文字、小文字は区別せずにマッチングされます。 066 * 067 * @version 4.0 068 * @author Hiroki Nakamura 069 * @since JDK5.0, 070 */ 071public class XML2TableParser extends DefaultHandler { 072 073 /*----------------------------------------------------------- 074 * 表形式パース 075 *-----------------------------------------------------------*/ 076 // 表形式パースの変数 077 String rowCpKey = ""; 078 String colCpKeys = ""; 079 Map<String,Integer> colCpIdxs = new HashMap<String, Integer>(); 080 081 // 表形式出力データ 082 List<String[]> rows = new ArrayList<String[]>(); 083 String[] data = null; 084 String[] cols = null; 085 086 /*----------------------------------------------------------- 087 * Map型パース 088 *-----------------------------------------------------------*/ 089 // Map型パースの変数 090 String rtnCpKeys = ""; 091 092 // Map型出力データ 093 Map<String,String> rtnKeyMap = new HashMap<String, String>(); 094 Map<String,String> rtnMap = new HashMap<String, String>(); 095 096 /*----------------------------------------------------------- 097 * パース中のタグの状態定義 098 *-----------------------------------------------------------*/ 099 boolean isInRow = false; // rowKey中に入る間のみtrue 100 String curQName = ""; // パース中のタグ名 ( [タグC] ) 101 String curFQName = ""; // パース中のフルタグ名( [タグA]>[タグB]>[タグC] ) 102 103 // 5.1.6.0 (2010/05/01) rowKeyの親タグが取得できるように対応 104 private static final String PARENT_FULL_TAG_KEY = "PARENT_FULL_TAG"; 105 private static final String PARENT_TAG_KEY = "PARENT_TAG"; 106 107 int pFullTagIdx = -1; 108 int pTagIdx = -1; 109 110 /*----------------------------------------------------------- 111 * href、IDによるデータリンク対応 112 *-----------------------------------------------------------*/ 113 String curId = ""; 114 List<RowColId> idList = new ArrayList<RowColId>(); // row,colとそのIDを記録 115 Map<String,String> idMap = new HashMap<String,String>(); // col__idをキーに値のマップを保持 116 117 final InputStream input; 118 119 /** 120 * XMLの文字列を指定してパーサーを形成します。 121 * 122 * @param st XMLデータ(文字列) 123 */ 124 public XML2TableParser( final String st ) { 125 byte[] bts = null; 126 try { 127 bts = st.getBytes( "UTF-8" ); 128 } 129 catch( UnsupportedEncodingException ex ) { 130 String errMsg = "不正なエンコードが指定されました。エンコード=[UTF-8]" ; 131 throw new RuntimeException( errMsg , ex ); 132 } 133 // XML宣言の前に不要なデータがあれば、取り除きます。 134 int offset = st.indexOf( '<' ); 135 input = new ByteArrayInputStream( bts, offset, bts.length - offset ); 136 } 137 138 /** 139 * ストリームを指定してパーサーを形成します。 140 * 141 * @param is XMLデータ(ストリーム) 142 */ 143 public XML2TableParser( final InputStream is ) { 144 input = is; 145 } 146 147 /** 148 * 2次元配列データ(表データ)の取り出しを行うための行キーと項目キーを指定します。 149 * 150 * @og.rev 5.1.6.0 (2010/05/01) rowKeyの親タグが取得できるように対応 151 * @og.rev 5.1.9.0 (2010/08/01) 可変オブジェクトへの参照の直接セットをコピーに変更 152 * 153 * @param rKey 行キー 154 * @param cKeys 項目キー 155 */ 156 public void setTableCols( final String rKey, final String[] cKeys ) { 157 if( rKey == null || rKey.length() == 0 || cKeys == null || cKeys.length == 0 ) { 158 return; 159 } 160 cols = cKeys.clone(); // 5.1.9.0 (2010/08/01) 161 rowCpKey = rKey.toUpperCase( Locale.JAPAN ); 162 colCpKeys = "," + StringUtil.array2csv( cKeys ).toUpperCase( Locale.JAPAN ) + ","; 163 164 for( int i = 0; i < cols.length; i++ ) { 165 String tmpKey = cols[i].toUpperCase( Locale.JAPAN ); 166 // 5.1.6.0 (2010/05/01) rowKeyの親タグが取得できるように対応 167 if( PARENT_TAG_KEY.equals( tmpKey ) ) { 168 pTagIdx = Integer.valueOf( i ); 169 } 170 else if( PARENT_FULL_TAG_KEY.equals( tmpKey ) ) { 171 pFullTagIdx = Integer.valueOf( i ); 172 } 173 colCpIdxs.put( tmpKey, Integer.valueOf( i ) ); 174 } 175 } 176 177 /** 178 * 属性データのマップの取り出しを行うための属性キーを指定します。 179 * 180 * @param rKeys 属性キー 181 */ 182 public void setReturnCols( final String[] rKeys ) { 183 if( rKeys == null || rKeys.length == 0 ) { 184 return; 185 } 186 187 rtnCpKeys = "," + StringUtil.array2csv( rKeys ).toUpperCase( Locale.JAPAN ) + ","; 188 for( int i = 0; i < rKeys.length; i++ ) { 189 rtnKeyMap.put( rKeys[i].toUpperCase( Locale.JAPAN ), rKeys[i] ); 190 } 191 } 192 193 /** 194 * 表データのヘッダーの項目名を配列で返します。 195 * 196 * @og.rev 5.1.9.0 (2010/08/01) 可変オブジェクトの参照返しをコピー返しに変更 197 * 198 * @return 表データのヘッダーの項目名の配列 199 */ 200 public String[] getCols() { 201 return (cols == null) ? null : cols.clone(); // 5.1.9.0 (2010/08/01) 202 } 203 204 /** 205 * 表データを2次元配列で返します。 206 * 207 * @return 表データの2次元配列 208 */ 209 public String[][] getData() { 210 return rows.toArray( new String[rows.size()][0] ); 211 } 212 213 /** 214 * 属性データをマップ形式で返します。 215 * 216 * @return 属性データのマップ 217 */ 218 public Map<String,String> getRtn() { 219 return rtnMap; 220 } 221 222 /** 223 * XMLのパースを実行します。 224 */ 225 public void parse() { 226 SAXParserFactory spfactory = SAXParserFactory.newInstance(); 227 try { 228 SAXParser parser = spfactory.newSAXParser(); 229 parser.parse( input, this ); 230 } 231 catch( ParserConfigurationException ex ) { 232 throw new RuntimeException( "パーサーの設定に問題があります。", ex ); 233 } 234 catch( SAXException ex ) { 235 throw new RuntimeException( "パースに失敗しました。", ex ); 236 } 237 catch( IOException ex ) { 238 throw new RuntimeException( "データの読み取りに失敗しました。", ex ); 239 } 240 } 241 242 /** 243 * 要素の開始タグ読み込み時に行う処理を定義します。 244 * 245 * @og.rev 5.1.6.0 (2010/05/01) rowKeyの親タグが取得できるように対応 246 * 247 * @param uri 名前空間URI。要素が名前空間 URIを持たない場合、または名前空間処理が行われない場合は空文字列 248 * @param localName 接頭辞を含まないローカル名。名前空間処理が行われない場合は空文字列 249 * @param qName 接頭辞を持つ修飾名。修飾名を使用できない場合は空文字列 250 * @param attributes 要素に付加された属性。属性が存在しない場合、空の Attributesオブジェクト 251 */ 252 @Override 253 public void startElement( final String uri, final String localName, final String qName, final Attributes attributes ) { 254 255 // 処理中のタグ名を設定します。 256 curQName = getCpTagName( qName ); 257 258 if( rowCpKey.equals( curQName ) ) { 259 isInRow = true; 260 data = new String[cols.length]; 261 // 5.1.6.0 (2010/05/01) rowKeyの親タグが取得できるように対応 262 if( pTagIdx >= 0 ) { data[pTagIdx] = getCpParentTagName( curFQName ); } 263 if( pFullTagIdx >= 0 ) { data[pFullTagIdx] = curFQName; } 264 } 265 266 curFQName += ">" + curQName + ">"; 267 268 // href属性で、ID指定(初めが"#")の場合は、その列番号、行番号、IDを記憶しておきます。(後で置き換え) 269 String href = attributes.getValue( "href" ); 270 if( href != null && href.length() > 0 && href.charAt( 0 ) == '#' ) { 271 int colIdx = -1; 272 if( isInRow && ( colIdx = getColIdx( curQName ) ) >= 0 ) { 273 idList.add( new RowColId( rows.size(), colIdx, href.substring( 1 ) ) ); 274 } 275 } 276 277 // id属性を記憶します。 278 curId = attributes.getValue( "id" ); 279 } 280 281 /** 282 * href属性を記憶するための簡易ポイントクラスです。 283 */ 284 private static class RowColId { 285 final int row; 286 final int col; 287 final String id; 288 289 RowColId( final int rw, final int cl, final String st ) { 290 row = rw; col = cl; id = st; 291 } 292 } 293 294 /** 295 * テキストデータ読み込み時に行う処理を定義します。 296 * 297 * @param ch 文字データ配列 298 * @param offset 文字配列内の開始位置 299 * @param length 文字配列から使用される文字数 300 */ 301 @Override 302 public void characters( final char[] ch, final int offset, final int length ) { 303 String val = new String( ch, offset, length ); 304 int colIdx = -1; 305 306 // 表形式データの値をセットします。 307 if( isInRow && ( colIdx = getColIdx( curQName ) ) >= 0 ) { 308 data[colIdx] = ( data[colIdx] == null ? "" : data[colIdx] ) + val; 309 } 310 311 // 属性マップの値を設定します。 312 // 5.1.6.0 (2010/05/01) 313 if( curQName != null && curQName.length() > 0 && rtnCpKeys.indexOf( curQName ) >= 0 ) { 314 String key = rtnKeyMap.get( curQName ); 315 String curVal = rtnMap.get( key ); 316 rtnMap.put( key, ( curVal == null ? "" : curVal ) + val ); 317 } 318 319 // ID属性が付加された要素の値を取り出し、保存します。 320 if( curId != null && curId.length() > 0 && ( colIdx = getColIdx( curQName ) ) >= 0 ) { 321 String curVal = rtnMap.get( colIdx + "__" + curId ); 322 idMap.put( colIdx + "__" + curId, ( curVal == null ? "" : curVal ) + val ); 323 } 324 } 325 326 /** 327 * 要素の終了タグ読み込み時に行う処理を定義します。 328 * 329 * @param uri 名前空間 URI。要素が名前空間 URI を持たない場合、または名前空間処理が行われない場合は空文字列 330 * @param localName 接頭辞を含まないローカル名。名前空間処理が行われない場合は空文字列 331 * @param qName 接頭辞を持つ修飾名。修飾名を使用できない場合は空文字列 332 */ 333 @Override 334 public void endElement( final String uri, final String localName, final String qName ) { 335 curQName = ""; 336 curId = ""; 337 338 // 表形式の行データを書き出します。 339 String tmpCpQName = getCpTagName( qName ); 340 if( rowCpKey.equals( tmpCpQName ) ) { 341 rows.add( data ); 342 isInRow = false; 343 } 344 345 curFQName = curFQName.replace( ">" + tmpCpQName + ">", "" ); 346 } 347 348 /** 349 * ドキュメント終了時に行う処理を定義します。 350 * 351 */ 352 @Override 353 public void endDocument() { 354 // hrefのIDに対応する値を置き換えます。 355 for( RowColId rci : idList ) { 356 rows.get( rci.row )[rci.col] = idMap.get( rci.col + "__" + rci.id ); 357 } 358 } 359 360 /** 361 * PREFIXを取り除き、さらに大文字かしたタグ名を返します。 362 * 363 * @param qName PREFIX付きタグ名 364 * 365 * @return PREFIXを取り除いた大文字のタグ名 366 */ 367 private String getCpTagName( final String qName ) { 368 String tmpCpName = qName.toUpperCase( Locale.JAPAN ); 369 int preIdx = -1; 370 if( ( preIdx = tmpCpName.indexOf( ':' ) ) >= 0 ) { 371 tmpCpName = tmpCpName.substring( preIdx + 1 ); 372 } 373 return tmpCpName; 374 } 375 376 /** 377 * >[タグC]>[タグB]>[タグA]>と言う形式のフルタグ名から[タグA](直近の親タグ名)を 378 * 取り出します。 379 * 380 * @og.rev 5.1.9.0 (2010/08/01) 引数がメソッド内部で使用されていなかったため、修正します。 381 * 382 * @param fQName フルタグ名 383 * 384 * @return 親タグ名 385 */ 386 private String getCpParentTagName( final String fQName ) { 387 String tmpPQName = ""; 388 389 int curNStrIdx = fQName.lastIndexOf( ">", fQName.length() - 2 ) + 1; 390 int curNEndIdx = fQName.length() - 1; 391 if( curNStrIdx >= 0 && curNEndIdx >= 0 && curNStrIdx < curNEndIdx ) { 392 tmpPQName = fQName.substring( curNStrIdx, curNEndIdx ); 393 } 394 return tmpPQName; 395 } 396 397 /** 398 * タグ名に相当するカラムの配列番号を返します。 399 * 400 * @og.rev 5.1.6.0 (2010/05/01) colKeysで指定できない項目が存在しない場合にエラーとなるバグを修正 401 * 402 * @param tagName タグ名 403 * 404 * @return 配列番号(存在しない場合は、-1) 405 */ 406 private int getColIdx( final String tagName ) { 407 int idx = -1; 408 if( tagName != null && tagName.length() > 0 && colCpKeys.indexOf( tagName ) >= 0 ) { 409 // 5.1.6.0 (2010/05/01) 410 Integer key = colCpIdxs.get( tagName ); 411 if( key != null ) { 412 idx = key.intValue(); 413 } 414 } 415 return idx; 416 } 417}