001/*
002 * Copyright (c) 2017 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.fileexec;
017
018import java.util.Locale;
019import java.util.Date;
020import java.text.DateFormat;
021import java.text.SimpleDateFormat;
022
023/**
024 * StringUtilは、共通的に使用される文字列処理に関する、ユーティリティークラスです。
025 *
026 * @og.rev 7.0.0.0 (2017/07/07) 新規作成
027 *
028 * @version  7.0
029 * @author   Kazuhiko Hasegawa
030 * @since    JDK1.8,
031 */
032public final class StringUtil {
033        /** システム依存の改行記号(String)。        */
034        public static final String CR = System.getProperty("line.separator");
035
036        private static final int        BUFFER_MIDDLE    = 200 ;        // 7.2.9.5 (2020/11/28)
037
038        /**
039         * デフォルトコンストラクターをprivateにして、
040         * オブジェクトの生成をさせないようにする。
041         */
042        private StringUtil() {}
043
044        /**
045         * 指定の文字列が nullか、ゼロ文字列 の場合は、2番目以降の引数から、null でない最初の値を返します。
046         * 2番目以降の引数には、ゼロ文字列の指定も可能です。
047         *
048         * @param       val 判定する文字列
049         * @param       def 初期値の可変長引数
050         * @return      指定の値で、最初にnullでない値。最後まで、無ければ、val を返します。
051         */
052        public static String nval( final String val , final String... def ) {
053                // 8.5.5.1 (2024/02/29) PMD 7.0.0 OnlyOneReturn メソッドには終了ポイントが 1 つだけ必要
054                String rtn = val;
055
056                // 8.5.4.2 (2024/01/12) PMD 7.0.0 InefficientEmptyStringCheck 対応
057//              if( val == null || val.trim().isEmpty() ) {
058                if( val == null || val.isBlank() ) {                            // fukurou.util.StringUtil#isNull(String) を使いたくなかった。
059                        for( final String str : def ) {
060//                              if( str != null ) { return str.trim(); }        // ゼロ文字列 の場合も、返します。
061                                if( str != null ) {                                                     // ゼロ文字列 の場合も、返します。
062//                                      return str.trim();
063                                        rtn = str.trim();
064                                        break;
065                                }
066                        }
067                }
068
069//              return val;             // 最後まで null以外の値が見つからない場合なので、val が null の場合もありうる。
070                return rtn;             // 最後まで null以外の値が見つからない場合なので、val が null の場合もありうる。
071        }
072
073        /**
074         * 指定の数値型文字列が nullか、ゼロ文字列 の場合は、2番目の初期値を返します。
075         *
076         * そうでない場合は、指定の文字列を、Integer.parseInt( String ) した値を返します。
077         *
078         * @param       val 判定する数値型文字列
079         * @param       def 初期値の数値
080         * @return      指定の文字列が nullか、ゼロ文字列でない場合は、数値に変換し、そうでなければ、初期値の数値を返します。
081         * @throws      NumberFormatException   文字列が解析可能な整数型を含まない場合。
082         */
083        public static int nval( final String val , final int def ) {
084                // 8.5.4.2 (2024/01/12) PMD 7.0.0 InefficientEmptyStringCheck 対応
085//              return val == null || val.trim().isEmpty() ? def : Integer.parseInt( val.trim() );
086                // fukurou.util.StringUtil#isNull(String) を使いたくなかった。
087                return val == null || val.isBlank() ? def : Integer.parseInt( val.trim() );
088        }
089
090        /**
091         * LOGファイルのフォーマットに対応した日付、時刻文字列を作成します。
092         *
093         * yyyyMMddHHmmss 形式のフォーマットで、返します。
094         *
095         * @return      現在の日付、時刻文字列
096         */
097        public static String getTimeFormat() {
098                return getTimeFormat( "yyyyMMddHHmmss" );
099        }
100
101        /**
102         * 指定のフォーマットに対応した日付、時刻文字列を作成します。
103         *
104         * 例えば、LOGフォーマットの場合は、yyyyMMdd HH:mm:ss 形式です。
105         *
106         * @param format        日付、時刻文字列のフォーマット
107         * @return      指定のフォーマットに対応した現在の日付、時刻文字列
108         */
109        public static String getTimeFormat( final String format ) {
110                final DateFormat formatter = new SimpleDateFormat( format , Locale.getDefault() );
111                return formatter.format( new Date() );
112        }
113
114        /**
115         * 指定のフォーマットに対応した日付、時刻文字列を作成します。
116         *
117         * 例えば、LOGフォーマットの場合は、yyyyMMdd HH:mm:ss 形式です。
118         *
119         * @param date          フォーマットする日付情報
120         * @param format        日付、時刻文字列のフォーマット
121         * @return      指定のフォーマットに対応した指定の日付、時刻文字列
122         */
123        public static String getTimeFormat( final long date , final String format ) {
124                final DateFormat formatter = new SimpleDateFormat( format , Locale.getDefault() );
125                return formatter.format( new Date( date ) );
126        }
127
128        /**
129         * EUROMAPの日付、時間文字列から、openGion系日付時刻文字列を作成します。
130         *
131         * 日付は、yyyyMMdd 形式で、時間は、HH:mm:ss 形式を標準としますが、
132         * 数字以外の文字列を削除して、連結した文字列を作成します。
133         * その場合、14桁数字文字列 になります。
134         *
135         * その形式に合わない場合は、現在時刻 を返します。
136         * ただし、日付の整合性チェックは、行っていません。
137         *
138         * @og.rev 7.0.5.1 (2019/09/27) 日付指定がない場合に、無条件に1秒待つ仕様を廃止。ロジックミス修正
139         * @og.rev 7.2.5.0 (2020/06/01) 日付と時間を分けて取得します。
140         * @og.rev 7.4.1.0 (2021/04/23) 1秒待つ仕様を復活。あくまで現在時刻を設定した場合のみ待ちます。
141         *
142         * @param       ymd EUROMAPの日付(yyyyMMdd形式の8桁数字)
143         * @param       hms EUROMAPの時間(HH:mm:ss形式の8桁数字)
144         * @return      openGion系日付時刻文字列(yyyyMMddHHmmss形式の14桁数字)
145         */
146        public static String getTime( final String ymd , final String hms ) {
147                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
148
149                // yyyyMMdd形式のはずだが、数字以外を削除する。
150                // 7.4.1.0 (2021/04/23) 仕様変更。null 出なければ、数字項目を削除してから、桁数を判定する。
151//              if( ymd != null && ymd.length() == 8 ) {
152                if( ymd != null ) {
153                        for( int i=0; i<ymd.length(); i++ ) {
154                                final char ch = ymd.charAt( i );
155                                if( '0' <= ch && ch <= '9' ) { buf.append( ch ); }      // 数字のみ許可
156                        }
157                }
158//              else {
159//                      buf.append( getTimeFormat( "yyyyMMdd" ) );      // 現在の年月日
160//              }
161
162                final int ymdLen = buf.length();
163                // 8桁以上の場合は、8桁に切る。
164                if( ymdLen > 8 ) {
165                        buf.setLength( 8 );
166                }
167                // 8桁未満の場合は、現在の年月日を後ろから埋める。つまり、年月しか指定されていなければ、日だけ現在になる。
168                else if( ymdLen < 8 ) {
169                        buf.append( getTimeFormat( "yyyyMMdd" ).substring( ymdLen ) );
170                }
171                // 8桁の場合は、日付として不正でも無条件で信頼する。
172
173                // HH:mm:ss形式のはずだが、数字以外を削除する。
174//              if( hms != null && hms.length() == 8 ) {
175                if( hms != null ) {
176                        for( int i=0; i<hms.length(); i++ ) {
177                                // HH:mm:ss形式から、':'を取り除く
178                                final char ch = hms.charAt( i );
179                                if( '0' <= ch && ch <= '9' ) { buf.append( ch ); }      // 数字のみ許可
180                        }
181                }
182//              else {
183//                      buf.append( getTimeFormat( "HHmmss" ) );        // 現在の時分秒
184//              }
185
186                final int ymdhmsLen = buf.length();
187                // 14桁以上の場合は、14桁に切る。
188                if( ymdhmsLen > 14 ) {
189                        buf.setLength( 14 );
190                }
191                // 14桁未満の場合は、現在の現在時刻を後ろから埋める。日付は埋めているので、日時の不足分を補完する。
192                else if( ymdhmsLen < 14 ) {
193                        buf.append( getTimeFormat().substring( ymdhmsLen ) );
194
195                        // 現在時刻を後ろから埋めた場合は、無条件に1秒待ちます。
196                        // これは、テスト用に日付無しのDATデータを用意して登録する場合に使用します。
197//                      try{ Thread.sleep( 1000 ); } catch( final InterruptedException ex ){}
198//                      try{ Thread.sleep( 1100 ); } catch( final InterruptedException ex ){}           // 7.4.4.0 (2021/06/30) 何となく重複エラーが出るので
199                        try { Thread.sleep( 1100 ); } catch( final InterruptedException ignored ) {}    // 8.5.4.2 (2024/01/12) PMD 7.0.0 EmptyCatchBlock
200                }
201
202//              // 14桁でなければ、現在時刻を設定します。日付の整合性チェックは行いません。
203//              if( buf.length() != 14 ) {
204//                      buf.append( getTimeFormat() );                          // 現在時刻
205//              }
206
207//              // 1秒待たないので、時刻が重複するとエラーになる可能性があります。
208                return buf.toString();
209        }
210
211//      public static String getTime( final String ymd , final String hms ) {
212//              if( ymd != null && hms != null ) {
213//                      if( ymd.length() == 8 && hms.length() == 8 ) {
214//                              return ymd + hms.substring(0,2) + hms.substring(3,5) + hms.substring(6);
215//                      }
216//                      else {
217//                              final StringBuilder buf = new StringBuilder();
218//                              for( int i=0; i<ymd.length(); i++ ) {
219//                                      final char ch = ymd.charAt( i );
220////                                    if( '0' <= ch && ch < '9' ) { buf.append( ch ); }       // 数字のみ許可
221//                                      if( '0' <= ch && ch <= '9' ) { buf.append( ch ); }      // 数字のみ許可               7.0.5.1 (2019/09/27)
222//                              }
223//
224//                              for( int i=0; i<hms.length(); i++ ) {
225//                                      final char ch = hms.charAt( i );
226////                                    if( '0' <= ch && ch < '9' ) { buf.append( ch ); }       // 数字のみ許可
227//                                      if( '0' <= ch && ch <= '9' ) { buf.append( ch ); }      // 数字のみ許可               7.0.5.1 (2019/09/27)
228//                              }
229//
230//                              if( buf.length() == 14 ) {
231//                                      return buf.toString();
232//                              }
233//                      }
234//              }
235//
236//              // 引数が存在しない場合は、無条件に1秒待ちます。
237//              // これは、テスト用に日付無しのDATデータを用意して登録する場合に使用します。
238////            try{ Thread.sleep( 1000 ); } catch( final InterruptedException ex ){}
239//
240//              return getTimeFormat() ;                // 現在時刻
241//      }
242
243        /**
244         * すべての引数をスペースで連結してtrim()した文字列を返します。
245         *
246         * 各引数は、それぞれ、trim()した後、スペースで連結します。
247         * ただし、引数が、nullの場合は、ゼロ文字列に変換します。
248         * すべてを連結して、trim() した結果が、ゼロ文字列の場合は、defVal を返します。
249         *
250         * @og.rev 7.2.5.0 (2020/06/01) 日付と時間を分けて取得します。
251         *
252         * @param       defVal 結果が、ゼロ文字列の場合に返す文字列
253         * @param       keys 連結したいキーの可変長文字列配列
254         * @return      合成した文字列。
255         */
256        public static String keyAppend( final String defVal , final String... keys ) {
257                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
258                for( final String key : keys ) {
259//                      buf.append( nval( key , "" ).trim() ).append( ' ' );
260                        final String key2 = nval( key , "" ).trim();
261                        if( !key2.isEmpty() ) { buf.append( key2 ).append( ' ' ); }             // keyがゼロ文字列でなければ連結する。
262                }
263
264                return nval( buf.toString().trim() , defVal );          // 最後のスペースを削除
265        }
266
267        /**
268         * 指定の文字列を、引数の文字で、前後に分割します。
269         * キー=値 や、キー,値 などの文字列を分割して、キーと値の配列を返します。
270         * 1番目の配列[0] は、キー(chの左側)、2番目の配列[1] は、値(chの右側) です。
271         * キーも値も、前後のスペースを削除:trim() します。
272         * キー(1番目の配列[0])は、空文字列を許可しません。
273         *
274//       * 分割文字を含まない場合や、キーが、空文字列の場合は、null を返します。
275         * 分割文字を含まない場合や、キーが、空文字列の場合は、長さゼロの配列 を返します。
276         * nullで無い場合は、配列は、必ず2個存在しそのどちらの値も、null を含みません。
277         *
278         * @og.rev 7.3.0.0 (2021/01/06) SpotBugs:null ではなく長さが0の配列を返すことを検討する
279         *
280         * @param       line    1行分の文字列
281         * @param       ch      分割する文字
282         * @return      キーと値の配列。無ければ、長さゼロの配列
283         * @og.rtnNotNull
284         */
285        public static String[] split( final String line , final char ch ) {
286                // 8.5.5.1 (2024/02/29) PMD 7.0.0 OnlyOneReturn メソッドには終了ポイントが 1 つだけ必要
287                String[] rtns = new String[0];
288
289                final int ad = line.indexOf( ch );
290                if( ad > 0 ) {          // ch が先頭は無視
291                        final String key = line.substring( 0,ad ).trim();
292
293                        if( !key.isEmpty() ) {
294                                final String val = line.substring( ad+1 ).trim();
295//                              return new String[] { key , val };
296                                rtns = new String[] { key , val };
297                        }
298                }
299//              return null;
300//              return new String[0];
301                return rtns;
302        }
303
304        /**
305         * 数字型文字列の一致をチェックします。
306         *
307         * 処理としては、数字型文字列以外を判定条件に入れても、同一文字列の場合は、
308         * true になります。
309         * ここでは、どちらかの文字列の末尾が、".0" などの場合に、同じかどうかを
310         * 数値に変換して、判定します。
311         * どちらかが、null の場合は、必ず false になります。両方とも、null でも false です。
312         *
313         * @param       val1 判定用の引数1
314         * @param       val2 判定用の引数2
315         * @return      判定結果(一致する場合は、true)
316         */
317        public static boolean numEquals( final String val1 , final String val2 ) {
318                if( val1 == null || val2 == null ) { return false; }
319                if( val1.equals( val2 ) ) { return true; }
320//              else {
321                        // 8.5.5.1 (2024/02/29) PMD 7.0.0 OnlyOneReturn メソッドには終了ポイントが 1 つだけ必要
322                        boolean flag = false;
323                        try {
324//                              return Double.parseDouble( val1 ) == Double.parseDouble( val2 );
325
326                                // 6.9.8.0 (2018/05/28) FindBugs:浮動小数点の等価性のためのテスト
327                                // FindBugs では、等価性のための比較は、if (Math.abs(x - y) < .0000001) が推奨されています。
328                                // return Math.abs( Double.parseDouble( val1 ) - Double.parseDouble( val2 ) ) < .0000001 ;
329//                              return Double.compare( Double.parseDouble( val1 ) , Double.parseDouble( val2 ) ) == 0 ;
330                                flag = Double.compare( Double.parseDouble( val1 ) , Double.parseDouble( val2 ) ) == 0 ;
331                        }
332                        catch( final NumberFormatException ex ) {
333                                System.err.println( ex.getMessage() );  // 8.0.0.0 (2021/07/31)
334//                              ;       // 数値変換できなかった場合は、無視します。
335                        }
336//              }
337
338//              return false;
339                return flag;
340        }
341
342        /**
343         * 変数の置き換え処理を行います。
344         *
345         * これは、org.opengion.fukurou.util.StringUtil#replaceText( String ,String ,String ,UnaryOperator&lt;String&gt; ) の簡易版です。
346         *
347         * これは、単純な変数ではなく、${env.XXX}または、{&#064;ENV..XXX} の XXX を環境変数に置き換えたり、
348         * {&#064;DATE.XXXX} を、日付文字列に置き換えたりする場合に、使用できます。
349         *
350         * fukurou.util.StringUtil#replaceText 版との違いは、1回のみの変換と、ENV と DATE を決め打ちで処理していることです。
351         * fileexec パッケージに、util  パッケージと同じクラス名のStringUtil を作ったのがそもそもの間違いで、
352         * fileexec パッケージ単独で使用していたものを、統合するにあたり、統合しきれなかったメソッドが
353         * 悪さしています。
354         *
355         * @og.rev 7.2.5.0 (2020/06/01) 新規追加
356         *
357         * @param  orgTxt 変換元の文字列
358         * @return  置換処理したテキスト
359         */
360        public static String replaceText( final String orgTxt ) {
361                // null か、ゼロ文字列か、'{' を含んでいない場合は、そのまま返します。
362                if( orgTxt == null || orgTxt.isEmpty() || orgTxt.indexOf( '{' ) < 0 ) { return orgTxt; }
363
364                final StringBuilder buf = new StringBuilder( orgTxt );
365
366                // 環境変数の処理
367                int st1 = buf.indexOf( "${env." );
368                if( st1 < 0 ) { st1 = buf.indexOf( "{@ENV." ); }                                // どちらか
369                if( st1 >= 0 ) {                                                                                                // 環境変数の処理がある
370                        final int ed1 = buf.indexOf( "}",st1+6 );
371                        final String envKey = buf.substring( st1+6,ed1 );
372                        final String envVal = nval( System.getenv( envKey ), "" );      // 未定義の場合は、ゼロ文字列
373                        buf.replace( st1,ed1+1,envVal );
374                }
375                else {                                                                                                                  // どちらか
376                        // 日付文字列の処理
377                        st1 = buf.indexOf( "{@DATE." );
378                        if( st1 >= 0 ) {                                                                                        // 日付文字列の処理がある
379                                final int ed1 = buf.indexOf( "}",st1+7 );
380                                final String dateKey = buf.substring( st1+7,ed1 );
381                                final String dateVal = getTimeFormat( dateKey );                // 未定義の場合は、ゼロ文字列
382                                buf.replace( st1,ed1+1,dateVal );
383                        }
384                }
385
386                return buf.toString();
387        }
388}