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