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
030import java.security.AccessController;                                          // 6.1.0.0 (2014/12/26) findBugs
031import java.security.PrivilegedAction;                                          // 6.1.0.0 (2014/12/26) findBugs
032import java.lang.reflect.InvocationTargetException;                     // Ver7.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         * @param clsNm クラス名
118         *
119         * @return クラス
120         */
121        public Class<?> load( final String clsNm ) {
122                final String clsName = getQualifiedName( clsNm );
123                if( isAutoCompile ) {
124                        compileClass( clsName );
125                }
126                final Class<?> cls = loadClass( clsName );
127
128                return cls;
129        }
130
131        /**
132         * 指定されたクラス名のクラスをロードし、デフォルトコンストラクターを使用して
133         * インスタンスを生成します。
134         *
135         * @og.rev 5.1.8.0 (2010/07/01) Exceptionのエラーメッセージの修正(状態の出力)
136         * @og.rev 6.8.2.3 (2017/11/10) java9対応(cls.newInstance() → cls.getDeclaredConstructor().newInstance())
137         *
138         * @param clsName クラス名(Qualified Name)
139         *
140         * @return インスタンス
141         */
142        public Object newInstance( final String clsName ) {
143                final Class<?> cls = load( clsName );
144                Object obj = null;
145                try {
146                        obj = cls.getDeclaredConstructor().newInstance();               // Ver7.0.0.0
147                }
148                catch( final InstantiationException | InvocationTargetException | NoSuchMethodException ex ) {                  // 6.8.2.3 (2017/11/10)
149                        final String errMsg = "インスタンスの生成に失敗しました。["  + clsName + "]" ;
150                        throw new OgRuntimeException( errMsg , ex );
151                }
152                catch( final IllegalAccessException ex ) {
153                        final String errMsg = "アクセスが拒否されました。["  + clsName + "]" ;
154                        throw new OgRuntimeException( errMsg , ex );
155                }
156                return obj;
157        }
158
159        /**
160         * クラス名より完全クラス名を検索します。
161         *
162         * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
163         * @og.rev 6.4.3.1 (2016/02/12) Collections.synchronizedMap に置き換え。
164         *
165         * @param clsNm クラス名
166         *
167         * @return 完全クラス名
168         */
169        private String getQualifiedName( final String clsNm ) {
170                String clsName = null;
171                if( clsNm.indexOf( '.' ) >= 0 ) {
172                        clsName = clsNm;
173                }
174                else {
175                                clsName = clsNameMap.get( clsNm );
176                                if( clsName == null ) {
177                                        clsName = findFile( "", clsNm );
178                                }
179                                if( clsName == null ) {
180                                        clsName = findFileByCls( "", clsNm );
181                                }
182                                clsNameMap.put( clsNm, clsName );
183
184                        if( clsName == null ) {
185                                throw new OgRuntimeException( "ソースファイルが存在しません。ファイル=[" + clsNm + "]" );
186                        }
187                }
188                return clsName;
189        }
190
191        /**
192         * クラス名に対応するJavaファイルを再帰的に検索します。
193         *
194         * @param path 既定パス
195         * @param nm クラス名
196         *
197         * @return 完全クラス名
198         */
199        private String findFile( final String path, final String nm ) {
200                final String tmpSrcPath = srcDir + path;
201                final File[] files = new File( tmpSrcPath ).listFiles();
202                if( files != null && files.length > 0 ) {
203                        for( int i=0; i<files.length; i++ ) {
204                                if( files[i].isDirectory() ) {
205                                        final String rtn = findFile( path + files[i].getName() + File.separator, nm );
206                                        if( rtn != null && rtn.length() > 0 ) {
207                                                return rtn;
208                                        }
209                                }
210                                else if( ( nm + ".java" ).equals( files[i].getName() ) ) {
211                                        return path.replace( File.separatorChar, '.' ) + nm;
212                                }
213                        }
214                }
215                return null;
216        }
217
218        /**
219         * クラス名に対応するJavaファイルを再帰的に検索します。
220         *
221         * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
222         *
223         * @param path 既定パス
224         * @param nm クラス名
225         *
226         * @return 完全クラス名
227         */
228        private String findFileByCls( final String path, final String nm ) {
229                final String tmpSrcPath = classDir + path;
230                final File[] files = new File( tmpSrcPath ).listFiles();
231                if( files != null && files.length > 0 ) {
232                        for( int i=0; i<files.length; i++ ) {
233                                if( files[i].isDirectory() ) {
234                                        final String rtn = findFile( path + files[i].getName() + File.separator, nm );
235                                        if( rtn != null && rtn.length() > 0 ) {
236                                                return rtn;
237                                        }
238                                }
239                                else if( ( nm + ".class" ).equals( files[i].getName() ) ) {
240                                        return path.replace( File.separatorChar, '.' ) + nm;
241                                }
242                        }
243                }
244                return null;
245        }
246
247        /**
248         * クラスをコンパイルします。
249         *
250         * @og.rev 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。
251         * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
252         *
253         * @param clsNm クラス名
254         */
255        private void compileClass( final String clsNm ) {
256                if( COMPILER == null ) {
257                        throw new OgRuntimeException( "コンパイラクラスが定義されていません。tools.jarが存在しない可能性があります" );
258                }
259
260                final String srcFqn = srcDir + clsNm.replace( ".", File.separator ) + ".java";
261                final File srcFile = new File( srcFqn );
262                final String classFqn =  classDir + clsNm.replace( ".", File.separator ) + ".class";
263                final File clsFile = new File ( classFqn );
264
265                // クラスファイルが存在する場合は、エラーにしない。
266                if( !srcFile.exists() ) {
267                        if( clsFile.exists() ) {
268                                return;
269                        }
270                        throw new OgRuntimeException( "ソースファイルが存在しません。ファイル=[" + srcFqn + "]" );
271                }
272
273                if( clsFile.exists() && srcFile.lastModified() <= clsFile.lastModified() ) {
274                        return;
275                }
276
277                // 6.0.0.1 (2014/04/25) These nested if statements could be combined
278                if( !clsFile.getParentFile().exists() && !clsFile.getParentFile().mkdirs() ) {
279                        throw new OgRuntimeException( "ディレクトリが作成できませんでした。ファイル=[" + clsFile + "]" );
280                }
281
282                final StringWriter sw = new StringWriter();
283                final File[] sourceFiles = { new File( srcFqn ) };
284                // 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。
285                final String[] cpOpts = new String[]{ "-d", classDir, "-classpath", classPath, "-encoding", "UTF-8" };
286
287                final CompilationTask task = COMPILER.getTask(sw, FILE_MANAGER, null, Arrays
288                                .asList(cpOpts), null, FILE_MANAGER
289                                .getJavaFileObjects(sourceFiles));
290
291                boolean isOk = false;
292                // lockしておかないと、java.lang.IllegalStateExceptionが発生することがある
293                synchronized( this ) {
294                        isOk = task.call();
295                }
296                if( !isOk ) {
297                        throw new OgRuntimeException( "コンパイルに失敗しました。" + CR + sw.toString() );
298                }
299        }
300
301        /**
302         * クラスをロードします。
303         *
304         * @og.rev 6.4.3.1 (2016/02/12) Collections.synchronizedMap に置き換え。
305         *
306         * @param       clsNm クラス名
307         *
308         * @return      ロードしたクラスオブジェクト
309         */
310        private Class<?> loadClass( final String clsNm ) {
311
312                final String classFqn =  classDir + clsNm.replace( ".", File.separator ) + ".class";
313                final File clsFile = new File( classFqn );
314                if( !clsFile.exists() ) {
315                        throw new OgRuntimeException( "クラスファイルが存在しません。ファイル=[" + classFqn + "]" );
316                }
317                final long lastModifyTime = clsFile.lastModified();             // 6.0.2.5 (2014/10/31) refactoring
318
319                HybsURLClassLoader loader = null;
320                        // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point.
321                        final String key = isHotDeploy ? clsNm : CONST_LOADER_KEY;
322                        loader = loaderMap.get( key );
323                        if( loader == null || lastModifyTime > loader.getCreationTime() ) {             // 6.0.2.5 (2014/10/31) refactoring
324                                try {
325                                        // 6.3.9.1 (2015/11/27) In J2EE, getClassLoader() might not work as expected.  Use Thread.currentThread().getContextClassLoader() instead.(PMD)
326                                        loader = new HybsURLClassLoader( new URL[] { new File( classDir ).toURI().toURL() }, Thread.currentThread().getContextClassLoader() );
327                                }
328                                catch( final MalformedURLException ex ) {
329                                        throw new OgRuntimeException( "クラスロードのURL変換に失敗しました。ファイル=[" + classFqn + "]", ex );
330                                }
331                                loaderMap.put( key, loader );
332                        }
333
334                Class<?> cls;
335                try {
336                        cls = loader.loadClass( clsNm );
337                }
338                catch( final ClassNotFoundException ex ) {
339                        final String errMsg = "クラスが存在しません。ファイル=[" + classFqn + "]"  ;
340                        throw new OgRuntimeException( errMsg , ex );
341                }
342                return cls;
343        }
344
345        /**
346         * このオブジェクトの内部表現を、文字列にして返します。
347         *
348         * @og.rev 6.1.0.0 (2014/12/26) refactoring
349         *
350         * @return  オブジェクトの内部表現
351         * @og.rtnNotNull
352         */
353        @Override
354        public String toString() {
355                return "srcDir=" + srcDir + " , classDir=" + classDir ;
356        }
357
358        /**
359         * URLClassLoaderを拡張し、クラスローダーの生成時間を管理できるようにしています。
360         */
361        private static final class HybsURLClassLoader {         // 6.3.9.1 (2015/11/27) final を追加
362                private final URLClassLoader loader;
363                private final long creationTime;
364
365                /**
366                 * URL配列 を引数に取るコンストラクタ
367                 *
368                 * @param  urls URL配列
369                 */
370                HybsURLClassLoader( final URL[] urls ) {
371                        this( urls, null );
372                }
373
374                /**
375                 * URL配列と、クラスローダーを引数に取るコンストラクタ
376                 *
377                 * @param  urls URL配列
378                 * @param  clsLd クラスローダー
379                 */
380                HybsURLClassLoader( final URL[] urls, final ClassLoader clsLd ) {
381                        // 6.1.0.0 (2014/12/26) findBugs: Bug type DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED (click for details)
382                        //  new org.opengion.fukurou.util.HybsLoader$HybsURLClassLoader(URL[], ClassLoader) は、doPrivileged ブロックの中でクラスローダ java.net.URLClassLoader を作成するべきです。 
383                        loader = AccessController.doPrivileged(
384                                                new PrivilegedAction<URLClassLoader>() {
385                                                        /**
386                                                         * 特権を有効にして実行する PrivilegedAction<T> の run() メソッドです。
387                                                         *
388                                                         * このメソッドは、特権を有効にしたあとに AccessController.doPrivileged によって呼び出されます。 
389                                                         *
390                                                         * @return  URLClassLoaderオブジェクト
391                                                         * @og.rtnNotNull
392                                                         */
393                                                        public URLClassLoader run() {
394                                                                return new URLClassLoader( urls, clsLd );
395                                                        }
396                                                }
397                                        );
398                        creationTime = System.currentTimeMillis();
399                }
400
401                /**
402                 * クラスをロードします。
403                 *
404                 * @param       clsName クラス名の文字列
405                 * @return      Classオブジェクト
406                 */
407                /* default */ Class<?> loadClass( final String clsName ) throws ClassNotFoundException {
408                        return loader.loadClass( clsName );
409                }
410
411                /**
412                 * 作成時間を返します。
413                 *
414                 * @return      作成時間
415                 */
416                /* default */ long getCreationTime() {
417                        return creationTime;
418                }
419        }
420}