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