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.db;
017
018import java.util.List;
019import java.util.ArrayList;
020import java.util.Locale ;
021import java.util.Arrays ;
022import java.util.Set ;
023import java.util.HashSet ;
024import java.util.LinkedHashSet ;
025import java.util.StringJoiner ;
026
027import org.opengion.fukurou.util.StringUtil;
028import org.opengion.fukurou.system.OgBuilder ;
029import org.opengion.fukurou.system.OgRuntimeException ;
030import static org.opengion.fukurou.system.HybsConst.CR;
031import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;
032
033/**
034 * QueryMaker は、カラム名などから、SELECT,INSERT,UPDATE,DALETE 文字列を作成するクラスです。
035 *
036 * 基本的には、カラム名と、それに対応する値のセットで、QUERY文を作成します。
037 * 値には、[カラム名] が使用でき、出力される値として、? が使われます。
038 * これは、PreparedStatement に対する引数で、処理を行うためです。
039 * この[カラム名]のカラム名は、検索された側のカラム名で、INSERT/UPDATE/DELETE等が実行される
040 * データベース(テーブル)のカラム名ではありません。(偶然、一致しているかどうかは別として)
041 *
042 * @og.rev 6.8.6.0 (2018/01/19) 新規作成
043 *
044 * @version  6.8.6.0 (2018/01/19)
045 * @author       Kazuhiko Hasegawa
046 * @since    JDK6.0,
047 */
048public class QueryMaker {
049        private static final String QUERY_TYPE = "SELECT,INSERT,UPDATE,DELETE,MERGE" ;
050
051        private final List<String> whrList = new ArrayList<>() ;        // where条件に含まれる [カラム名] のリスト(パラメータ一覧)
052
053        private String queryType ;              // QUERYタイプ(SELECT,INSERT,UPDATE,DELETE,MERGE) を指定します。
054        private String table ;
055        private String names ;
056        private String omitNames ;
057        private String where ;
058        private String whrNames ;
059        private String orderBy ;
060        private String cnstKeys ;
061        private String cnstVals ;
062
063        private int             clmLen;                 // names カラムの "?" に置き換えられる個数
064        private boolean isSetup ;               // セットアップ済みを管理しておきます。
065        private String[] nameAry;
066
067        /**
068         * デフォルトコンストラクター
069         *
070         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
071         */
072        public QueryMaker() { super(); }                // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
073
074        /**
075         * 処理の前に、入力データの整合性チェックや、初期設定を行います。
076         *
077         * あまり、何度も実行したくないので、フラグ管理しておきます。
078         *
079         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
080         * @og.rev 6.9.0.2 (2018/02/13) omitNamesの対応
081         * @og.rev 6.9.8.0 (2018/05/28) setup() は、内部処理からのみ呼ばれるので、private化します。
082         */
083//      public void setup() {
084        private void setup() {
085                if( isSetup ) { return; }               // セットアップ済み
086
087                if( StringUtil.isNull( table ) ) {
088                        final String errMsg = "指定の table に、null、ゼロ文字列は指定できません。"
089                                                        + " table=" + table ;
090                        throw new OgRuntimeException( errMsg );
091                }
092
093                if( StringUtil.isNull( names ) ) {
094                        final String errMsg = "指定の names に、null、ゼロ文字列は指定できません。"
095                                                        + " names=" + names ;
096                        throw new OgRuntimeException( errMsg );
097                }
098
099                // 6.9.0.2 (2018/02/13) omitNamesの対応
100                final String[]    nmAry  = StringUtil.csv2Array( names );
101                final Set<String> nmSet  = new LinkedHashSet<>( Arrays.asList( nmAry ) );               // names の順番は、キープします。
102                final String[]    omtAry = StringUtil.csv2Array( omitNames );
103                final Set<String> omtSet = new HashSet<>( Arrays.asList( omtAry ) );                    // 除外する順番は、問いません。
104                nmSet.removeAll( omtSet );
105
106                // 初期設定
107                clmLen  = nmSet.size();
108                nameAry = nmSet.toArray( new String[clmLen] );
109
110//              // 初期設定
111//              nameAry = StringUtil.csv2Array( names );
112//              clmLen = nameAry.length;
113
114                // [カラム名] List は、whereNames + where の順番です。(whrListの登録順を守る必要がある)
115                // where条件も、この順番に連結しなければなりません。
116                where = StringUtil.join( " AND " , whrNames , formatSplit( where ) );   // formatSplit で、whrListの登録を行っている。
117
118                isSetup = true;
119        }
120
121        /**
122         * データを検索する場合に使用するSQL文を作成します。
123         *
124         * SELECT names FROM table WHERE where ORDER BY orderBy ;
125         *
126         * cnstKeys,cnstVals は、使いません。
127         * where,orderBy は、それぞれ、値が存在しない場合は、設定されません。
128         *
129         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
130         * @og.rev 6.9.0.2 (2018/02/13) omitNamesの対応
131         *
132         * @return  検索SQL
133         * @og.rtnNotNull
134         */
135        public String getSelectSQL() {
136                if( !"SELECT".equals( queryType ) ) {
137                        final String errMsg = "指定のQUERYタイプと異なるSQL文を要求しています。" + CR
138                                                        + " 要求SQL=SELECT  queryType=" + queryType ;
139                        throw new OgRuntimeException( errMsg );
140                }
141
142                setup();
143
144                return new OgBuilder()
145//                      .append(   "SELECT "    , names )
146                        .append(   "SELECT "            )
147                        .join(     ","          , nameAry )             // 6.9.0.2 (2018/02/13) names ではなく、omitNames後のカラム配列を使用します。
148                        .append(   " FROM "     , table )
149                        .appendNN( " WHERE "    , where )               // nullなら、追加しない。where + whereNames
150                        .appendNN( " ORDER BY " , orderBy )             // nullなら、追加しない。
151                        .toString();
152        }
153
154        /**
155         * データを追加する場合に使用するSQL文を作成します。
156         *
157         * INSERT INTO table ( names,cnstKeys ) VALUES ( values,cnstVals ) ;
158         *
159         * cnstKeys,cnstVals は、INSERTカラムとして使います。
160         * where,orderBy は、使いません。
161         *
162         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
163         * @og.rev 6.9.0.2 (2018/02/13) omitNamesの対応
164         *
165         * @return  追加SQL
166         * @og.rtnNotNull
167         */
168        public String getInsertSQL() {
169                if( !"INSERT".equals( queryType ) && !"MERGE".equals( queryType ) ) {
170                        final String errMsg = "指定のQUERYタイプと異なるSQL文を要求しています。" + CR
171                                                        + " 要求SQL=INSERT  queryType=" + queryType ;
172                        throw new OgRuntimeException( errMsg );
173                }
174
175                setup();
176
177                return new OgBuilder()
178                        .append( "INSERT INTO " ).append( table )
179//                      .append( " ( " ).append( names )
180                        .append( " ( " )
181                        .join(     "," , nameAry  )                     // 6.9.0.2 (2018/02/13) names ではなく、omitNames後のカラム配列を使用します。
182                        .appendNN( "," , cnstKeys )
183                        .append( " ) VALUES ( " )
184                        .appendRoop( 0,clmLen,",",i -> "?" )
185                        .appendNN( "," , cnstVals )
186                        .append( " )" )
187                        .toString();
188        }
189
190        /**
191         * データを更新する場合に使用するSQL文を作成します。
192         *
193         * UPDATE table SET names[i]=values[i], ・・・cnstKeys[i]=cnstVals[i], ・・・ WHERE where;
194         *
195         * cnstKeys,cnstVals は、UPDATEカラムとして使います。
196         * orderBy は、使いません。
197         *
198         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
199         * @og.rev 6.9.9.1 (2018/08/27) cnstKeys,cnstValsは、個数違いの場合のみ、エラーです。
200         *
201         * @return  更新SQL
202         * @og.rtnNotNull
203         */
204        public String getUpdateSQL() {
205                if( !"UPDATE".equals( queryType ) && !"MERGE".equals( queryType ) ) {
206                        final String errMsg = "指定のQUERYタイプと異なるSQL文を要求しています。" + CR
207                                                        + " 要求SQL=UPDATE  queryType=" + queryType ;
208                        throw new OgRuntimeException( errMsg );
209                }
210
211                setup();
212
213                final String[] cnKey = StringUtil.csv2Array( cnstKeys );        // @og.rtnNotNull
214                final String[] cnVal = StringUtil.csv2Array( cnstVals );        // @og.rtnNotNull
215
216                // 整合性チェック
217                // 6.9.8.0 (2018/05/28) FindBugs:null でないことがわかっている値の冗長な null チェック
218//              if( cnKey != null && cnVal == null ||
219//                      cnKey == null && cnVal != null ||
220//                      cnKey != null && cnVal != null && cnKey.length != cnVal.length ) {
221                // 6.9.9.1 (2018/08/27) cnstKeys,cnstValsは、個数違いの場合のみ、エラーです。
222//              if( cnKey.length == 0 || cnVal.length == 0 || cnKey.length != cnVal.length ) {
223//                      final String errMsg = "指定の keys,vals には、null、ゼロ件配列、または、個数違いの配列は指定できません。"
224                if( cnKey.length != cnVal.length ) {
225                        final String errMsg = "指定の keys,vals の個数が違ます。"
226                                                        + " keys=" + cnstKeys 
227                                                        + " vals=" + cnstVals ;
228                        throw new OgRuntimeException( errMsg );
229                }
230
231                // 6.9.8.0 (2018/05/28) FindBugs:コンストラクタで初期化されていないフィールドを null チェックなしで null 値を利用している
232                // queryType と、nameAry は、setup() メソッドで設定されるため、FindBugs の指摘は、対応済みとなります。
233                // とりあえず、条件判定を入れておいて、FindBugs の警告が出ないようにしておきます。
234                if( nameAry == null ) {
235                        // nameAry は、setup() メソッドで設定されるため、このエラーは出ません。
236                        final String errMsg = "何らかの不測の事態が発生しました。本来、このエラーは出ません。";
237                        throw new OgRuntimeException( errMsg );
238                }
239
240                return new OgBuilder()
241                        .append( "UPDATE " ).append( table )
242                        .append( " SET " )
243                        .appendRoop( 0,clmLen      ,",",i -> nameAry[i] + "=?" )
244                        .appendRoop( 0,cnVal.length,",",i -> cnKey[i]   + "="  + cnVal[i] )
245                        .appendNN( " WHERE " , where )          // nullなら、追加しない。where + whereNames
246                        .toString();
247        }
248
249        /**
250         * データを削除する場合に使用するSQL文を作成します。
251         *
252         * DELETE FROM table WHERE where;
253         *
254         * cnstKeys,cnstVal,orderBys は、使いません。
255         * where は、値が存在しない場合は、設定されません。
256         * orderBy は、使いません。
257         *
258         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
259         *
260         * @return  削除SQL
261         * @og.rtnNotNull
262         */
263        public String getDeleteSQL() {
264                if( !"DELETE".equals( queryType ) ) {
265                        final String errMsg = "指定のQUERYタイプと異なるSQL文を要求しています。" + CR
266                                                        + " 要求SQL=DELETE  queryType=" + queryType ;
267                        throw new OgRuntimeException( errMsg );
268                }
269
270                setup();
271
272                return new OgBuilder()
273                        .append(   "DELETE FROM " ).append( table )
274                        .appendNN( " WHERE " , where )          // nullなら、追加しない。where + whereNames
275                        .toString();
276        }
277
278        /**
279         * [カラム名]を含む文字列を分解し、Map に登録します。
280         *
281         * これは、[カラム名]を含む文字列を分解し、カラム名 を取り出し、whrList に
282         * 追加していきます。
283         * 戻り値は、[XXXX] を、? に置換済みの文字列になります。
284         *
285         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
286         *
287         * @param       fmt     [カラム名]を含む文字列
288         * @return  PreparedStatementに対応した変換後の文字列
289         */
290        private String formatSplit( final String fmt ) {
291                if( StringUtil.isNull( fmt ) ) { return fmt; }          // null,ゼロ文字列チェック
292
293                final StringBuilder rtnStr = new StringBuilder( BUFFER_MIDDLE );
294
295                int start = 0;
296                int index = fmt.indexOf( '[' );
297                while( index >= 0 ) {
298                        final int end = fmt.indexOf( ']',index );
299                        if( end < 0 ) {
300                                final String errMsg = "[ と ] との対応関係がずれています。"
301                                                                + "format=[" + fmt + "] : index=" + index ;
302                                throw new OgRuntimeException( errMsg );
303                        }
304
305                        // [ より前方の文字列は、rtnStr へ追加する。
306                        if( index > 0 ) { rtnStr.append( fmt.substring( start,index ) ); }
307        //              index == 0 は、][ と連続しているケース
308
309                        // [XXXX] の XXXX部分と、位置(?の位置になる)を、Listに登録
310                        whrList.add( fmt.substring( index+1,end ) );
311
312                        rtnStr.append( '?' );           // [XXXX] を、? に置換する。
313
314                        start = end+1 ;
315                        index = fmt.indexOf( '[',start );
316                }
317                // ] の後方部分は、rtnStr へ追加する。
318                rtnStr.append( fmt.substring( start ) );                // '[' が見つからなかった場合は、この処理で、すべての fmt データが、append される。
319
320                return rtnStr.toString();
321        }
322
323        /**
324         * QUERYタイプ(SELECT,INSERT,UPDATE,DELETE,MERGE) を指定します。
325         *
326         * 引数が nullか、ゼロ文字列の場合は、登録しません。
327         *
328         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
329         *
330         * @param       queryType       QUERYタイプ
331         */
332        public void setQueryType( final String queryType ) {
333                if( !StringUtil.isNull( queryType ) ) {
334                        if( QUERY_TYPE.contains( queryType ) ) {
335                                this.queryType = queryType;
336                        }
337                        else {
338                                final String errMsg = "queryType は、" + QUERY_TYPE + " から、指定してください。";
339                                throw new OgRuntimeException( errMsg );
340                        }
341                }
342        }
343
344        /**
345         * テーブル名をセットします。
346         *
347         * 引数が nullか、ゼロ文字列の場合は、登録しません。
348         *
349         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
350         *
351         * @param       table   テーブル名
352         */
353        public void setTable( final String table ) {
354                if( !StringUtil.isNull( table ) ) {
355                        this.table = table;
356                }
357        }
358
359        /**
360         * テーブル名を取得します。
361         *
362         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
363         *
364         * @return      テーブル名
365         */
366        public String getTable() {
367                return table;
368        }
369
370        /**
371         * カラム名をセットします。
372         *
373         * カラム名は、登録時に、大文字に変換しておきます。
374         * カラム名は、CSV形式でもかまいません。
375         * 引数が nullか、ゼロ文字列の場合は、登録しません。
376         *
377         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
378         *
379         * @param   names  キー(大文字のみ。内部で変換しておきます。)
380         */
381        public void setNames( final String names ) {
382                if( !StringUtil.isNull( names ) ) {
383                        this.names = names.toUpperCase(Locale.JAPAN);
384                }
385        }
386
387        /**
388         * カラム名を取得します。
389         *
390         * 登録時に、すでに、大文字に変換していますので、
391         * ここで取得するカラム名も、大文字に変換されています。
392         *
393         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
394         *
395         * @return      カラム名(大文字に変換済み)
396         */
397        public String getNames() {
398                return names;
399        }
400
401        /**
402         * 除外するカラム名をセットします。
403         *
404         * カラム名は、登録時に、大文字に変換しておきます。
405         * カラム名は、CSV形式でもかまいません。
406         * 引数が nullか、ゼロ文字列の場合は、登録しません。
407         *
408         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
409         *
410         * @param   omitNames  キー(大文字のみ。内部で変換しておきます。)
411         */
412        public void setOmitNames( final String omitNames ) {
413                if( !StringUtil.isNull( omitNames ) ) {
414                        this.omitNames = omitNames.toUpperCase(Locale.JAPAN);
415                }
416        }
417
418        /**
419         * WHERE条件をセットします。
420         *
421         * whereNames属性と同時に使用する場合は、"AND" で、処理します。
422         * 引数が nullか、ゼロ文字列の場合は、登録しません。
423         *
424         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
425         *
426         * @param   where  WHERE条件
427         */
428        public void setWhere( final String where ) {
429                if( !StringUtil.isNull( where ) ) {
430                        this.where = where;
431                }
432        }
433
434        /**
435         * WHERE条件となるカラム名をCSV形式でセットします。
436         *
437         * カラム名配列より、WHERE条件を、KEY=[KEY] 文字列で作成します。
438         * where属性と同時に使用する場合は、"AND" で、処理します。
439         * 引数が nullか、ゼロ件配列の場合は、登録しません。
440         *
441         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
442         *
443         * @param       whNames WHERE句作成のためのカラム名
444         */
445        public void setWhereNames( final String whNames ) {
446                if( !StringUtil.isNull( whNames ) ) {
447                        final String[] whAry = StringUtil.csv2Array( whNames );
448
449                        final StringJoiner sj = new StringJoiner( " AND " );            // 区切り文字
450                        for( final String whName : whAry ) {
451                                whrList.add( whName );
452                                sj.add( whName + "=?" );
453                        }
454                        whrNames = sj.toString();
455                }
456        }
457
458        /**
459         * orderBy条件をセットします。
460         *
461         * 引数が nullか、ゼロ文字列の場合は、登録しません。
462         *
463         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
464         *
465         * @param   orderBy  orderBy条件
466         */
467        public void setOrderBy( final String orderBy ) {
468                if( !StringUtil.isNull( orderBy ) ) {
469                        this.orderBy = orderBy;
470                }
471        }
472
473        /**
474         * 固定値のカラム名をセットします。
475         *
476         * nullでなく、ゼロ文字列でない場合のみセットします。
477         * カラム名は、CSV形式でもかまいません。
478         * 引数が nullか、ゼロ文字列の場合は、登録しません。
479         *
480         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
481         *
482         * @param   keys  固定値のカラム名
483         */
484        public void setConstKeys( final String keys ) {
485                if( !StringUtil.isNull( keys ) ) {
486                        this.cnstKeys = keys;
487                }
488        }
489
490        /**
491         * 固定値のカラム名に対応した、固定値文字列をセットします。
492         *
493         * nullでなく、ゼロ文字列でない場合のみセットします。
494         * 固定値は、CSV形式でもかまいません。
495         * 引数が nullか、ゼロ文字列の場合は、登録しません。
496         *
497         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
498         *
499         * @param   vals  固定値
500         */
501        public void setConstVals( final String vals ) {
502                if( !StringUtil.isNull( vals ) ) {
503                        this.cnstVals = vals;
504                }
505        }
506
507        /**
508         * PreparedStatement で、パラメータとなるカラム名の配列を返します。
509         *
510         * これは、QUERYの変数部分 "[カラム名]" を、"?" に置き換えており、
511         * この、カラム名の現れた順番に、配列として返します。
512         * データベース処理では、パラメータを設定する場合に、このカラム名を取得し、
513         * オリジナル(SELECT)のカラム番号から、その値を取得しなければなりません。
514         *
515         * カラム名配列は、QUERYタイプ(queryType)に応じて作成されます。
516         * SELECT : パラメータ は使わないので、長さゼロの配列
517         * INSERT : where条件は使わず、names部分のみなので、0 ~ clmLen までの配列
518         * UPDATE : names も、where条件も使うため、すべての配列
519         * DELETE : names条件は使わず、where部分のみなので、clmLen ~ clmLen+whrLen までの配列(clmLen以降の配列)
520         *
521         * @og.rev 6.8.6.0 (2018/01/19) 新規作成
522         * @og.rev 6.9.8.0 (2018/05/28) セットアップチェックが漏れていた。
523         *
524         * @param       useInsert       queryType="MERGE" の場合に、false:UPDATE , true:INSERT のパラメータのカラム名配列を返します。
525         * @return      パラメータとなるカラム名の配列
526         * @og.rtnNotNull
527         */
528        public String[] getParamNames( final boolean useInsert ) {
529                // 6.9.8.0 (2018/05/28) FindBugs:コンストラクタで初期化されていないフィールドを null チェックなしで null 値を利用している
530                // queryType と、nameAry は、setup() メソッドで設定されるため、FindBugs の指摘は、対応済みとなります。
531                // とりあえず、条件判定を入れておいて、FindBugs の警告が出ないようにしておきます。
532
533                // 6.9.8.0 (2018/05/28) セットアップチェックが漏れていた。
534                if( !isSetup || StringUtil.isNull( queryType ) || nameAry == null ) {
535                        final String errMsg = "getParamNames(boolean) は、SQL文を取得してから、行ってください。";
536                        throw new OgRuntimeException( errMsg );
537                }
538
539                final String[] whrAry = whrList.toArray( new String[whrList.size()] );
540                final String[] allAry = Arrays.copyOf( nameAry , nameAry.length + whrList.size() );
541                System.arraycopy( whrAry , 0 , allAry , nameAry.length , whrAry.length );               // allAry = nameAry + whrAry の作成
542
543                String[] rtnClms = null;
544                switch( queryType ) {
545                        case "SELECT" : rtnClms = new String[0];        break;          // パラメータはない。
546                        case "INSERT" : rtnClms = nameAry;                      break;          // names指定の分だけ、パラメータセット
547                        case "UPDATE" : rtnClms = allAry;                       break;          // names+whereの分だけ、パラメータセット
548                        case "DELETE" : rtnClms = whrAry;                       break;          // whereの分だけ、パラメータセット
549                        case "MERGE"  : rtnClms = allAry;                       break;          // useInsert=false は、UPDATEと同じ
550                        default : break;
551                }
552
553                if( useInsert && "MERGE".equals( queryType ) ) {
554                        rtnClms = nameAry;                                      // MERGEで、useInsert=true は、INSERTと同じ
555                }
556
557                return rtnClms;
558        }
559}