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.plugin.develop;
017
018import org.opengion.fukurou.system.OgBuilder ;                          // 6.4.4.1 (2016/03/18)
019import org.opengion.hayabusa.develop.AbstractJspCreate;
020import org.opengion.hayabusa.develop.JspEnumeration.GROUPING_FUNCTIONS ;
021import org.opengion.hayabusa.develop.JspEnumeration.WHERE_OPERATORS ;
022import org.opengion.hayabusa.develop.JspConvertEntity;
023import org.opengion.fukurou.xml.OGElement;
024import static org.opengion.fukurou.util.StringUtil.isNull;
025
026import java.util.ArrayList;
027import java.util.List;
028import java.util.Map;
029import java.util.HashMap;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032
033/**
034 * result.jspの<og:query >タグを作成します。
035 *
036 * ●使用例
037 *      <og:query
038 *              command     = "{@command}"
039 *              debug       = "{@debug}
040 *              dbid        = "{@FROM_DBID}"
041 *              maxRowCount = "{@maxRowCount}" >
042 *          select A1.xx , A1.xx ,・・・
043 *          from   xxx A1 inner join xxx B1
044 *          where  ・・・
045 *          group by  ・・・
046 *          having    ・・・
047 *          ORDER BY  ・・・
048 *      </og:query>
049 *
050 * @og.rev 5.6.1.2 (2013/02/22) 文字列連結から、XML処理するように変更します。
051 * @author Takeshi.Takada
052 *
053 */
054public class JspCreate_QUERY extends AbstractJspCreate {
055        /** このプログラムのVERSION文字列を設定します。   {@value} */
056        private static final String VERSION = "6.4.4.1 (2016/03/18)" ;
057
058        private static final String SPACE = "                              " ;  // カラムの位置合わせ用
059
060        // 6.3.9.1 (2015/11/27) Variables should start with a lowercase character(PMD)
061        private List<JspConvertEntity> queryROWS        ;
062        private List<JspConvertEntity> resultROWS       ;
063        private List<JspConvertEntity> constROWS        ;
064        private List<JspConvertEntity> joinROWS         ;
065        private List<JspConvertEntity> joinOnROWS       ;
066        private List<JspConvertEntity> havingROWS       ;
067
068        private String ns = "";         // 5.2.1.0 (2010/10/01) 名前空間
069
070        /**
071         * コンストラクター
072         *
073         * インスタンス構築時に、タグ名(key)とファイル名(names)を指定します。
074         *
075         * @og.rev 6.3.9.1 (2015/11/27) コンストラクタを用意して、KEY,NAME をセットするように変更します。
076         */
077        public JspCreate_QUERY() {
078                super( ":query" , "result" );
079        }
080
081        /**
082         * 初期化メソッド
083         *
084         * 内部で使用する JspConvertEntity の リスト のマップを受け取り、初期化を行います。
085         *
086         * @og.rev 5.2.1.0 (2010/10/01) 名前空間を、og 決め打ちから、名前空間指定無しに変更します。
087         *
088         * @param       master  JspConvertEntityのリストのマップ
089         */
090        @Override
091        protected void init( final Map<String,List<JspConvertEntity>> master ) {
092                // 6.3.9.1 (2015/11/27) Variables should start with a lowercase character(PMD)
093                queryROWS       = master.get( "QUERY" );
094                resultROWS      = master.get( "RESULT" );
095                constROWS       = master.get( "CONST" );
096                joinROWS        = master.get( "JOIN" );
097                joinOnROWS      = master.get( "JOIN_ON" );
098                havingROWS      = master.get( "HAVING" );
099
100        }
101
102        /**
103         * JSPに出力するタグの内容を作成します。
104         * 引数より作成前のタグの属性内容を確認するする事が出来ます。
105         *
106         * @og.rev 5.2.1.0 (2010/10/01) メソッドの引数を、OGAttributes から OGElement に変更します。
107         * @og.rev 5.2.1.0 (2010/10/01) 名前空間を、og 決め打ちから、引数を使用するように変更します。
108         * @og.rev 5.6.4.4 (2013/05/31) select カラムに、コメントを付与します。
109         *
110         * @param ele OGElementエレメントオブジェクト
111         * @param       nameSpace       このドキュメントのnameSpace( og とか mis とか )
112         *
113         * @return      変換された文字列
114         * @og.rtnNotNull
115         * @throws Throwable 変換時のエラー
116         */
117        @Override
118        protected String execute( final OGElement ele , final String nameSpace )  throws Throwable {
119                ns = nameSpace.isEmpty() ? "" : nameSpace + ":" ;       // 5.2.1.0 (2010/10/01) 名前空間
120
121                // この OGElement の階層の深さを探ります。
122                // ele.getText( para ) とすることでXML全体を階層表示できる。
123        //      int para = ele.getParentCount();
124
125                // TODO Auto-generated method stub
126                //書き出す文字列を作成開始。
127
128                final List<String> selects              = new ArrayList<>();
129                final List<String> clmCmnt              = new ArrayList<>();            // 5.6.4.4 (2013/05/31) select カラムに、コメントを付与します。
130                final List<String> tables               = new ArrayList<>();
131                final List<String> orders               = new ArrayList<>();
132                final List<String> group                = new ArrayList<>();
133                final List<String> having_part  = new ArrayList<>();
134                final List<String> grp_clm              = new ArrayList<>();            // 6.0.2.5 (2014/10/31) refactoring
135
136                //HAVING情報から<og:query>タグのテキスト部を生成する準備をします。
137                // 6.3.9.1 (2015/11/27) Variables should start with a lowercase character(PMD)
138                if( isNotEmpty(havingROWS) ){
139                        for( final JspConvertEntity row : havingROWS ){
140                                having_part.add(row.getRemarks());
141                                if( GROUPING_FUNCTIONS.search( row.getRemarks() ) ){
142                                        grp_clm.add( row.getFullColumnName() );         // 6.0.2.5 (2014/10/31) refactoring
143                                }
144                        }
145                }
146                //RESULT情報から<og:query>タグのテキスト部を生成する準備をします。
147                boolean grouping = false;
148                // 6.3.9.1 (2015/11/27) Variables should start with a lowercase character(PMD)
149                if( isNotEmpty(resultROWS) ){
150                        for( int i=0 ; i<resultROWS.size() ; i++ ){
151                                final JspConvertEntity result = resultROWS.get(i);
152                                //Select句の情報を作成
153                                selects.add( result.getSelectPartColumnName() );
154                                // 5.6.4.4 (2013/05/31) select カラムに、コメントを付与します。
155                                clmCmnt.add( result.getTableName() + "." + result.getColumnCommentName() );
156                                //テーブル名を検証して、テーブル数のみの情報にします。
157                                if( tables != null && !tables.contains( result.getTableName() ) ) {
158                                        tables.add( result.getFromPartTableName() );
159                                }
160                                //並び順に利用するカラムを取得する。
161                                if( "1".equals( result.getUseOrder() ) ) {
162                                        orders.add(Integer.toString( i + 1 ));
163                                }
164                                //GROUP BYに必要な情報を取得します。
165                                if( GROUPING_FUNCTIONS.contains( result.getRemarks() ) ){
166                                        grouping = true;
167                //              }else if(having_grouping_column.indexOf( result.getFullColumnName() ) > -1 ){
168                //                      group.add( result.getFullColumnName() );
169                                }else{
170                                        group.add( result.getFullColumnName() );
171                                }
172                        }
173                }
174
175                //JOIN情報から<og:query>タグのテキスト部(join句)を生成する準備をします。
176                // 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
177
178                final JspConvertEntity join_on = isNotEmpty(joinOnROWS) ? joinOnROWS.get( 0 ) : null;           // 6.3.9.1 (2015/11/27)
179
180                //JOIN情報から<og:query><og:where><og:and>タグの検索句を生成する準備をします。
181                // 6.3.9.1 (2015/11/27) Variables should start with a lowercase character(PMD)
182                if( queryROWS != null && isNotEmpty( constROWS ) ){
183                        queryROWS.addAll( constROWS );
184                }
185
186                final OGElement queryEle  = new OGElement( ns + "query" );
187                queryEle.addAttr( "command"             ,"{@command}" );
188                queryEle.addAttr( "debug"               ,"{@debug}" );
189                queryEle.addAttr( "dbid"                ,"{@FROM_DBID}" );
190                queryEle.addAttr( "maxRowCount" ,"{@maxRowCount}" );
191
192                // 6.3.9.1 (2015/11/27) Variables should start with a lowercase character(PMD)
193                queryEle.addNode( queryText(selects , clmCmnt , tables , joinROWS  , join_on ) );       // 5.6.4.4 (2013/05/31) select カラムに、コメントを付与
194
195                if( isNotEmpty(queryROWS) ){            // 6.3.9.1 (2015/11/27)
196                        final OGElement whereEle = new OGElement( ns + "where" );
197
198                        for( final JspConvertEntity where : queryROWS ) {               // 6.3.9.1 (2015/11/27)
199                                if( "QUERY".equals(where.getType()) ){
200                                        whereEle.addNode( andWhereQuery(where.getFullColumnName() , where.getRemarks() ,"{@"+ where.getColumnName() +"}" ,where.isNumber()) );
201                                }
202                                if( "CONST".equals(where.getType()) ) {
203                                        whereEle.addNode( andWhereConst( where.getFullColumnName(), where.getRemarks() , where.isNumber()) );
204                                }
205                        }
206                        queryEle.addNode( whereEle );
207                }
208                if( grouping || !grp_clm.isEmpty() ) {                                  // 6.0.2.5 (2014/10/31) refactoring
209                        queryEle.addNode( T2 + "group by " + chainChar(group,",") + CR );
210                }
211                if( !grp_clm.isEmpty() ){                                                               // 6.0.2.5 (2014/10/31) refactoring
212                        queryEle.addNode( T2 + "having " + chainChar(having_part ," and ") + CR );
213                }
214                if( !orders.isEmpty() ){
215                        queryEle.addNode( apperEle( "ORDER BY" , "ORDER_BY" , orders ) );
216                }
217
218                return queryEle.getText(0);
219        }
220
221        /**
222         * result.jspのog:queryタグのテキスト部を生成します。
223         *
224         * 補足1
225         * 引数のjoin_onがnullでないときは、優先的にjoin_onの内容でJOIN句を生成します。
226         *
227         * @og.rev 5.6.4.4 (2013/05/31) select カラムに、コメントを付与します。
228         * @og.rev 6.3.9.1 (2015/11/27) 継承されていないため、protected → private に変更します。
229         * @og.rev 6.4.4.1 (2016/03/18) StringBuilderの代わりに、OgBuilderを使用する。
230         *
231         * @param       selects 検索SQLのリスト
232         * @param       clmCmnt カラムコメントのリスト
233         * @param       tables  テーブル名のリスト
234         * @param       joins   JspConvertEntityのリスト
235         * @param       join_on JspConvertEntityオブジェクト
236         *
237         * @return      og:queryタグのテキスト部
238         * @og.rtnNotNull
239         */
240        private String queryText( final List<String> selects , final List<String> clmCmnt , final List<String> tables ,
241                                                                final List<JspConvertEntity> joins ,final JspConvertEntity join_on ) {
242
243                final OgBuilder buf = new OgBuilder()
244                                .appendCR()
245                                .appendCR( T2 , "select" );
246
247                // 5.6.4.4 (2013/05/31) select カラムに、コメントを付与します。
248                final int size = selects.size();
249                for( int i=0; i<size; i++ ) {
250                        final String clm = selects.get(i) ;
251                        buf.append( T3 )
252                                .appendCase( i == 0  , " " , "," )              // flag , true , false
253                                .appendCR( clm , SPACE.substring( clm.length() )
254                                                        , T3 , T3 , T3 , T3
255                                                        , "<!-- " , clmCmnt.get(i) , " -->" );
256                }
257
258                buf.append( T2 , "from " );
259
260                if( join_on != null ) {
261                        //JOIN_ONが存在する場合は、直接SQLを組み立てて処理を終了する。
262                        return buf.append( T2 , join_on.getRemarks() ).toString();
263                }
264
265                if( !isNotEmpty( joins ) ) {
266                        return buf.append( tables.get(0) ).toString();
267                }
268
269                //テーブルの内容を構造化します。
270                final TableStruct structs = createStruct(joins);
271
272                String beforeLeft  = "";
273                String beforeRight = "";
274                final StringBuilder sbPre = new StringBuilder( BUFFER_MIDDLE ); // 6.1.0.0 (2014/12/26) refactoring
275
276                final Map<String,String> joinPartsMap = new HashMap<>();
277
278                boolean isStartJoin = false;
279
280                for( int i=0 ; i<joins.size() ; i++ ){
281                        //join句を作るのとネスト構造を作るのは処理を分離させる。
282                        final JspConvertEntity join = joins.get( i );
283
284                        if( beforeLeft.equals(join.getFromPartTableName()) ){
285                                //前の処理と左側のテーブルは同じ
286                                if( ! beforeRight.equals(join.getJoinColumn().getFromPartTableName()) ) {
287                                        //前の処理と右側のテーブルが違う
288                                        sbPre.append( sqlJoinOn( "", join.getJoinColumn().getFromPartTableName() , join.getJoinType()) );
289                                        isStartJoin = true;
290                                }
291                        } else {
292                                //前の処理と左側のテーブルが違う
293                                if( ! beforeRight.equals(join.getJoinColumn().getFromPartTableName()) ) {
294                                        //前の処理と右側のテーブルが違う
295                                        //前の処理のJoin句をテーブル名別にセット
296                                        final String str = sbPre.toString();
297                                        joinPartsMap.put( beforeLeft, str );
298                                        //バッファを初期化
299                                        sbPre.setLength(0);
300                                        sbPre.append( sqlJoinOn( join.getFromPartTableName() , join.getJoinColumn().getFromPartTableName() , join.getJoinType()) );
301                                        isStartJoin = true;
302                                }
303                        }
304                        if( !isStartJoin  ) {
305                                sbPre.append( CR ).append( T3 ).append( "and").append( T1 );
306                        }
307                        sbPre.append( join.getFullColumnName() )
308                                .append( T2 ).append( '=' ).append( T1 )                // 6.0.2.5 (2014/10/31) char を append する。
309                                .append( join.getJoinColumn().getFullColumnName() );
310                        beforeLeft  = join.getFromPartTableName();
311                        beforeRight = join.getJoinColumn().getFromPartTableName();
312                        isStartJoin = false;
313                }
314                //最終分
315                joinPartsMap.put( beforeLeft, sbPre.toString() );
316
317                final StringBuilder sbJoin = new StringBuilder( BUFFER_MIDDLE );
318                //Join句を組み立てます。
319                createJoinPart(structs.getJoinTables(),joinPartsMap,sbJoin);
320
321                return buf.append( sbJoin.toString() ).toString();
322
323        }
324
325        /**
326         * join句の一部を作成する。
327         *
328         * @og.rev 6.4.4.1 (2016/03/18) StringBuilderの代わりに、OgBuilderを使用する。
329         *
330         * @param       left            join句のレフト
331         * @param       right           join句のライト
332         * @param       join_type       [1:inner join/その他:left outer join]
333         *
334         * @return      join句の一部
335         * @og.rtnNotNull
336         */
337        private String sqlJoinOn(final String left , final String right ,final String join_type){
338                return new OgBuilder()
339                                .append( " " , left )
340                                .appendCase( "1".equals( join_type )
341                                                                , " inner join "                        // true
342                                                                , " left outer join " )         // false
343                                .appendCR( right )
344                                .append( T3 , "on" , T1 )
345                                .toString();
346
347        }
348
349        /**
350         * JOIN句を組み立てます。
351         *
352         * JOIN句は、内部で再帰処理されます。引数の StringBuilder に最終的な JOIN句が格納されます。
353         *
354         * @param       structs テーブル内容の構造化TableStructのリスト
355         * @param       joinMap JOIN句マップ
356         * @param       buff    StringBuilderオブジェクト
357         */
358        private void createJoinPart( final List<TableStruct> structs , final Map<String,String> joinMap ,final StringBuilder buff ) {
359                // 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD)
360                for( int i=0 ; i<structs.size() ; i++ ){
361                        final TableStruct struct = structs.get(i);
362                        final String part = joinMap.get(struct.getTableName());
363                        if( part != null ){
364                                final Matcher matcher = Pattern.compile( "join " + struct.getTableName() ).matcher( buff );
365                                if( matcher.find()) {
366                                        final int start = matcher.start();
367                                        buff.delete(start,matcher.end());
368                                        buff.insert(start , "join ( " + part + " )" );
369                                }else{
370                                        buff.append( part );
371                                }
372                        }
373                        createJoinPart(struct.getJoinTables(),joinMap,buff);
374                }
375        }
376
377        /**
378         * result.jspの og:query og:appear タグを生成します。
379         *
380         * @og.rev 5.2.1.0 (2010/10/01) 名前空間を、og 決め打ちから、引数を使用するように変更します。
381         * @og.rev 6.3.9.1 (2015/11/27) 継承されていないため、protected → private に変更します。
382         *
383         * @param start_key             開始キー
384         * @param value                 値
385         * @param default_value 初期値リスト
386         *
387         * @return      og:query og:appear タグ
388         * @og.rtnNotNull
389         */
390        private OGElement apperEle( final String start_key , final String value , final List<String> default_value ){
391
392                final OGElement apper = new OGElement( ns + "appear" );
393                apper.addAttr( "startKey"               ,start_key );
394                apper.addAttr( "value"                  ,"{@" + value + "}" );
395                apper.addAttr( "defaultVal"             ,chainChar( default_value , "," ) );
396                return apper;
397        }
398
399        /**
400         * result.jspの og:query og:where og:and タグを生成します。
401         * 処理グループ:QUERY
402         *
403         * @og.rev 6.3.9.1 (2015/11/27) 継承されていないため、protected → private に変更します。
404         *
405         * @param       left            左側式
406         * @param       operator        オペレーター
407         * @param       right           右側式
408         * @param       is_number       数字かどうか[true/false]
409         *
410         * @return      og:and タグ
411         * @og.rtnNotNull
412         */
413        private OGElement andWhereQuery( final String left , final String operator , final String right , final boolean is_number){
414
415                final String ope = isNull(operator) ? "eq" : operator ;
416
417                final WHERE_OPERATORS wrOpe = WHERE_OPERATORS.valueOf( ope );
418
419                final OGElement and = new OGElement( ns + "and" );
420                and.addAttr( "value" , wrOpe.apply( left , right , is_number ) );
421                return and;
422        }
423
424        /**
425         * result.jspのog:query og:where og:and タグを生成します。
426         * 処理グループ:CONST
427         *
428         * @og.rev 6.3.9.1 (2015/11/27) 継承されていないため、protected → private に変更します。
429         *
430         * @param       left            左側式
431         * @param       right           右側式
432         * @param       is_number       数字かどうか[true/false]
433         *
434         * @return      og:and タグ
435         */
436        private OGElement andWhereConst( final String left , final String right , final boolean is_number ){
437                final String operator = right.indexOf( ',' ) >= 0 ? "in" : "eq";
438                return  andWhereQuery( left , operator , right , is_number );
439        }
440
441        /**
442         * query.jspの og:column タグを生成します。
443         *
444         * @param name                  タグのname
445         * @param default_value 初期値
446         *
447         * @return      og:columnタグ
448         */
449
450        /**
451         * テーブルの結合関係を再現する構造体につめ直すメソッド。
452         *
453         * @param       joins   JspConvertEntityのリスト
454         *
455         * @return      テーブルの結合関係を再現する構造体
456         * @og.rtnNotNull
457         */
458        private TableStruct createStruct( final List<JspConvertEntity> joins ) {
459                final TableStruct st = new TableStruct();
460                for( int i=0 ; i<joins.size() ; i++ ){
461                        final JspConvertEntity join = joins.get( i );
462                        final String left_name = join.getFromPartTableName();
463                        final String right_name = join.getJoinColumn().getFromPartTableName();
464
465                        TableStruct left = st.getJoinTable( left_name );
466                        TableStruct right = st.getJoinTable( right_name );
467
468                        if( left == null && right == null ) {
469                                //全くの新規。
470                                left = new TableStruct();
471                                left.setTableName( left_name );
472                                right = new TableStruct();
473                                right.setTableName(right_name);
474                                left.addJoinTable( right );
475                                st.addJoinTable( left );
476                        }else{
477                                if( left != null && right == null ){
478                                        right = new TableStruct();
479                                        right.setTableName(right_name);
480                                        left.addJoinTable( right );
481                                }
482                        }
483                }
484                return st;
485        }
486
487        /**
488         * テーブルの結合状態を階層構図にする為のオブジェクト
489         *
490         * @author Administrator
491         *
492         */
493        private static final class TableStruct {
494                private final List<TableStruct> _joins = new ArrayList<>();
495                private String tableName;
496
497                /**
498                 * テーブル名を設定。
499                 *
500                 * @param table_name テーブル名
501                 */
502                public void setTableName( final String table_name ) {
503                        tableName = table_name;
504                }
505
506                /**
507                 * テーブル名を取得。
508                 *
509                 * @return テーブル名
510                 */
511                public String getTableName() {
512                        return tableName;
513                }
514
515                /**
516                 * 結合テーブルを追加。
517                 *
518                 * @param join_table 結合テーブル
519                 */
520                public void addJoinTable( final TableStruct join_table ) {
521                        _joins.add(join_table);
522                }
523
524                /**
525                 * 結合テーブルを全て取得。
526                 *
527                 * @return 全ての結合テーブル
528                 */
529                public List<TableStruct> getJoinTables() {
530                        return _joins;
531                }
532
533                /**
534                 * 指定したテーブルを取得。
535                 *
536                 * @param table_name テーブル名
537                 * @return 指定したテーブル
538                 */
539                public TableStruct getJoinTable( final String table_name ) {
540                        return search(_joins,table_name);
541                }
542
543                /**
544                 * テーブル同士が一致しているか検証する。
545                 *
546                 * @param table_name テーブル名
547                 * @return 検証した結果の真偽
548                 */
549                public boolean equalTable( final String table_name ) {
550                        return tableName != null && tableName.equals( table_name ) ;
551                }
552
553                /**
554                 * 指定したテーブルが存在しているか検証する。
555                 *
556                 * @param table_name テーブル名
557                 * @return  検証した結果の真偽
558                 */
559                public boolean constains( final String table_name ) {
560                        for( int i=0; i<_joins.size() ; i++ ){
561                                final TableStruct join = _joins.get( i );
562                                if( join.equalTable(table_name) ){
563                                        return true;
564                                }
565                        }
566                        return false;
567                }
568
569                /**
570                 * 結合先を含めて指定したテーブルを取得する。
571                 *
572                 * @param joins 結合先Listオブジェクト
573                 * @param table_name テーブル名
574                 * @return 指定したテーブル
575                 */
576                private TableStruct search( final List<TableStruct> joins , final String table_name ) {
577                        TableStruct join =  null;
578                        for( int i=0; i<joins.size() ; i++ ){
579                                join = joins.get( i );
580                                if( join.equalTable(table_name) ){
581                                        return join;
582                                } else {
583                                        join = search(join.getJoinTables(),table_name);
584                                }
585                        }
586                        return join;
587                }
588        }
589}