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.util;
017
018import java.io.File;
019import java.io.StringWriter;
020import java.net.MalformedURLException;
021import java.net.URL;
022import java.net.URLClassLoader;
023import java.util.Arrays;
024import java.util.Map;
025import java.util.WeakHashMap;
026import java.util.Collections;                                                           // 6.4.3.1 (2016/02/12) refactoring
027import java.lang.reflect.InvocationTargetException;                     // 7.0.0.0
028
029// import java.security.AccessController;                                       // 6.1.0.0 (2014/12/26) findBugs
030// import java.security.PrivilegedAction;                                       // 6.1.0.0 (2014/12/26) findBugs
031
032import javax.tools.JavaCompiler;
033import javax.tools.StandardJavaFileManager;
034import javax.tools.ToolProvider;
035import javax.tools.JavaCompiler.CompilationTask;
036
037import static org.opengion.fukurou.system.HybsConst.CR;         // 6.1.0.0 (2014/12/26) refactoring
038import org.opengion.fukurou.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
039
040/**
041 * AutoCompile機能、HotDeploy機能を実現するためのクラスローダーです。
042 *
043 * AutoCompile機能は、クラスの動的コンパイルを行います。
044 * AutoCompile機能を有効にするには、コンストラクタで与えられるHybsLoaderConfigオブジェクトで、
045 * AutoCompileフラグをtrueにしておく必要があります。
046 *
047 * HotDeploy機能は、クラスの動的ロードを行います。
048 * HotDeploy機能を有効にするには、コンストラクタで与えられるHybsLoaderConfigオブジェクトで、
049 * HotDeployフラグをtrueにしておく必要があります。
050 *
051 * (1)クラスの動的コンパイル
052 *  {@link #loadClass(String)}メソッドが呼ばれた場合に、ソースディレクトより、対象となるソースファイルを
053 *  検索し、クラスのコンパイルを行います。
054 *  コンパイルが行われる条件は、「クラスファイルが存在しない」または「クラスファイルのタイムスタンプがソースファイルより古い」です。
055 *
056 *  コンパイルを行うには、JDKに含まれるtools.jarが存在している必要があります。
057 *  tools.jarが見つからない場合、エラーとなります。
058 *
059 *  また、コンパイルのタスクのクラス(オブジェクト)は、JVMのシステムクラスローダー上のクラスに存在しています。
060 *  このため、サーブレットコンテナで、通常読み込まれるWEB-INF/classes,WEB-INF/lib以下のクラスファイルも、
061 *  そのままでは参照することができません。
062 *  これらのクラスを参照する場合は、HybsLoaderConfigオブジェクトに対してクラスパスを設定しておく必要があります。
063 *
064 * (2)クラスロード
065 *  クラスの動的ロードは、クラスローダーの入れ替えによって実現しています。
066 *  HotDeploy機能を有効にした場合、読み込むクラス単位にURLClassLoaderを生成しています。
067 *  クラスロードを行う際に、URLClassLoaderを新しく生成することで、クラスの再ロードを行っています。
068 *  つまり、HotDeployにより読み込まれるそれぞれのクラスは、お互いに独立した(平行な位置に存在する)関係に
069 *  なります。
070 *  このため、あるHotDeployによりロードされたクラスAから、同じくHotDeployによりロードされたクラスBを直接参照
071 *  することができません。
072 *  この場合は、クラスBのインターフェースを静的なクラスローダー(クラスAから参照できる位置)に配置することで、クラスB
073 *  のオブジェクトにアクセスすることができます。
074 *
075 * @og.rev 5.1.1.0 (2009/12/01) 新規作成
076 * @og.group 業務ロジック
077 *
078 * @version 5.0
079 * @author Hiroki Nakamura
080 * @since JDK1.6,
081 */
082public class HybsLoader {
083
084        /** HotDeploy機能を使用しない場合のURLClossLoaderのキャッシュキー */
085        private static final String CONST_LOADER_KEY = "CONST_LOADER_KEY";
086
087        private static final JavaCompiler COMPILER = ToolProvider.getSystemJavaCompiler();
088        private static final StandardJavaFileManager FILE_MANAGER = COMPILER.getStandardFileManager(null, null, null);
089
090        /** 6.4.3.1 (2016/02/12) Collections.synchronizedMap で同期処理を行います。 */
091        private final Map<String, HybsURLClassLoader> loaderMap = Collections.synchronizedMap( new WeakHashMap<>() );
092        /** 6.4.3.1 (2016/02/12) Collections.synchronizedMap で同期処理を行います。 */
093        private final Map<String, String> clsNameMap = Collections.synchronizedMap( new WeakHashMap<>() );
094        private final String    srcDir                  ;
095        private final String    classDir                ;
096        private final boolean   isHotDeploy             ;
097        private final boolean   isAutoCompile   ;
098        private final String    classPath               ;
099
100        /**
101         * HybsLoaderOptionを使用してHybsLoaderオブジェクトを生成します。
102         *
103         * @param option HybsLoaderを構築するための設定情報
104         */
105        public HybsLoader( final HybsLoaderConfig option ) {
106                srcDir                  = option.getSrcDir()    ;
107                classDir                = option.getClassDir()  ;
108                isHotDeploy             = option.isHotDeploy()  ;
109                isAutoCompile   = option.isAutoCompile();
110                classPath               = option.getClassPath() ;
111        }
112
113        /**
114         * 指定されたクラス名のクラスをロードします。
115         * クラス名については、クラス自身の名称のみを指定することができます。
116         * (パッケージ名を含めた完全な形のクラス名を指定することもできます)
117         *
118         * @og.rev 6.9.7.0 (2018/05/14) 中間変数を用意せず、直接返します。
119         *
120         * @param clsNm クラス名
121         * @return クラス
122         */
123        public Class<?> load( final String clsNm ) {
124                final String clsName = getQualifiedName( clsNm );
125                if( isAutoCompile ) {
126                        compileClass( clsName );
127                }
128//              final Class<?> cls = loadClass( clsName );
129
130//              return cls;
131                return loadClass( clsName );                            // 6.9.7.0 (2018/05/14)
132        }
133
134        /**
135         * 指定されたクラス名のクラスをロードし、デフォルトコンストラクターを使用して
136         * インスタンスを生成します。
137         *
138         * @og.rev 5.1.8.0 (2010/07/01) Exceptionのエラーメッセージの修正(状態の出力)
139         * @og.rev 6.8.2.3 (2017/11/10) java9対応(cls.newInstance() → cls.getDeclaredConstructor().newInstance())
140         *
141         * @param clsName クラス名(Qualified Name)
142         *
143         * @return インスタンス
144         */
145        public Object newInstance( final String clsName ) {
146                final Class<?> cls = load( clsName );
147                Object obj = null;
148                try {
149                        obj = cls.getDeclaredConstructor().newInstance();               // 7.0.0.0
150                }
151                catch( final InstantiationException | InvocationTargetException | NoSuchMethodException ex ) {                  // 6.8.2.3 (2017/11/10)
152                        final String errMsg = "インスタンスの生成に失敗しました。["  + clsName + "]" ;
153                        throw new OgRuntimeException( errMsg , ex );
154                }
155                catch( final IllegalAccessException ex ) {
156                        final String errMsg = "アクセスが拒否されました。["  + clsName + "]" ;
157                        throw new OgRuntimeException( errMsg , ex );
158                }
159                return obj;
160        }
161
162        /**
163         * クラス名より完全クラス名を検索します。
164         *
165         * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
166         * @og.rev 6.4.3.1 (2016/02/12) Collections.synchronizedMap に置き換え。
167         *
168         * @param clsNm クラス名
169         *
170         * @return 完全クラス名
171         */
172        private String getQualifiedName( final String clsNm ) {
173                String clsName = null;
174                if( clsNm.indexOf( '.' ) >= 0 ) {
175                        clsName = clsNm;
176                }
177                else {
178                                clsName = clsNameMap.get( clsNm );
179                                if( clsName == null ) {
180                                        clsName = findFile( "", clsNm );
181                                }
182                                if( clsName == null ) {
183                                        clsName = findFileByCls( "", clsNm );
184                                }
185                                clsNameMap.put( clsNm, clsName );
186
187                        if( clsName == null ) {
188                                throw new OgRuntimeException( "ソースファイルが存在しません。ファイル=[" + clsNm + "]" );
189                        }
190                }
191                return clsName;
192        }
193
194        /**
195         * クラス名に対応するJavaファイルを再帰的に検索します。
196         *
197         * @param path 既定パス
198         * @param nm クラス名
199         *
200         * @return 完全クラス名
201         */
202        private String findFile( final String path, final String nm ) {
203                // 8.5.5.1 (2024/02/29) PMD 7.0.0 OnlyOneReturn メソッドには終了ポイントが 1 つだけ必要
204                String rtn = null;
205
206                final String tmpSrcPath = srcDir + path;
207                final File[] files = new File( tmpSrcPath ).listFiles();
208                if( files != null && files.length > 0 ) {
209                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 ForLoopCanBeForeach
210//                      for( int i=0; i<files.length; i++ ) {
211//                              if( files[i].isDirectory() ) {
212//                                      final String rtn = findFile( path + files[i].getName() + File.separator, nm );
213                        for( final File clsFile : files ) {
214                                if( clsFile.isDirectory() ) {
215                                        final String find = findFile( path + clsFile.getName() + File.separator, nm );
216                                        if( find != null && find.length() > 0 ) {
217//                                              return find;
218                                                rtn = find;
219                                                break;
220                                        }
221                                }
222//                              else if( ( nm + ".java" ).equals( files[i].getName() ) ) {
223                                else if( ( nm + ".java" ).equals( clsFile.getName() ) ) {
224//                                      return path.replace( File.separatorChar, '.' ) + nm;
225                                        rtn = path.replace( File.separatorChar, '.' ) + nm;
226                                        break;
227                                }
228                        }
229                }
230//              return null;
231                return rtn;
232        }
233
234        /**
235         * クラス名に対応するJavaファイルを再帰的に検索します。
236         *
237         * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
238         *
239         * @param path 既定パス
240         * @param nm クラス名
241         *
242         * @return 完全クラス名
243         */
244        private String findFileByCls( final String path, final String nm ) {
245                // 8.5.5.1 (2024/02/29) PMD 7.0.0 OnlyOneReturn メソッドには終了ポイントが 1 つだけ必要
246                String rtn = null;
247
248                final String tmpSrcPath = classDir + path;
249                final File[] files = new File( tmpSrcPath ).listFiles();
250                if( files != null && files.length > 0 ) {
251                        // 8.5.4.2 (2024/01/12) PMD 7.0.0 ForLoopCanBeForeach
252//                      for( int i=0; i<files.length; i++ ) {
253//                              if( files[i].isDirectory() ) {
254//                                      final String rtn = findFile( path + files[i].getName() + File.separator, nm );
255                        for( final File clsfile : files ) {
256                                if( clsfile.isDirectory() ) {
257                                        final String find = findFile( path + clsfile.getName() + File.separator, nm );
258                                        if( find != null && find.length() > 0 ) {
259//                                              return find;
260                                                rtn = find;
261                                                break;
262                                        }
263                                }
264//                              else if( ( nm + ".class" ).equals( files[i].getName() ) ) {
265                                else if( ( nm + ".class" ).equals( clsfile.getName() ) ) {
266//                                      return path.replace( File.separatorChar, '.' ) + nm;
267                                        rtn = path.replace( File.separatorChar, '.' ) + nm;
268                                        break;
269                                }
270                        }
271                }
272//              return null;
273                return rtn;
274        }
275
276        /**
277         * クラスをコンパイルします。
278         *
279         * @og.rev 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。
280         * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
281         *
282         * @param clsNm クラス名
283         */
284        private void compileClass( final String clsNm ) {
285                if( COMPILER == null ) {
286                        throw new OgRuntimeException( "コンパイラクラスが定義されていません。tools.jarが存在しない可能性があります" );
287                }
288
289                final String srcFqn = srcDir + clsNm.replace( ".", File.separator ) + ".java";
290                final File srcFile = new File( srcFqn );
291                final String classFqn =  classDir + clsNm.replace( ".", File.separator ) + ".class";
292                final File clsFile = new File ( classFqn );
293
294                // クラスファイルが存在する場合は、エラーにしない。
295                if( !srcFile.exists() ) {
296                        if( clsFile.exists() ) {
297                                return;
298                        }
299                        throw new OgRuntimeException( "ソースファイルが存在しません。ファイル=[" + srcFqn + "]" );
300                }
301
302                if( clsFile.exists() && srcFile.lastModified() <= clsFile.lastModified() ) {
303                        return;
304                }
305
306                // 6.0.0.1 (2014/04/25) These nested if statements could be combined
307                if( !clsFile.getParentFile().exists() && !clsFile.getParentFile().mkdirs() ) {
308                        throw new OgRuntimeException( "ディレクトリが作成できませんでした。ファイル=[" + clsFile + "]" );
309                }
310
311                final StringWriter sw = new StringWriter();
312                final File[] sourceFiles = { new File( srcFqn ) };
313                // 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。
314                // 8.5.4.2 (2024/01/12) PMD 7.0.0 UseShortArrayInitializer
315//              final String[] cpOpts = new String[]{ "-d", classDir, "-classpath", classPath, "-encoding", "UTF-8" };
316                final String[] cpOpts = { "-d", classDir, "-classpath", classPath, "-encoding", "UTF-8" };
317
318                final CompilationTask task = COMPILER.getTask(sw, FILE_MANAGER, null, Arrays
319                                .asList(cpOpts), null, FILE_MANAGER
320                                .getJavaFileObjects(sourceFiles));
321
322                boolean isOk = false;
323                // lockしておかないと、java.lang.IllegalStateExceptionが発生することがある
324                synchronized( this ) {
325                        isOk = task.call();
326                }
327                if( !isOk ) {
328                        throw new OgRuntimeException( "コンパイルに失敗しました。" + CR + sw.toString() );
329                }
330        }
331
332        /**
333         * クラスをロードします。
334         *
335         * @og.rev 6.4.3.1 (2016/02/12) Collections.synchronizedMap に置き換え。
336         *
337         * @param       clsNm クラス名
338         *
339         * @return      ロードしたクラスオブジェクト
340         */
341        private Class<?> loadClass( final String clsNm ) {
342
343                final String classFqn =  classDir + clsNm.replace( ".", File.separator ) + ".class";
344                final File clsFile = new File( classFqn );
345                if( !clsFile.exists() ) {
346                        throw new OgRuntimeException( "クラスファイルが存在しません。ファイル=[" + classFqn + "]" );
347                }
348                final long lastModifyTime = clsFile.lastModified();             // 6.0.2.5 (2014/10/31) refactoring
349
350                HybsURLClassLoader loader = null;
351                        // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point.
352                        final String key = isHotDeploy ? clsNm : CONST_LOADER_KEY;
353                        loader = loaderMap.get( key );
354                        if( loader == null || lastModifyTime > loader.getCreationTime() ) {             // 6.0.2.5 (2014/10/31) refactoring
355                                try {
356                                        // 6.3.9.1 (2015/11/27) In J2EE, getClassLoader() might not work as expected.  Use Thread.currentThread().getContextClassLoader() instead.(PMD)
357                                        loader = new HybsURLClassLoader( new URL[] { new File( classDir ).toURI().toURL() }, Thread.currentThread().getContextClassLoader() );
358                                }
359                                catch( final MalformedURLException ex ) {
360                                        throw new OgRuntimeException( "クラスロードのURL変換に失敗しました。ファイル=[" + classFqn + "]", ex );
361                                }
362                                loaderMap.put( key, loader );
363                        }
364
365                Class<?> cls;
366                try {
367                        cls = loader.loadClass( clsNm );
368                }
369                catch( final ClassNotFoundException ex ) {
370                        final String errMsg = "クラスが存在しません。ファイル=[" + classFqn + "]"  ;
371                        throw new OgRuntimeException( errMsg , ex );
372                }
373                return cls;
374        }
375
376        /**
377         * このオブジェクトの内部表現を、文字列にして返します。
378         *
379         * @og.rev 6.1.0.0 (2014/12/26) refactoring
380         *
381         * @return  オブジェクトの内部表現
382         * @og.rtnNotNull
383         */
384        @Override
385        public String toString() {
386                return "srcDir=" + srcDir + " , classDir=" + classDir ;
387        }
388
389        /**
390         * URLClassLoaderを拡張し、クラスローダーの生成時間を管理できるようにしています。
391         */
392        private static final class HybsURLClassLoader {         // 6.3.9.1 (2015/11/27) final を追加
393                private final URLClassLoader loader;
394                private final long creationTime;
395
396                /**
397                 * URL配列 を引数に取るコンストラクタ
398                 *
399                 * @param  urls URL配列
400                 */
401                /* default */ HybsURLClassLoader( final URL[] urls ) {
402                        this( urls, null );
403                }
404
405                /**
406                 * URL配列と、クラスローダーを引数に取るコンストラクタ
407                 *
408                 * @param  urls URL配列
409                 * @param  clsLd クラスローダー
410                 */
411                /* default */ HybsURLClassLoader( final URL[] urls, final ClassLoader clsLd ) {
412                        // 6.1.0.0 (2014/12/26) findBugs: Bug type DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED (click for details)
413                        //  new org.opengion.fukurou.util.HybsLoader$HybsURLClassLoader(URL[], ClassLoader) は、
414                        // doPrivileged ブロックの中でクラスローダ java.net.URLClassLoader を作成するべきです。
415        // JDK1.8 警告:[removal] java.securityのAccessControllerは推奨されておらず、削除用にマークされています
416        //              loader = AccessController.doPrivileged(
417        //                                      new PrivilegedAction<URLClassLoader>() {
418        //                                              /**
419        //                                               * 特権を有効にして実行する PrivilegedAction<T> の run() メソッドです。
420        //                                               *
421        //                                               * このメソッドは、特権を有効にしたあとに AccessController.doPrivileged によって呼び出されます。
422        //                                               *
423        //                                               * @return  URLClassLoaderオブジェクト
424        //                                               * @og.rtnNotNull
425        //                                               */
426        //                                              public URLClassLoader run() {
427        //                                                      return new URLClassLoader( urls, clsLd );
428        //                                              }
429        //                                      }
430        //                              );
431                        loader = new URLClassLoader( urls, clsLd );
432                        creationTime = System.currentTimeMillis();
433                }
434
435                /**
436                 * クラスをロードします。
437                 *
438                 * @param       clsName クラス名の文字列
439                 * @return      Classオブジェクト
440                 * @throws      ClassNotFoundException クラスが見つからなかった場合
441                 */
442                /* default */ Class<?> loadClass( final String clsName ) throws ClassNotFoundException {
443                        return loader.loadClass( clsName );
444                }
445
446                /**
447                 * 作成時間を返します。
448                 *
449                 * @return      作成時間
450                 */
451                /* default */ long getCreationTime() {
452                        return creationTime;
453                }
454        }
455}