    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 *   直近の親タグ、及びフルの親タグ名(親タグの階層を"&gt;[タグA]&gt;[タグB]&gt;[タグC]&gt;"で表現)を
    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//                      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                     名前空間ＵＲＩ。要素が名前空間 ＵＲＩを持たない場合、または名前空間処理が行われない場合は空文字列
    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}