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