    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 org.xml.sax.InputSource;
    020import org.xml.sax.SAXException;
    021import org.xml.sax.Attributes;
    022import org.xml.sax.helpers.DefaultHandler;
    023
    024import javax.xml.parsers.SAXParserFactory;
    025import javax.xml.parsers.SAXParser;
    026import javax.xml.parsers.ParserConfigurationException;
    027
    028import java.io.Reader;
    029import java.io.IOException;
    030import java.util.Map;
    031
    032import static org.opengion.fukurou.system.HybsConst.CR;                         // 6.1.0.0 (2014/12/26) refactoring
    033import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;      // 6.1.0.0 (2014/12/26) refactoring
    034
    035/**
    036 * このｸﾗｽは、拡張ｵﾗｸﾙ XDK形式のXMLﾌｧｲﾙを処理するﾊﾝﾄﾞﾗです。
    037 * ｵﾗｸﾙXDK形式のXMLとは、下記のような ROWSET をﾄｯﾌﾟとする ROW の
    038 * 集まりで１ﾚｺｰﾄﾞを表し、各ROWには、ｶﾗﾑ名をｷｰとするXMLになっています。
    039 *
    040 *   &lt;ROWSET&gt;
    041 *       &lt;ROW num="1"&gt;
    042 *           &lt;ｶﾗﾑ1&gt;値1&lt;/ｶﾗﾑ1&gt;
    043 *             ･･･
    044 *           &lt;ｶﾗﾑn&gt;値n&lt;/ｶﾗﾑn&gt;
    045 *       &lt;/ROW&gt;
    046 *        ･･･
    047 *       &lt;ROW num="n"&gt;
    048 *          ･･･
    049 *       &lt;/ROW&gt;
    050 *   &lt;ROWSET&gt;
    051 *
    052 * この形式であれば、XDK(Oracle XML Developer's Kit)を利用すれば、非常に簡単に
    053 * ﾃﾞｰﾀﾍﾞｰｽとXMLﾌｧｲﾙとの交換が可能です。
    054 * <a href="https://docs.oracle.com/cd/F19136_01/adxdk/introduction-to-XDK.html" target="_blank" >
    055 * XDK(Oracle XML Developer's Kit)</a>
    056 *
    057 * 拡張XDK形式とは、ROW 以外に、SQL処理用ﾀｸﾞ(EXEC_SQL)を持つ XML ﾌｧｲﾙです。
    058 * また、登録するﾃｰﾌﾞﾙ(table)を ROWSETﾀｸﾞの属性情報として付与することができます。
    059 * (大文字小文字に注意)
    060 * これは、ｵﾗｸﾙXDKで処理する場合、無視されますので、同様に扱うことが出来ます。
    061 * この、EXEC_SQL は、それそれの XMLﾃﾞｰﾀをﾃﾞｰﾀﾍﾞｰｽに登録する際に、
    062 * SQL処理を自動的に流す為の、SQL文を記載します。
    063 * この処理は、ｲﾍﾞﾝﾄ毎に実行される為、その配置順は重要です。
    064 * このﾀｸﾞは、複数記述することも出来ますが、BODY部には、１つのSQL文のみ記述します。
    065 *
    066 *   &lt;ROWSET tableName="XX" &gt;
    067 *       &lt;EXEC_SQL&gt;                    最初に記載して、初期処理(ﾃﾞｰﾀｸﾘｱ等)を実行させる。
    068 *           delete from GEXX where YYYYY
    069 *       &lt;/EXEC_SQL&gt;
    070 *       &lt;MERGE_SQL&gt;                   このSQL文で UPDATEして、結果が０件ならINSERTを行います。
    071 *           update GEXX set AA=[AA] , BB=[BB] where CC=[CC]
    072 *       &lt;/MERGE_SQL&gt;
    073 *       &lt;ROW num="1"&gt;
    074 *           &lt;ｶﾗﾑ1&gt;値1&lt;/ｶﾗﾑ1&gt;
    075 *             ･･･
    076 *           &lt;ｶﾗﾑn&gt;値n&lt;/ｶﾗﾑn&gt;
    077 *       &lt;/ROW&gt;
    078 *        ･･･
    079 *       &lt;ROW num="n"&gt;
    080 *          ･･･
    081 *       &lt;/ROW&gt;
    082 *       &lt;EXEC_SQL&gt;                    最後に記載して、項目の設定(整合性登録)を行う。
    083 *           update GEXX set AA='XX' , BB='YY' where CC='ZZ'
    084 *       &lt;/EXEC_SQL&gt;
    085 *   &lt;ROWSET&gt;
    086 *
    087 * DefaultHandler ｸﾗｽを拡張している為、通常の処理と同様に、使用できます。
    088 *
    089 *      InputSource input = new InputSource( reader );
    090 *      HybsXMLHandler hndler = new HybsXMLHandler();
    091 *
    092 *      SAXParserFactory f = SAXParserFactory.newInstance();
    093 *      SAXParser parser = f.newSAXParser();
    094 *      parser.parse( input,hndler );
    095 *
    096 * また、上記の処理そのものを簡略化したﾒｿｯﾄﾞ:parse( Reader ) を持っているため、
    097 * 通常そのﾒｿｯﾄﾞを使用します。
    098 *
    099 * 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。
    100 *   EXEC_SQL は、『;』で複数SQLを実行できます。
    101 *   これに、属性 exists="0" があれば、最初のSQLを実行し、結果が 0 の場合のみ、
    102 *   以下のSQLを実行します。
    103 *
    104 *       &lt;EXEC_SQL exists="0"&gt;
    105 *           select count(*) from user_tables where table_name=upper('BONUS');
    106 *           CREATE TABLE BONUS ( ････ )
    107 *       &lt;/EXEC_SQL&gt;
    108 *
    109 *   exists="0" があるため、1行目を実行後、結果が一致した場合(=0)は、CREATE TABLE文を実行します。
    110 *   exists="1" を指定した場合は、(!=0)と同じで、0以外という意味になります。
    111 *   値の判定は、検索処理後の1行目1列目の値で判定します。
    112 *
    113 * HybsXMLHandler には、TagElementListener をｾｯﾄすることができます。
    114 * これは、ROW 毎に 内部情報を TagElement ｵﾌﾞｼﾞｪｸﾄ化し、action( TagElement )
    115 * が呼び出されます。この Listener を介して、１ﾚｺｰﾄﾞずつ処理することが
    116 * 可能です。
    117 *
    118 * @version     4.0
    119 * @author      Kazuhiko Hasegawa
    120 * @since       JDK5.0,
    121 */
    122public class HybsXMLHandler extends DefaultHandler {
    123
    124        /** このﾊﾝﾄﾞﾗのﾄｯﾌﾟﾀｸﾞ名 {@value} */
    125        public static final     String ROWSET           = "ROWSET";
    126        /** このﾊﾝﾄﾞﾗで取り扱える ROWSETﾀｸﾞの属性 */
    127        public static final     String ROWSET_TABLE = "tableName";
    128
    129        /** このﾊﾝﾄﾞﾗで取り扱えるﾀｸﾞ名 {@value} */
    130        public static final     String ROW                      = "ROW";
    131        /** このﾊﾝﾄﾞﾗで取り扱える ROWﾀｸﾞの属性 {@value} */
    132        public static final     String ROW_NUM          = "num";
    133
    134        /** このﾊﾝﾄﾞﾗで取り扱えるﾀｸﾞ名 {@value} */
    135        public static final     String EXEC_SQL         = "EXEC_SQL";
    136        /** このﾊﾝﾄﾞﾗで取り扱える EXEC_SQLﾀｸﾞの属性 {@value} */
    137        public static final     String EXEC_EXISTS      = "exists";             // 8.1.0.3 (2022/01/21)
    138
    139        /** このﾊﾝﾄﾞﾗで取り扱えるﾀｸﾞ名 {@value} */
    140        public static final     String MERGE_SQL        = "MERGE_SQL";
    141
    142        /** 6.4.3.1 (2016/02/12) 作成元のMapを、HashMap から ConcurrentHashMap に置き換え。 */
    143        private Map<String,String>      defaultMap;
    144        private TagElementListener listener     ;
    145        private TagElement              element         ;
    146        private String                  key                     ;
    147        private boolean                 bodyIn          ;
    148        private int                             level           ;
    149
    150        private final StringBuilder     body = new StringBuilder( BUFFER_MIDDLE );                      // 6.4.2.1 (2016/02/05) PMD refactoring.
    151
    152        /**
    153         * ﾃﾞﾌｫﾙﾄｺﾝｽﾄﾗｸﾀｰ
    154         *
    155         * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
    156         */
    157        public HybsXMLHandler() { super(); }            // これも、自動的に呼ばれるが、空のﾒｿｯﾄﾞを作成すると警告されるので、明示的にしておきます。
    158
    159        /**
    160         * ﾊﾟｰｽ処理を行います。
    161         * 通常のﾊﾟｰｽ処理の簡易ﾒｿｯﾄﾞになっています。
    162         *
    163         * @param       reader  ﾊﾟｰｽ処理用のReaderｵﾌﾞｼﾞｪｸﾄ
    164         */
    165        public void parse( final Reader reader ) {
    166                try {
    167                        final SAXParserFactory fact = SAXParserFactory.newInstance();
    168                        final SAXParser parser = fact.newSAXParser();
    169
    170                        final InputSource input = new InputSource( reader );
    171
    172                        try {
    173                                parser.parse( input,this );
    174                        }
    175                        catch( final SAXException ex ) {
    176                                if( ! "END".equals( ex.getMessage() ) ) {
    177                                        // 6.4.2.1 (2016/02/05) PMD refactoring.
    178                                        final String errMsg = "XMLパースエラー key=" + key + CR
    179                                                                + "element=" + element + CR
    180                                                                + ex.getMessage() + CR
    181                                                                + body.toString();
    182                                        throw new OgRuntimeException( errMsg,ex );
    183                                }
    184                        }
    185                }
    186                catch( final ParserConfigurationException ex1 ) {
    187                        final String errMsg = "SAXParser のコンフィグレーションが構築できません。"
    188                                                + "key=" + key + CR + ex1.getMessage();
    189                        throw new OgRuntimeException( errMsg,ex1 );
    190                }
    191                catch( final SAXException ex2 ) {
    192                        final String errMsg = "SAXParser が構築できません。"
    193                                                + "key=" + key + CR + ex2.getMessage();
    194                        throw new OgRuntimeException( errMsg,ex2 );
    195                }
    196                catch( final IOException ex3 ) {
    197                        final String errMsg = "InputSource の読み取り時にエラーが発生しました。"
    198                                                + "key=" + key + CR + ex3.getMessage();
    199                        throw new OgRuntimeException( errMsg,ex3 );
    200                }
    201        }
    202
    203        /**
    204         * 内部に TagElementListener を登録します。
    205         * これは、&lt;ROW&gt; ﾀｸﾞの endElement 処理毎に呼び出されます。
    206         * つまり、行ﾃﾞｰﾀを取得都度、TagElement ｵﾌﾞｼﾞｪｸﾄを作成し、
    207         * この TagElementListener の action( TagElement ) ﾒｿｯﾄﾞを呼び出します。
    208         * 何もｾｯﾄしない、または、null がｾｯﾄされた場合は、何もしません。
    209         *
    210         * @param       listener        TagElementListenerｵﾌﾞｼﾞｪｸﾄ
    211         */
    212        public void setTagElementListener( final TagElementListener listener ) {
    213                this.listener = listener;
    214        }
    215
    216        /**
    217         * TagElement ｵﾌﾞｼﾞｪｸﾄを作成する時の 初期ｶﾗﾑ/値を設定します。
    218         * TagElements ｵﾌﾞｼﾞｪｸﾄは、XMLﾌｧｲﾙより作成する為、項目(ｶﾗﾑ)も
    219         * XMLﾌｧｲﾙのROW属性に持っている項目と値で作成されます。
    220         * このｶﾗﾑ名を、外部から初期設定することが可能です。
    221         * その場合、ここで登録したｶﾗﾑ順(Mapに、LinkedHashMap を使用した場合)
    222         * が保持されます。また、ROW属性に存在しないｶﾗﾑがあれば、値とともに
    223         * 初期値として設定しておくことが可能です。
    224         * なお、ここでのMapは、直接設定していますので、ご注意ください。
    225         *
    226         * @param       map     初期ｶﾗﾑﾏｯﾌﾟ
    227         */
    228        public void setDefaultMap( final Map<String,String> map ) {
    229                defaultMap = map;
    230        }
    231
    232        /**
    233         * 要素内の文字ﾃﾞｰﾀの通知を受け取ります。
    234         * ｲﾝﾀﾌｪｰｽ ContentHandler 内の characters ﾒｿｯﾄﾞをｵｰﾊﾞｰﾗｲﾄﾞしています。
    235         * 各文字ﾃﾞｰﾀﾁｬﾝｸに対して特殊なｱｸｼｮﾝ (ﾉｰﾄﾞまたはﾊﾞｯﾌｧへのﾃﾞｰﾀの追加、
    236         * ﾃﾞｰﾀのﾌｧｲﾙへの出力など) を実行することができます。
    237         *
    238         * @param       buffer  文字ﾃﾞｰﾀ配列
    239         * @param       start   配列内の開始位置
    240         * @param       length  配列から読み取られる文字数
    241         * @see org.xml.sax.helpers.DefaultHandler#characters(char[] , int , int )
    242         */
    243        @Override
    244        public void characters( final char[] buffer, final int start, final int length ) throws SAXException {
    245                if( ! ROW.equals( key ) && ! ROWSET.equals( key ) && length > 0 ) {
    246                        body.append( buffer,start,length );
    247                        bodyIn = true;
    248                }
    249        }
    250
    251        /**
    252         * 要素の開始通知を受け取ります。
    253         * ｲﾝﾀﾌｪｰｽ ContentHandler 内の startElement ﾒｿｯﾄﾞをｵｰﾊﾞｰﾗｲﾄﾞしています。
    254         * ﾊﾟｰｻは XML 文書内の各要素の前でこのﾒｿｯﾄﾞを呼び出します。
    255         * 各 startElement ｲﾍﾞﾝﾄには対応する endElement ｲﾍﾞﾝﾄがあります。
    256         * これは、要素が空である場合も変わりません。対応する endElement ｲﾍﾞﾝﾄの前に、
    257         * 要素のｺﾝﾃﾝﾂ全部が順番に報告されます。
    258         * ここでは、ﾀｸﾞがﾚﾍﾞﾙ３以上の場合は、上位ﾀｸﾞの内容として取り扱います。よって、
    259         * ﾀｸﾞに名前空間が定義されている場合、その属性は削除します。
    260         *
    261         * @og.rev 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。
    262         *
    263         * @param       namespace       名前空間 ＵＲＩ
    264         * @param       localName       前置修飾子を含まないﾛｰｶﾙ名。名前空間処理が行われない場合は空文字列
    265         * @param       qname           前置修飾子を持つ修飾名。修飾名を使用できない場合は空文字列
    266         * @param       attributes      要素に付加された属性。属性が存在しない場合、空の Attributesｵﾌﾞｼﾞｪｸﾄ
    267         * @see org.xml.sax.helpers.DefaultHandler#startElement(String , String , String , Attributes )
    268         */
    269        @Override
    270        public void startElement(final String namespace, final String localName,
    271                                                         final String qname, final Attributes attributes) throws SAXException {
    272                if( ROWSET.equals( qname ) ) {
    273                        if( listener != null ) {
    274                                element = new TagElement( ROWSET,defaultMap );
    275                                element.put( ROWSET_TABLE,attributes.getValue( ROWSET_TABLE ) );
    276                                listener.actionInit( element );
    277                        }
    278                        element = null;
    279                }
    280                else if( ROW.equals( qname ) ) {
    281                        element = new TagElement( ROW,defaultMap );
    282                        final String num = attributes.getValue( ROW_NUM );
    283                        element.setRowNo( num );
    284                }
    285                else if( EXEC_SQL.equals( qname ) ) {
    286                        element = new TagElement( EXEC_SQL );
    287                        // 8.1.0.3 (2022/01/21) EXEC_SQLに、exists属性追加。
    288                        element.put( EXEC_EXISTS,attributes.getValue( EXEC_EXISTS ) );
    289                }
    290                else if( MERGE_SQL.equals( qname ) ) {
    291                        element = new TagElement( MERGE_SQL );
    292                }
    293
    294                if( level <= 2 ) {
    295                        key = qname;
    296                        body.setLength(0);              // StringBuilder の初期化
    297                }
    298                else {
    299                        // ﾚﾍﾞﾙ3 以上のﾀｸﾞは上位ﾀｸﾞの内容として扱います。
    300                        // 6.0.2.5 (2014/10/31) char を append する。
    301                        body.append( '<' ).append( qname );
    302                        final int len = attributes.getLength();
    303                        for( int i=0; i<len; i++ ) {
    304                                // 名前空間の宣言は、削除しておきます。あくまでﾃﾞｰﾀとして取り扱う為です。
    305                                final String attr = attributes.getQName(i);
    306                                if( ! attr.startsWith( "xmlns:" ) ) {
    307                                        body.append( ' ' )
    308                                                .append( attr ).append( "=\"" )
    309                                                .append( attributes.getValue(i) ).append( '"' );
    310                                }
    311                        }
    312                        body.append( '>' );
    313                }
    314
    315                bodyIn = false;         // 入れ子状のﾀｸﾞのBODY部の有無
    316                level ++ ;
    317        }
    318
    319        /**
    320         * 要素の終了通知を受け取ります。
    321         * ｲﾝﾀﾌｪｰｽ ContentHandler 内の endElement ﾒｿｯﾄﾞをｵｰﾊﾞｰﾗｲﾄﾞしています。
    322         * SAX ﾊﾟｰｻは、XML 文書内の各要素の終わりにこのﾒｿｯﾄﾞを呼び出します。
    323         * 各 endElement ｲﾍﾞﾝﾄには対応する startElement ｲﾍﾞﾝﾄがあります。
    324         * これは、要素が空である場合も変わりません。
    325         *
    326         * @og.rev 6.4.3.2 (2016/02/19) findBugs. element は、ｺﾝｽﾄﾗｸﾀで初期化されません。
    327         * @og.rev 6.9.9.0 (2018/08/20) body の最後の処理の修正。
    328         *
    329         * @param       namespace       名前空間 URI
    330         * @param       localName       前置修飾子を含まないﾛｰｶﾙ名。名前空間処理が行われない場合は空文字列
    331         * @param       qname   前置修飾子を持つ XML 1.0 修飾名。修飾名を使用できない場合は空文字列
    332         * @see org.xml.sax.helpers.DefaultHandler#endElement(String , String , String )
    333         */
    334        @Override
    335        public void endElement(final String namespace, final String localName, final String qname) throws SAXException {
    336                level -- ;
    337                if( ROW.equals( qname ) ) {
    338                        if( listener != null ) {
    339                                listener.actionRow( element );
    340                        }
    341                        element = null;
    342                }
    343                // 6.4.3.2 (2016/02/19) findBugs. element は、ｺﾝｽﾄﾗｸﾀで初期化されません。
    344                else if( EXEC_SQL.equals( qname ) && element != null ) {
    345                        element.setBody( body.toString().trim() );
    346                        if( listener != null ) {
    347                                listener.actionExecSQL( element );
    348                        }
    349                        element = null;
    350                }
    351                // 6.4.3.2 (2016/02/19) findBugs. element は、ｺﾝｽﾄﾗｸﾀで初期化されません。
    352                else if( MERGE_SQL.equals( qname ) && element != null ) {
    353                        element.setBody( body.toString().trim() );
    354                        if( listener != null ) {
    355                                listener.actionMergeSQL( element );
    356                        }
    357                        element = null;
    358                }
    359                else if( level <= 2 && element != null ) {
    360                                element.put( key , body.toString().trim() );
    361                }
    362                else {
    363                        if( bodyIn ) {
    364                                body.append( "</" ).append( qname ).append( '>' );              // 6.0.2.5 (2014/10/31) char を append する。
    365                        }
    366                        else {
    367                                // 6.9.9.0 (2018/08/20) body の最後の処理の修正。
    368//                              body.insert( body.length()-1, " /" );           // ﾀｸﾞの最後を " />" とする。
    369                                final int len = body.length();
    370                                if( len > 0 && body.charAt( len-1 ) == '>' ) {
    371                                        body.insert( len-1, " /" );                             // ﾀｸﾞの最後を " />" とする。
    372                                }
    373                        }
    374                }
    375        }
    376}