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.ResourceBundle;
019import java.util.PropertyResourceBundle;
020import java.util.Locale;
021import java.util.Arrays;
022import java.text.MessageFormat;
023
024import java.io.InputStream;
025import java.io.InputStreamReader;
026import java.io.BufferedReader;
027import java.io.IOException;
028import java.net.URL;
029import java.net.URLConnection;
030
031import static java.nio.charset.StandardCharsets.UTF_8;
032
033/**
034 * MsgUtilは、共通的に使用されるリソースからメッセージを作成する、ユーティリティークラスです。
035 *
036 *<pre>
037 * 現状は、{@value F_BS_NM}.properties ファイルをリソースとして使用します。
038 * このリソースファイルを、各言語別に作成することで、アプリケーションのメッセージを国際化できます。
039 * 通常のリソース変換以外に、キーワードと引数で、RuntimeException を返す簡易メソッドも提供します。
040 *
041 *</pre>
042 * @og.rev 7.0.0.0 (2017/07/07) 新規作成
043 *
044 * @version  7.0
045 * @author   Kazuhiko Hasegawa
046 * @since    JDK1.8,
047 */
048public final class MsgUtil {
049        private static final XLogger LOGGER= XLogger.getLogger( MsgUtil.class.getName() );              // ログ出力
050
051        /** 初期設定されているリソースバンドルのbaseName {@value} */
052        public static final String F_BS_NM = "org.opengion.fukurou.message" ;
053
054        private static final int        BUFFER_MIDDLE    = 200 ;
055        private static final int        STACKTRACE_COUNT = 5 ;
056
057        /**
058         * デフォルトコンストラクターをprivateにして、
059         * オブジェクトの生成をさせないようにする。
060         */
061        private MsgUtil() {}
062
063        /**
064         * "jp.euromap.eu63.message" の、Locale.getDefault() リソースから取得するメッセージを文字列で返します。
065         *
066         * id と引数を受け取り、ResourceBundle と、MessageFormat.format で加工した
067         * 文字列を返します。
068         * baseName は、F_BS_NM で、Locale に、Locale.getDefault() を指定したメッセージを作成します。
069         *
070         * @og.rev 6.4.3.1 (2016/02/12) 新規追加
071         * @og.rev 6.8.1.5 (2017/09/08) LOGGER.debug 情報の追加
072         *
073         * @param id    リソースのキーとなるID。
074         * @param args  リソースを、MessageFormat.format で加工する場合の引数。
075         *
076         * @return MessageFormat.formatで加工された文字列
077         * @see         #F_BS_NM
078         */
079        public static String getMsg( final String id , final Object... args ) {
080
081                // リソースバンドルのすべてがキャッシュに格納される・・・はず。
082                final ResourceBundle resource = ResourceBundle.getBundle( F_BS_NM , Locale.getDefault() , UTF8_CONTROL );
083
084                try {
085                        return id + ":" + MessageFormat.format( resource.getString( id ) , args );
086                }
087                catch( final RuntimeException ex ) {
088                        final String errMsg = id + "[" + Arrays.toString ( args ) + "]" ;
089                        LOGGER.warning( ex , () -> "【WARNING】 " + errMsg );
090                        return errMsg ;
091                }
092        }
093
094        /**
095         * メッセージを作成して、RuntimeExceptionの引数にセットして、throw します。
096         *
097         * @og.rev 6.4.3.1 (2016/02/12) 新規追加
098         *
099         * @param id    リソースのキーとなるID。
100         * @param args  リソースを、MessageFormat.format で加工する場合の引数。
101         * @return              メッセージを書き込んだ、RuntimeException
102         *
103         * @see         #getMsg( String,Object... )
104         * @see         #throwException( Throwable,String,Object... )
105         */
106        public static RuntimeException throwException( final String id , final Object... args ) {
107                return throwException( null , id , args );
108        }
109
110        /**
111         * メッセージを作成して、RuntimeExceptionの引数にセットして、throw します。
112         *
113         * @og.rev 6.4.3.1 (2016/02/12) 新規追加
114         * @og.rev 6.8.1.5 (2017/09/08) LOGGER.debug 情報の追加
115         *
116         * @param th    発生元のThrowable( null値は許容されます )
117         * @param id    リソースのキーとなるID。
118         * @param args  リソースを、MessageFormat.format で加工する場合の引数。
119         * @return              メッセージを書き込んだ、RuntimeException
120         *
121         * @see         #getMsg( String,Object... )
122         * @see         #throwException( String,Object... )
123         */
124        public static RuntimeException throwException( final Throwable th , final String id , final Object... args ) {
125                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
126                        .append( getMsg( id , args ) );
127
128                if( th != null ) {
129                        buf.append( "\n\t" ).append( th.getMessage() );
130                }
131
132                // ラムダ式で、Exception が throw された場合、上位にアップされない。(非検査例外(RuntimeException系)なら、スローできる・・・はず)
133                // 原因がわかるまで、とりあえず、printStackTrace しておきます。
134                final String errMsg = buf.toString();
135                final RuntimeException ex = new RuntimeException( errMsg , th );
136                LOGGER.warning( ex , () -> "【WARNING】 " + errMsg );
137                return ex;
138        }
139
140        /**
141         * エラーメッセージを作成して、LOGGER で出力します。
142         *
143         * @og.rev 6.4.3.1 (2016/02/12) 新規追加
144         *
145         * @param id    リソースのキーとなるID。
146         * @param args  リソースを、MessageFormat.format で加工する場合の引数。
147         * @return 作成されたエラーメッセージ文字列
148         *
149         * @see         #getMsg( String,Object... )
150         */
151        public static String errPrintln( final String id , final Object... args ) {
152                return errPrintln( null , id , args );
153        }
154
155        /**
156         * Throwable付きのエラーメッセージを作成して、LOGGER で出力します。
157         *
158         * @og.rev 6.4.3.1 (2016/02/12) 新規追加
159         *
160         * @param th    発生元のThrowable( null値は許容されます )
161         * @param id    リソースのキーとなるID。
162         * @param args  リソースを、MessageFormat.format で加工する場合の引数。
163         * @return 作成されたエラーメッセージ文字列
164         *
165         * @see         #getMsg( String,Object... )
166         */
167        public static String errPrintln( final Throwable th , final String id , final Object... args ) {
168                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE )
169                        .append( getMsg( id , args ) );
170
171                if( th != null ) {
172                        buf.append( "\n\t" ).append( th.getMessage() );
173
174                        int cnt = 0;
175                        for( final StackTraceElement stEle : th.getStackTrace() ) {
176                                final String clnNm = stEle.getClassName();
177
178                                if( clnNm.contains( "MsgUtil" ) ) { continue; }
179
180                                if( clnNm.contains( "jp.euromap.eu63" ) || cnt < STACKTRACE_COUNT ) {
181                                        buf.append( "\n\t" ).append( stEle.toString() );
182                                        cnt++;
183                                }
184                        }
185                }
186
187                LOGGER.warning( () -> "【WARNING】 " + buf.toString() );
188
189                return buf.toString();
190        }
191
192        /**
193         * ResourceBundle.Controlは、バンドル・ロード処理中にResourceBundle.getBundleファクトリによって呼び出される一連のコールバック・メソッドを定義します。
194         *
195         * @og.rev 6.4.3.1 (2016/02/12) 新規追加
196         */
197        private static final ResourceBundle.Control UTF8_CONTROL = new ResourceBundle.Control() {
198                /**
199                 * 指定された形式とロケールを持つ指定されたバンドル名のリソース・バンドルを、指定されたクラス・ローダーを必要に応じて使用してインスタンス化します。
200                 *
201                 * 指定されたパラメータに対応する使用可能なリソース・バンドルが存在しない場合、このメソッドはnullを返します。
202                 * 予想外のエラーが発生したためにリソース・バンドルのインスタンス化が行えない場合には、単純にnullを返す代わりに、
203                 * ErrorまたはExceptionをスローすることでエラーを報告する必要があります。 
204                 * reloadフラグがtrueの場合、それは、以前にロードされたリソース・バンドルの有効期限が切れたためにこのメソッドが呼び出されたことを示します。 
205                 *
206                 * @og.rev 6.4.3.1 (2016/02/12) 新規追加
207                 *
208                 * @param baseName      リソース・バンドルの基底バンドル名。完全指定クラス名
209                 * @param locale        リソース・バンドルのインスタンス化対象となるロケール
210                 * @param format        ロードされるリソース・バンドルの形式
211                 * @param loader        バンドルをロードするために使用するClassLoader
212                 * @param reload        バンドルの再ロードを示すフラグ。有効期限の切れたリソース・バンドルを再ロードする場合はtrue、それ以外の場合はfalse
213                 *
214                 * @return ResourceBundle.Controオブジェクト
215                 *
216                 * @throws NullPointerException                 bundleName、locale、format、またはloaderがnullの場合、またはtoBundleNameからnullが返された場合
217                 * @throws IllegalArgumentException             formatが不明である場合、または指定されたパラメータに対して見つかったリソースに不正なデータが含まれている場合。
218                 * @throws ClassCastException                   ロードされたクラスをResourceBundleにキャストできない場合
219                 * @throws IllegalAccessException               クラスまたはその引数なしのコンストラクタにアクセスできない場合。
220                 * @throws InstantiationException               クラスのインスタンス化が何かほかの理由で失敗する場合。
221                 * @throws ExceptionInInitializerError  このメソッドによる初期化に失敗した場合。
222                 * @throws SecurityException                    セキュリティ・マネージャが存在し、新しいインスタンスの作成が拒否された場合。詳細は、Class.newInstance()を参照してください。
223                 * @throws IOException                                  何らかの入出力操作を使ってリソースを読み取る際にエラーが発生した場合
224                 */
225                @Override
226                public ResourceBundle newBundle( final String baseName, 
227                                                                                 final Locale locale, 
228                                                                                 final String format, 
229                                                                                 final ClassLoader loader, 
230                                                                                 final boolean reload ) throws IllegalAccessException, InstantiationException, IOException {
231                        // The below is a copy of the default implementation.
232                        final String bundleName   = toBundleName( baseName , locale );
233                        final String resourceName = toResourceName( bundleName, "properties" );
234                        InputStream stream = null;
235                        if( reload ) {
236                                final URL url = loader.getResource( resourceName );
237                                if( url != null ) {
238                                        final URLConnection urlConn = url.openConnection();
239                                        if( urlConn != null ) {
240                                                urlConn.setUseCaches( false );
241                                                stream = urlConn.getInputStream();
242                                        }
243                                }
244                        } else {
245                                stream = loader.getResourceAsStream( resourceName );
246                        }
247
248                        ResourceBundle bundle = null;
249                        if( stream != null ) {
250                                try {
251                                        // Only this line is changed to make it to read properties files as UTF-8.
252                                        bundle = new PropertyResourceBundle( new BufferedReader( new InputStreamReader( stream,UTF_8 ) ) );
253                                } finally {
254                                        stream.close();
255                                }
256                        }
257                        return bundle;
258                }
259        };
260
261        /**
262         * リソース一覧を表示する main メソッドです。
263         *
264         * @param       args    コマンド引数配列
265         */
266        public static void main( final String[] args ) {
267                // ResourceBundle.getBundle は、キャッシュされる・・・はず。
268                final ResourceBundle resource = ResourceBundle.getBundle( F_BS_NM , Locale.getDefault() , UTF8_CONTROL );               // リソースバンドルのすべてがキャッシュに格納されます。
269
270                for( final String key : new java.util.TreeSet<String>( resource.keySet() ) ) {
271                        System.out.println( key + ":\t" + resource.getString( key ) );
272                }
273        }
274}