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 * A属性データのマップの取り出し
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//                      throw new RuntimeException( "不正なエンコードが指定されました。" );
131                        String errMsg = "不正なエンコードが指定されました。エンコード=[UTF-8]"  ;
132                        throw new RuntimeException( errMsg , ex );
133                }
134                // XML宣言の前に不要なデータがあれば、取り除きます。
135                int offset = st.indexOf( '<' );
136                input = new ByteArrayInputStream( bts, offset, bts.length - offset  );
137        }
138
139        /**
140         * ストリームを指定してパーサーを形成します。
141         *
142         * @param is XMLデータ(ストリーム)
143         */
144        public XML2TableParser( final InputStream is ) {
145                input = is;
146        }
147
148        /**
149         * 2次元配列データ(表データ)の取り出しを行うための行キーと項目キーを指定します。
150         *
151         * @og.rev 5.1.6.0 (2010/05/01) rowKeyの親タグが取得できるように対応
152         * @og.rev 5.1.9.0 (2010/08/01) 可変オブジェクトへの参照の直接セットをコピーに変更
153         *
154         * @param rKey 行キー
155         * @param cKeys 項目キー
156         */
157        public void setTableCols( final String rKey, final String[] cKeys ) {
158                if( rKey == null || rKey.length() == 0 || cKeys == null || cKeys.length == 0 ) {
159                        return;
160                }
161                cols = cKeys.clone();           // 5.1.9.0 (2010/08/01)
162                rowCpKey = rKey.toUpperCase( Locale.JAPAN );
163                colCpKeys = "," + StringUtil.array2csv( cKeys ).toUpperCase( Locale.JAPAN ) + ",";
164
165                for( int i = 0; i < cols.length; i++ ) {
166                        String tmpKey = cols[i].toUpperCase( Locale.JAPAN );
167                        // 5.1.6.0 (2010/05/01) rowKeyの親タグが取得できるように対応
168                        if( PARENT_TAG_KEY.equals( tmpKey ) ) {
169                                pTagIdx = Integer.valueOf( i );
170                        }
171                        else if( PARENT_FULL_TAG_KEY.equals( tmpKey ) ) {
172                                pFullTagIdx = Integer.valueOf( i );
173                        }
174                        colCpIdxs.put( tmpKey, Integer.valueOf( i ) );
175                }
176        }
177
178        /**
179         * 属性データのマップの取り出しを行うための属性キーを指定します。
180         *
181         * @param rKeys 属性キー
182         */
183        public void setReturnCols( final String[] rKeys  ) {
184                if( rKeys == null || rKeys.length == 0 ) {
185                        return;
186                }
187
188                rtnCpKeys = "," + StringUtil.array2csv( rKeys ).toUpperCase( Locale.JAPAN ) + ",";
189                for( int i = 0; i < rKeys.length; i++ ) {
190                        rtnKeyMap.put( rKeys[i].toUpperCase( Locale.JAPAN ), rKeys[i] );
191                }
192        }
193
194        /**
195         * 表データのヘッダーの項目名を配列で返します。
196         *
197         * @og.rev 5.1.9.0 (2010/08/01) 可変オブジェクトの参照返しをコピー返しに変更
198         *
199         * @return 表データのヘッダーの項目名の配列
200         */
201        public String[] getCols() {
202//              return cols;
203                return (cols == null) ? null : cols.clone();    // 5.1.9.0 (2010/08/01)
204        }
205
206        /**
207         * 表データを2次元配列で返します。
208         *
209         * @return 表データの2次元配列
210         */
211        public String[][] getData() {
212//              for( String[] dt : rows ) {
213//                      System.out.println( "-----------------------" );
214//                      for( int i = 0; i < dt.length; i++ ) {
215//                              System.out.println( "col:" + cols[i] + "=" + dt[i] );
216//                      }
217//              }
218//              return rows.toArray( new String[0][0] );
219                return rows.toArray( new String[rows.size()][0] );
220        }
221
222        /**
223         * 属性データをマップ形式で返します。
224         *
225         * @return 属性データのマップ
226         */
227        public Map<String,String> getRtn() {
228//              for( Map.Entry<String, String> entry : rtnMap.entrySet() ) {
229//                      System.out.println( "param:" + entry.getKey() + "=" + entry.getValue() );
230//              }
231                return rtnMap;
232        }
233
234        /**
235         * XMLのパースを実行します。
236         */
237        public void parse() {
238                SAXParserFactory spfactory = SAXParserFactory.newInstance();
239                try {
240                        SAXParser parser = spfactory.newSAXParser();
241                        parser.parse( input, this );
242                }
243                catch( ParserConfigurationException ex ) {
244                        throw new RuntimeException( "パーサーの設定に問題があります。", ex );
245                }
246                catch( SAXException ex ) {
247                        throw new RuntimeException( "パースに失敗しました。", ex );
248                }
249                catch( IOException ex ) {
250                        throw new RuntimeException( "データの読み取りに失敗しました。", ex );
251                }
252        }
253
254        /**
255         * ドキュメント開始時に行う処理を定義します。
256         * (ここでは何もしません)
257         */
258//      public void startDocument() {
259//      }
260
261        /**
262         * 要素の開始タグ読み込み時に行う処理を定義します。
263         *
264         * @og.rev 5.1.6.0 (2010/05/01) rowKeyの親タグが取得できるように対応
265         *
266         * @param       uri                     名前空間URI。要素が名前空間 URIを持たない場合、または名前空間処理が行われない場合は空文字列
267         * @param       localName       接頭辞を含まないローカル名。名前空間処理が行われない場合は空文字列
268         * @param       qName           接頭辞を持つ修飾名。修飾名を使用できない場合は空文字列
269         * @param       attributes      要素に付加された属性。属性が存在しない場合、空の Attributesオブジェクト
270         */
271        @Override
272        public void startElement( final String uri, final String localName, final String qName, final Attributes attributes ) {
273
274                // 処理中のタグ名を設定します。
275                curQName = getCpTagName( qName );
276
277                if( rowCpKey.equals( curQName ) ) {
278                        isInRow = true;
279                        data = new String[cols.length];
280                        // 5.1.6.0 (2010/05/01) rowKeyの親タグが取得できるように対応
281                        if( pTagIdx >= 0 ) { data[pTagIdx] = getCpParentTagName( curFQName ); }
282                        if( pFullTagIdx >= 0 ) { data[pFullTagIdx] = curFQName; }
283                }
284
285                curFQName += ">" + curQName + ">";
286
287                // href属性で、ID指定(初めが"#")の場合は、その列番号、行番号、IDを記憶しておきます。(後で置き換え)
288                String href = attributes.getValue( "href" );
289                if( href != null && href.length() > 0 && href.charAt( 0 ) == '#' ) {
290                        int colIdx = -1;
291                        if( isInRow && ( colIdx = getColIdx( curQName ) ) >= 0 ) {
292                                idList.add( new RowColId( rows.size(), colIdx, href.substring( 1 ) ) );
293                        }
294                }
295
296                // id属性を記憶します。
297                curId = attributes.getValue( "id" );
298        }
299
300        /**
301         * href属性を記憶するための簡易ポイントクラスです。
302         */
303        private static class RowColId {
304                final int row;
305                final int col;
306                final String id;
307
308                RowColId( final int rw, final int cl, final String st ) {
309                        row = rw; col = cl; id = st;
310                }
311        }
312
313        /**
314         * テキストデータ読み込み時に行う処理を定義します。
315         *
316         * @param       ch              文字データ配列
317         * @param       offset  文字配列内の開始位置
318         * @param       length  文字配列から使用される文字数
319         */
320        @Override
321        public void characters( final char[] ch, final int offset, final int length ) {
322                String val = new String( ch, offset, length );
323                int colIdx = -1;
324
325                // 表形式データの値をセットします。
326                if( isInRow && ( colIdx = getColIdx( curQName ) ) >= 0 ) {
327                        data[colIdx] = ( data[colIdx] == null ? "" : data[colIdx] ) + val;
328                }
329
330                // 属性マップの値を設定します。
331                // 5.1.6.0 (2010/05/01)
332                if( curQName != null && curQName.length() > 0 && rtnCpKeys.indexOf( curQName ) >= 0 ) {
333                        String key = rtnKeyMap.get( curQName );
334                        String curVal = rtnMap.get( key );
335                        rtnMap.put( key, ( curVal == null ? "" : curVal ) + val );
336                }
337
338                // ID属性が付加された要素の値を取り出し、保存します。
339                if( curId != null && curId.length() > 0  && ( colIdx = getColIdx( curQName ) ) >= 0 ) {
340                        String curVal = rtnMap.get( colIdx + "__" + curId );
341                        idMap.put( colIdx + "__" + curId, ( curVal == null ? "" : curVal ) + val );
342                }
343        }
344
345        /**
346         * 要素の終了タグ読み込み時に行う処理を定義します。
347         *
348         * @param       uri                     名前空間 URI。要素が名前空間 URI を持たない場合、または名前空間処理が行われない場合は空文字列
349         * @param       localName       接頭辞を含まないローカル名。名前空間処理が行われない場合は空文字列
350         * @param       qName           接頭辞を持つ修飾名。修飾名を使用できない場合は空文字列
351         */
352        @Override
353        public void endElement( final String uri, final String localName, final String qName ) {
354                curQName = "";
355                curId = "";
356
357                // 表形式の行データを書き出します。
358                String tmpCpQName = getCpTagName( qName );
359                if( rowCpKey.equals( tmpCpQName ) ) {
360                        rows.add( data );
361                        isInRow = false;
362                }
363
364                curFQName = curFQName.replace( ">" + tmpCpQName + ">", "" );
365        }
366
367        /**
368         * ドキュメント終了時に行う処理を定義します。
369         *
370         */
371        @Override
372        public void endDocument() {
373                // hrefのIDに対応する値を置き換えます。
374                for( RowColId rci : idList ) {
375                        rows.get( rci.row )[rci.col] = idMap.get( rci.col + "__" + rci.id );
376                }
377        }
378
379        /**
380         * PREFIXを取り除き、さらに大文字かしたタグ名を返します。
381         *
382         * @param qName PREFIX付きタグ名
383         *
384         * @return PREFIXを取り除いた大文字のタグ名
385         */
386        private String getCpTagName( final String qName ) {
387                String tmpCpName = qName.toUpperCase( Locale.JAPAN );
388                int preIdx = -1;
389//              if( ( preIdx = tmpCpName.indexOf( ":" ) ) >= 0 ) {
390                if( ( preIdx = tmpCpName.indexOf( ':' ) ) >= 0 ) {
391                        tmpCpName = tmpCpName.substring( preIdx + 1 );
392                }
393                return tmpCpName;
394        }
395
396        /**
397         * >[タグC]>[タグB]>[タグA]>と言う形式のフルタグ名から[タグA](直近の親タグ名)を
398         * 取り出します。
399         *
400         * @og.rev 5.1.9.0 (2010/08/01) 引数がメソッド内部で使用されていなかったため、修正します。
401         *
402         * @param fQName フルタグ名
403         *
404         * @return 親タグ名
405         */
406        private String getCpParentTagName( final String fQName ) {
407                String tmpPQName = "";
408//              int curNStrIdx = curFQName.lastIndexOf( ">", curFQName.length() - 2 ) + 1;
409//              int curNEndIdx = curFQName.length() - 1;
410//              if( curNStrIdx >= 0 && curNEndIdx >= 0 && curNStrIdx < curNEndIdx ) {
411//                      tmpPQName = curFQName.substring( curNStrIdx, curNEndIdx );
412//              }
413                int curNStrIdx = fQName.lastIndexOf( ">", fQName.length() - 2 ) + 1;
414                int curNEndIdx = fQName.length() - 1;
415                if( curNStrIdx >= 0 && curNEndIdx >= 0 && curNStrIdx < curNEndIdx ) {
416                        tmpPQName = fQName.substring( curNStrIdx, curNEndIdx );
417                }
418                return tmpPQName;
419        }
420
421        /**
422         * タグ名に相当するカラムの配列番号を返します。
423         *
424         * @og.rev 5.1.6.0 (2010/05/01) colKeysで指定できない項目が存在しない場合にエラーとなるバグを修正
425         *
426         * @param       tagName タグ名
427         *
428         * @return 配列番号(存在しない場合は、-1)
429         */
430        private int getColIdx( final String tagName ) {
431                int idx = -1;
432                if( tagName != null && tagName.length() > 0 && colCpKeys.indexOf( tagName ) >= 0 ) {
433                        // 5.1.6.0 (2010/05/01)
434                        Integer key = colCpIdxs.get( tagName );
435                        if( key != null ) {
436                                idx = key.intValue();
437                        }
438//                      idx = colCpIdxs.get( tagName ).intValue();
439                }
440                return idx;
441        }
442}