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}