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.model;
017
018import java.util.ArrayList;
019import java.util.List;
020
021import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;      // 6.1.0.0 (2014/12/26) refactoring
022import org.opengion.fukurou.system.OgRuntimeException ;                         // 6.4.2.0 (2016/01/29)
023
024/**
025 * [PN],[OYA] などの [] で指定されたカラムで表されたフォーマットデータに対して、
026 * DataModel オブジェクトを適用して 各カラムに実データを割り当てるオブジェクトです。
027 *
028 * カラム名には、特殊カラム名が使用できます。これは、DataModel に存在しないカラム名
029 * ですが、値を返すことが出来ます。
030 * <pre>
031 * [KEY.カラム名] : 行番号付きカラム名
032 * [I]            : 行番号
033 * [J]            : 登録時の件数(1~)          7.3.1.3 (2021/03/09)
034 * [ROW.ID]       : 行毎のチェックボックスのID
035 * [ROW.JSON]     : 行毎の全データのJavaScriptオブジェクト形式 { key:val,key:val,... }
036 * カラムの前に修飾記号(#,$,!)を付けるとフォーマットを変更できます。
037 * ただし、FormatTextField 系 と FormatTable 系で、出力される形式が異なります。
038 *                  FormatTextField 系               FormatTable 系
039 * [#カラム名]    : TDなしのラベルと入力フィールド   ラベルを出力
040 * [$カラム名]    : TDなしの入力フィールドのみ       レンデラーを出力
041 * [!カラム名]    : TDなしの値のみ                   値を出力
042 *
043 * </pre>
044 * @og.group 画面表示
045 *
046 * @version  4.0
047 * @author   Kazuhiko Hasegawa
048 * @since    JDK5.0,
049 */
050// 8.5.5.1 (2024/02/29) spotbugs CT_CONSTRUCTOR_THROW(コンストラクタで、Excweptionを出さない) class を final にすれば、警告は消える。
051// public class Formatter {
052public final class Formatter {
053        /** カラムID(連結文字列)行番号の連結文字列を定義 {@value} */
054        public static final String JOINT_STRING = "__" ;
055
056        /** テーブル表示のチェックボックスを特定する id の 名称( id は、この名称+行番号) {@value} */
057        public static final String ROW_ID_KEY = "cb";   // 3.6.0.0 (2004/09/17)
058
059//      /** 特殊カラム名の定義: 行番号 [I]  */
060//      public static final int SYS_ROWNUM      = -1;           // [KEY.カラム名],[I],[ROW.ID]
061        /** 特殊カラム名の定義: [ROW.JSON]       */
062        public static final int SYS_JSON        = -2;           // [ROW.JSON]
063        /** 6.9.5.0 (2018/04/23) 特殊カラム名の定義: 行番号 [I]     */
064        public static final int SYS_ROWNUM      = -3;           // [KEY.カラム名],[I],[ROW.ID] 6.9.5.0 (2018/04/23) -1 は、カラム無しと同じなので、-3 に変更
065        /** 6.9.5.0 (2018/04/23) 特殊カラム名の定義: 行番号 [I]     */
066        public static final int SYS_CNT         = -4;           // 7.3.1.3 (2021/03/09) [J] は特殊記号で、登録時の件数(1~)
067        /** 特殊カラム名の定義: 非表示              */
068        public static final int NO_DISPLAY      = -9;           // 6.2.0.1 (2015/03/06) 非表示のマーカー
069
070        private final DataModel<?>      model   ;                       // 4.3.3.6 (2008/11/15) Generics警告対応
071        private int[]                           clmNos  ;                       // フォーマットのカラム番号配列
072        private String[]                        format  ;
073        private String[]                        clmKeys ;                       // フォーマットのカラム名配列
074        private String[]                        clmPrms ;                       // 6.8.3.1 (2017/12/01) フォーマットのカラムのパラメータ
075        private char[]                          type    ;                       // '#':ラベルのみ  '$':レンデラー '!':値のみ  その他:通常
076
077        /**
078         * データモデルとフォーマットを指定してフォーマッターを構築します。
079         *
080         * @og.rev 6.4.3.4 (2016/03/11) Formatterに新しいコンストラクターを追加する。
081         *
082         * @param       model データモデル
083         * @param       fmt  [カラム名]形式のフォーマットデータ
084         */
085        public Formatter( final DataModel<?> model , final String fmt ) {
086                this.model = model;
087                makeFormatList( fmt );
088                advanceFormat();
089        }
090
091        /**
092         * フォーマットをセットします。
093         *
094         * @og.rev 6.8.3.1 (2017/12/01) [$XXXX param] で、RendererValueのパラメータを動的に変更できます。
095         *
096         * @param       fmt  [カラム名]形式のフォーマットデータ
097         */
098        private void makeFormatList( final String fmt ) {
099                int start = 0;
100                int index = fmt.indexOf( '[' );
101                final List<String> formatList = new ArrayList<>();
102                final List<String> clmKeyList = new ArrayList<>();
103                while( index >= 0 ) {
104                        final int end = fmt.indexOf( ']',index );
105                        if( end < 0 ) {
106                                final String errMsg = "[ と ] との対応関係がずれています。"
107                                                                + "format=[" + fmt + "] : index=" + index ;
108                                throw new OgRuntimeException( errMsg );
109                        }
110
111                        // [ より前方の文字列は、formatList へ
112                        if( index > 0 ) { formatList.add( fmt.substring( start,index ) ); }
113                        else                    { formatList.add( "" ); }       // ][ と連続しているケース
114
115                        // [XXXX] の XXXX部分を処理
116                        clmKeyList.add( fmt.substring( index+1,end ) );
117
118                        start = end+1 ;
119                        index = fmt.indexOf( '[',start );
120                }
121                // ] の後方部分は、formatList へ
122                formatList.add( fmt.substring( start ) );
123
124//              format  = formatList.toArray( new String[formatList.size()] );
125//              clmKeys = clmKeyList.toArray( new String[clmKeyList.size()] );
126                format  = formatList.toArray( new String[0] );  // 8.5.4.2 (2024/01/12) PMD 7.0.0 OptimizableToArrayCall 対応
127                clmKeys = clmKeyList.toArray( new String[0] );  // 8.5.4.2 (2024/01/12) PMD 7.0.0 OptimizableToArrayCall 対応
128                clmPrms = new String[clmKeyList.size()];                // 6.8.3.1 (2017/12/01)
129        }
130
131        /**
132         * 追加機能フォーマットを作成します。
133         *
134         * @og.rev 6.8.3.1 (2017/12/01) [$XXXX param] で、RendererValueのパラメータを動的に変更できます。
135         * @og.rev 7.3.1.3 (2021/03/09) [J] で、登録件数(1~) を表現する。
136         */
137        private void advanceFormat() {
138                final int size = clmKeys.length ;
139                clmNos = new int[size];
140                type   = new char[size];
141
142                // カラム番号の設定と、特殊カラム名処理
143                String clm ;
144                for( int i=0; i<size; i++ ) {
145                        clm = clmKeys[i];
146                        final char ch = clm.charAt(0);
147                        if( ch == '#' || ch == '$' || ch == '!' ) {
148                                type[i] = ch;
149                                clm = clm.substring(1);
150                                // 6.8.3.1 (2017/12/01) [$XXXX param] 対応。
151                                final int sp = clm.indexOf( ' ' );              // スペース分割
152                                if( sp > 0 ) {
153                                        clmPrms[i] = clm.substring( sp+1 );     // 先にパラメータを取得
154                                        clm = clm.substring( 0,sp );
155                                }
156                                clmKeys[i] = clm;
157                                clmNos[i]  = model.getColumnNo( clm );  // 指定されたセルのカラム番号。存在しなければ、-1
158                        }
159                        // [KEY.カラム名] 機能追加
160                        else if( clm.startsWith( "KEY." ) ) {
161                                clmNos[i] = SYS_ROWNUM;
162                                format[i] = format[i] + clm.substring(4) + JOINT_STRING ;
163                        }
164                        // [I] 機能追加
165                        else if( "I".equals( clm ) ) {
166                                clmNos[i] = SYS_ROWNUM;
167                        }
168                        // 7.3.1.3 (2021/03/09) [J] 機能追加
169                        else if( "J".equals( clm ) ) {
170                                clmNos[i] = SYS_CNT;
171                        }
172                        // [ROW.ID] 機能追加
173                        else if( "ROW.ID".equals( clm ) ) {
174                                clmNos[i] = SYS_ROWNUM;
175                                format[i] = format[i] + ROW_ID_KEY ;
176                        }
177                        // [ROW.JSON] 機能追加
178                        else if( "ROW.JSON".equals( clm ) ) {
179                                clmNos[i] = SYS_JSON;
180                        }
181                        else {
182                                clmNos[i] = model.getColumnNo( clm );   // 指定されたセルのカラム番号。存在しなければ、-1
183                        }
184                }
185        }
186
187        /**
188         * column にあるセルの属性値をStringに変換して返します。
189         *
190         * ここでは、[KEY.カラム名] , [I] , [J] , [ROW.ID] は同じ値(row)を返します。
191         *
192         * @og.rev 6.4.3.2 (2016/02/19) 値が null の場合は、ゼロ文字列を設定する。
193         * @og.rev 7.3.1.3 (2021/03/09) [J] で、登録件数(1~) を表現する。
194         *
195         * @param       row     処理中の行番号
196         * @param       clm     値が参照されるカラム番号
197         *
198         * @return      指定されたセルの値
199         *
200         */
201        public String getValue( final int row,final int clm ) {
202                final String rtn ;
203                if( clm >= 0 ) {
204                        // 6.4.3.2 (2016/02/19) 値が null の場合は、ゼロ文字列を設定する。
205                        final Object obj = model.getValue( row,clm );
206                        rtn = obj == null ? "" : String.valueOf( obj );
207                }
208//              else if( clm == SYS_ROWNUM ) {
209                else if( clm == SYS_ROWNUM || clm == SYS_CNT ) {        // 7.3.1.3 (2021/03/09)
210                        rtn = String.valueOf( row );
211                }
212                else if( clm == SYS_JSON ) {
213                        rtn = getJson( row );
214                }
215                else if( clm == NO_DISPLAY ) {          // 7.3.1.3 (2021/03/09) ここで指定されるかどうか不明だが、入れておきます。
216                        rtn = "";
217                }
218                else {
219                        final String errMsg = "指定のカラム番号に該当する処理が見つかりません。"
220                                                        + "clm=[" + clm + "]" ;
221                        throw new OgRuntimeException( errMsg );
222                }
223
224                return rtn ;
225        }
226
227        /**
228         * 指定の 行番号に対する、DataModel を元に作成したフォーマット文字列を返します。
229         *
230         * @og.rev 7.2.9.0 (2020/10/12) type(#,$,!)を考慮した関数型I/F対応
231         *
232         * @param       row     行番号( [I]フォーマット処理用 )
233         *
234         * @return  指定のObject配列を元に作成したフォーマット文字列
235         * @og.rtnNotNull
236         */
237        public String getFormatString( final int row ) {
238//              return getFormatString( row, null );
239                return getFormatString( row, null, (val,typ,prm) -> val );
240        }
241
242        /**
243         * 指定の 行番号に対する、DataModel を元に作成したフォーマット文字列を返します。
244         * データはseparatorで指定された区切り文字で囲まれて返されます。
245         *
246         * @og.rev 7.2.9.0 (2020/10/12) type(#,$,!)を考慮した関数型I/F対応
247         *
248         * @param       row                     行番号( [I]フォーマット処理用 )
249         * @param       separator       セパレーター
250         *
251         * @return  指定のObject配列を元に作成したフォーマット文字列
252         * @og.rtnNotNull
253         */
254        public String getFormatString( final int row, final String separator ) {
255//              return getFormatString( row, null );
256                return getFormatString( row, separator, (val,typ,prm) -> val );
257        }
258
259        /**
260         * 指定の 行番号に対する、DataModel を元に作成したフォーマット文字列を返します。
261         * データはseparatorで指定された区切り文字で囲まれて返されます。
262         *
263         * ここでは、[KEY.カラム名] , [I] , [J] , [ROW.ID] は同じ値(row)を返します。
264         *
265         * @og.rev 4.3.1.1 (2008/08/23) switch に、default label が存在しないため、追加
266         * @og.rev 6.4.3.2 (2016/02/19) 値が null の場合は、ゼロ文字列を設定する。
267         * @og.rev 7.2.9.0 (2020/10/12) type(#,$,!)を考慮した関数型I/F対応
268         * @og.rev 7.3.1.3 (2021/03/09) [J] で、登録件数(1~) を表現する。
269         * @og.rev 8.5.5.1 (2024/02/29) switch文にアロー構文を使用
270         *
271         * @param       row                     行番号( [I]フォーマット処理用 )
272         * @param       separator       セパレーター
273         * @param       triFunc         TriFunction関数
274         *
275         * @return  内部のDataModelを元に作成したフォーマット文字列
276         * @og.rtnNotNull
277         */
278//      public String getFormatString( final int row, final String separator ) {
279        public String getFormatString( final int row, final String separator, final TriFunction<String,Character,String,String> triFunc ) {
280                final StringBuilder rtnStr = new StringBuilder( BUFFER_MIDDLE );
281
282                final int count = clmNos.length;
283                for( int i=0; i<count; i++ ) {
284                        final int clm = clmNos[i];                                      // 7.3.1.3 (2021/03/09)
285                        rtnStr.append( format[i] );
286//                      if( clmNos[i] == SYS_ROWNUM ) {
287                        if( clm == SYS_ROWNUM || clm == SYS_CNT ) {     // 7.3.1.3 (2021/03/09)
288                                rtnStr.append( row );
289                        }
290                        else if( clm == SYS_JSON ) {
291                                rtnStr.append( getJson( row ) );
292                        }
293                        else if( clm == NO_DISPLAY ) {          // 7.3.1.3 (2021/03/09) ここで指定されるかどうか不明だが、入れておきます。
294                                // なにも append しない = 空文字列を追加と同じ
295                                continue;               // 8.5.4.2 (2024/01/12) PMD 7.0.0 EmptyControlStatement
296                        }
297                        else {
298                                // 6.4.3.2 (2016/02/19) 値が null の場合は、ゼロ文字列を設定する。
299                                final Object obj = model.getValue( row,clm );
300                                final String val = obj == null ? "" : String.valueOf( obj );
301
302                                if( separator == null || separator.isEmpty() ) {
303//                                      rtnStr.append( val );
304                                        // separator の使い方がおかしい。
305                                        rtnStr.append( triFunc.apply( val,type[i],clmPrms[i] ) );               // 7.2.9.0 (2020/10/12)
306                                }
307                                else {
308                                        // 4.3.1.1 (2008/08/23) default label が存在しないため、追加
309                                        // 8.5.5.1 (2024/02/29) switch文にアロー構文を使用
310//                                      switch( model.getNativeType( clm ) ) {
311//                                              case INT:
312//                                              case LONG:
313//                                              case DOUBLE:
314//                                                      rtnStr.append( val );
315//                                                      break;
316//                                              case STRING:
317//                                              case CALENDAR:
318//                                                      rtnStr.append( separator ).append( val ).append( separator );
319//                                                      break;
320//                                              default:
321//                                                      throw new AssertionError( "Unexpected enumrated value! " + model.getNativeType( clm ) );
322//                                      }
323                                        switch( model.getNativeType( clm ) ) {
324                                                case INT, LONG, DOUBLE -> rtnStr.append( val );
325                                                case STRING, CALENDAR  -> rtnStr.append( separator ).append( val ).append( separator );
326                                                default -> {
327                                                        throw new AssertionError( "Unexpected enumrated value! " + model.getNativeType( clm ) );
328                                                }
329                                        }
330                                }
331                        }
332                }
333                rtnStr.append( format[count] );
334
335                return rtnStr.toString();
336        }
337
338        /**
339         * 引数を3つ取る Function の 関数型インターフェースを定義します。
340         *
341         * @og.rev 7.2.9.0 (2020/10/12) type(#,$,!)を考慮した関数型I/F対応
342         *
343         * @param       <V>     引数1の総称型 変換前の値を想定
344         * @param       <T>     引数2の総称型 タイプ(#,$,!)を想定
345         * @param       <P>     引数3の総称型 パラメータ(%L,%Sなど)を想定
346         * @param       <R>     returnの総称型 Format後の値を想定
347         */
348        public interface TriFunction<V,T,P,R> {
349//      public static interface TriFunction<V,T,P,R> {
350                /**
351                 * 指定された引数にこの関数を適用します。
352                 *
353                 * @og.rev 7.2.9.0 (2020/10/12) type(#,$,!)を考慮した関数型I/F対応
354                 *
355                 * @param       val     引数1の総称型 変換前の値を想定
356                 * @param       typ     引数2の総称型 タイプ(#,$,!)を想定
357                 * @param       prm     引数3の総称型 パラメータ(%L,%Sなど)を想定
358                 * @return      returnの総称型 Format後の値を想定
359                 */
360                R apply( V val,T typ,P prm );
361        }
362
363        /**
364         * 引数の DataModel を元に作成したフォーマット文字列を返します。
365         * これは、簡易処理で、DataModel オブジェクトは、実質的には、LineModel です。
366         * パッケージの関連から、引数が、DataModel オブジェクトになっています。
367         *
368         * @og.rev 6.3.2.0 (2015/07/10) LineModelで、Formatter処理できるように、対応します。
369         *
370         * @param       model   DataModelオブジェクト(実質的には、LineModelオブジェクト)
371         *
372         * @return  引数のDataModelを元に作成したフォーマット文字列
373         * @og.rtnNotNull
374         */
375        public String getLineFormatString( final DataModel<Object> model ) {
376                final StringBuilder rtnStr = new StringBuilder( BUFFER_MIDDLE );
377
378                final int count = clmNos.length;
379                for( int i=0; i<count; i++ ) {
380                        rtnStr.append( format[i] )
381                                .append( model.getValue( 0,clmNos[i] ) );               // 行番号は、0 にしておきます。
382                }
383                rtnStr.append( format[count] );
384
385                return rtnStr.toString();
386        }
387
388        /**
389         * 先のフォーマット情報の[カラム名]を、クエスチョンマークに置き換えたフォーマットを返します。
390         *
391         * これは、java.sql.PreparedStatement 形式のQuery文字列を合成する場合に使用します。
392         *
393         * @return  PreparedStatement形式のQuery文字列
394         * @og.rtnNotNull
395         */
396        public String getQueryFormatString() {
397                final StringBuilder rtnStr = new StringBuilder( BUFFER_MIDDLE );
398
399                final int count = clmKeys.length;
400                for( int i=0; i<count; i++ ) {
401                        rtnStr.append( format[i] ).append( '?' );
402                }
403                rtnStr.append( format[count] );
404
405                return rtnStr.toString();
406        }
407
408        /**
409         * フォーマットのカラム名配列を返します。
410         *
411         * @return      フォーマットのカラム名配列
412         * @og.rtnNotNull
413         */
414        public String[] getClmKeys() {
415                return clmKeys.clone();
416        }
417
418        /**
419         * フォーマットのカラムのパラメータ配列を返します。
420         *
421         * [#XXX]、[$XXX]、[!XXX] で、カラムオブジェクトに渡すパラメータを、[$XXX param]形式で
422         * 指定できます。param 部分を、カラム配列と関連付けて、返します。
423         * 未設定のパラメータは、null が設定されています。
424         *
425         * @og.rev 6.8.3.1 (2017/12/01) [$XXXX param] で、RendererValueのパラメータを動的に変更できます。
426         *
427         * @return      フォーマットのパラメータ名配列
428         * @og.rtnNotNull
429         */
430        public String[] getClmPrms() {
431                return clmPrms.clone();
432        }
433
434        /**
435         * フォーマットの指定の位置のカラムのパラメータを返します。
436         *
437         * [#XXX]、[$XXX]、[!XXX] で、カラムオブジェクトに渡すパラメータを、[$XXX param]形式で
438         * 指定できます。param 部分を、カラム番号を指定することで、返します。
439         * 未設定のパラメータは、null が設定されています。
440         *
441         * @og.rev 6.8.3.1 (2017/12/01) [$XXXX param] で、RendererValueのパラメータを動的に変更できます。
442         *
443         * @param       ad パラメータのアドレス(カラムと同じ位置)
444         * @return      フォーマットのパラメータ
445         * @og.rtnNotNull
446         */
447        public String getClmParam( final int ad ) {
448                return ( ad >= 0 && ad < clmPrms.length ) ? clmPrms[ad] : null;
449        }
450
451        /**
452         * フォーマットのカラム番号配列を返します。
453         *
454         * @return      フォーマットのカラム番号配列
455         * @og.rtnNotNull
456         */
457        public int[] getClmNos() {
458                return clmNos.clone();
459        }
460
461        /**
462         * フォーマット配列を返します。
463         *
464         * @return      フォーマット配列
465         * @og.rtnNotNull
466         */
467        public String[] getFormat() {
468                return format.clone();
469        }
470
471        /**
472         * タイプ文字列配列を返します。
473         * タイプとは、[XXX] の記述で、[#XXX] は、XXXカラムのラベルを、[$XXX]は、XXXカラムの
474         * レンデラーを、[!XXX] は、値のみ取り出す指定を行います。
475         * 主に、TextField系のフォーマットとTable系では、意味合いが異なりますので、
476         * ご注意ください。
477         *
478         * @return タイプ文字列配列 '#':ラベルのみ  '$':レンデラー '!':値のみ  その他:通常
479         * @og.rtnNotNull
480         */
481        public char[] getType() {
482                return type.clone();
483        }
484
485        /**
486         * 行毎の全データのJavaScriptオブジェクト形式 を返します。
487         *
488         * JavaScriptオブジェクト形式とは、{ key:val,key:val,... }の形式で、
489         * ここでは、内部設定された DataModel のすべてのカラム名をキーに、
490         * 引数で渡された 配列を 値として使用します。
491         *
492         * @og.rev 6.4.3.2 (2016/02/19) 値が null の場合は、ゼロ文字列を設定する。
493         *
494         * @param       row     (DataModelの)行番号
495         *
496         * @return  指定の行番号に対応する全データのJSON形式データ
497         * @og.rtnNotNull
498         */
499        public String getJson( final int row ) {
500                final String[] names = model.getNames();
501                final Object[] vals  = model.getValues( row );
502
503                final StringBuilder rtnStr = new StringBuilder( BUFFER_MIDDLE );
504
505                rtnStr.append( "{'I':'" ).append( row ).append( '\'' ); // 行番号
506
507                for( int i=0; i<names.length; i++ ) {
508                        // 6.4.3.2 (2016/02/19) 値が null の場合は、ゼロ文字列を設定する。
509                        rtnStr.append( ",'" ).append( names[i] ).append( "':'" )
510                                .append( vals[i] == null ? "" : vals[i] ).append( '\'' );
511                }
512                rtnStr.append( '}' );           // 6.0.2.5 (2014/10/31) char を append する。
513
514                return rtnStr.toString();
515        }
516}