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     */
016    package org.opengion.fukurou.xml;
017    
018    import java.io.ByteArrayInputStream;
019    import java.io.IOException;
020    import java.io.InputStream;
021    import java.io.UnsupportedEncodingException;
022    import java.util.ArrayList;
023    import java.util.HashMap;
024    import java.util.List;
025    import java.util.Locale;
026    import java.util.Map;
027    
028    import javax.xml.parsers.ParserConfigurationException;
029    import javax.xml.parsers.SAXParser;
030    import javax.xml.parsers.SAXParserFactory;
031    
032    import org.opengion.fukurou.util.StringUtil;
033    import org.xml.sax.Attributes;
034    import org.xml.sax.SAXException;
035    import org.xml.sax.helpers.DefaultHandler;
036    
037    /**
038     * XML2TableParser は、XMLを表形式に変換するためのXMLパ?サーです?
039     * XMLのパ?スには、SAXを採用して?す?
040     *
041     * こ?クラスでは、XML??タを?解し?2次??列?表??タ、及び、指定されたキーに対応す?
042     * 属???タのマップを生?します?
043     *
044     * これら?配?を生成するためには、以下?パラメータを指定する?があります?
045     *
046     * ?次??列データ(表??タ)の取り出?
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     */
071    public 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             * 表??タ?次??列で返します?
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                     名前空間U??。要?名前空????を持たな??合?また?名前空間??行われな??合?空??
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    }