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;
026
027import javax.tools.JavaCompiler;
028import javax.tools.StandardJavaFileManager;
029import javax.tools.ToolProvider;
030import javax.tools.JavaCompiler.CompilationTask;
031
032/**
033 * AutoCompile機能、HotDeploy機能を実現するためのクラスローダーです。
034 *
035 * AutoCompile機能は、クラスの動的コンパイルを行います。
036 * AutoCompile機能を有効にするには、コンストラクタで与えられるHybsLoaderConfigオブジェクトで、
037 * AutoCompileフラグをtrueにしておく必要があります。
038 *
039 * HotDeploy機能は、クラスの動的ロードを行います。
040 * HotDeploy機能を有効にするには、コンストラクタで与えられるHybsLoaderConfigオブジェクトで、
041 * HotDeployフラグをtrueにしておく必要があります。
042 *
043 * (1)クラスの動的コンパイル
044 *  {@link #loadClass(String)}メソッドが呼ばれた場合に、ソースディレクトより、対象となるソースファイルを
045 *  検索し、クラスのコンパイルを行います。
046 *  コンパイルが行われる条件は、「クラスファイルが存在しない」または「クラスファイルのタイムスタンプがソースファイルより古い」です。
047 *
048 *  コンパイルを行うには、JDKに含まれるtools.jarが存在している必要があります。
049 *  tools.jarが見つからない場合、エラーとなります。
050 *
051 *  また、コンパイルのタスクのクラス(オブジェクト)は、JVMのシステムクラスローダー上のクラスに存在しています。
052 *  このため、サーブレットコンテナで、通常読み込まれるWEB-INF/classes,WEB-INF/lib以下のクラスファイルも、
053 *  そのままでは参照することができません。
054 *  これらのクラスを参照する場合は、HybsLoaderConfigオブジェクトに対してクラスパスを設定しておく必要があります。
055 *
056 * (2)クラスロード
057 *  クラスの動的ロードは、クラスローダーの入れ替えによって実現しています。
058 *  HotDeploy機能を有効にした場合、読み込むクラス単位にURLClassLoaderを生成しています。
059 *  クラスロードを行う際に、URLClassLoaderを新しく生成することで、クラスの再ロードを行っています。
060 *  つまり、HotDeployにより読み込まれるそれぞれのクラスは、お互いに独立した(平行な位置に存在する)関係に
061 *  なります。
062 *  このため、あるHotDeployによりロードされたクラスAから、同じくHotDeployによりロードされたクラスBを直接参照
063 *  することができません。
064 *  この場合は、クラスBのインターフェースを静的なクラスローダー(クラスAから参照できる位置)に配置することで、クラスB
065 *  のオブジェクトにアクセスすることができます。
066 *
067 * @og.rev 5.1.1.0 (2009/12/01) 新規作成
068 * @og.group 業務ロジック
069 *
070 * @version 5.0
071 * @author Hiroki Nakamura
072 * @since JDK1.6,
073 */
074public class HybsLoader {
075        private static final String CR = System.getProperty("line.separator");
076
077        // HotDeploy機能を使用しない場合のURLClossLoaderのキャッシュキー
078        private static final String CONST_LOADER_KEY = "CONST_LOADER_KEY";
079
080        private static final JavaCompiler COMPILER = ToolProvider.getSystemJavaCompiler();
081        private static final StandardJavaFileManager FILE_MANAGER = COMPILER.getStandardFileManager(null, null, null);
082
083        private final Map<String, HybsURLClassLoader> loaderMap = new WeakHashMap<String, HybsURLClassLoader>();
084        private final Map<String, String> clsNameMap = new WeakHashMap<String, String>();
085        private final String srcDir;
086        private final String classDir;
087        private final boolean isHotDeploy;
088        private final boolean isAutoCompile;
089        private final String classPath;
090
091        /**
092         * HybsLoaderOptionを使用してHybsLoaderオブジェクトを生成します。
093         *
094         * @param option HybsLoaderを構築するための設定情報
095         */
096        public HybsLoader( final HybsLoaderConfig option ) {
097                srcDir = option.getSrcDir();
098                classDir = option.getClassDir();
099                isHotDeploy = option.isHotDeploy();
100                isAutoCompile = option.isAutoCompile();
101                classPath = option.getClassPath();
102        }
103
104        /**
105         * 指定されたクラス名のクラスをロードします。
106         * クラス名については、クラス自身の名称のみを指定することができます。
107         * (パッケージ名を含めた完全な形のクラス名を指定することもできます)
108         *
109         * @param clsNm クラス名
110         *
111         * @return クラス
112         */
113        public Class<?> load( final String clsNm ) {
114                String clsName = getQualifiedName( clsNm );
115                if( isAutoCompile ) {
116                        compileClass( clsName );
117                }
118                Class<?> cls = loadClass( clsName );
119
120                return cls;
121        }
122
123        /**
124         * 指定されたクラス名のクラスをロードし、デフォルトコンストラクターを使用して
125         * インスタンスを生成します。
126         *
127         * @og.rev 5.1.8.0 (2010/07/01) Exceptionのエラーメッセージの修正(状態の出力)
128         *
129         * @param clsName クラス名(Qualified Name)
130         *
131         * @return インスタンス
132         */
133        public Object newInstance( final String clsName ) {
134                Class<?> cls = load( clsName );
135                Object obj = null;
136                try {
137                        obj = cls.newInstance();
138                }
139                catch( InstantiationException ex ) {
140//                      throw new RuntimeException( "インスタンスの生成に失敗しました" );
141                        String errMsg = "インスタンスの生成に失敗しました。["  + clsName + "]" ;
142                        throw new RuntimeException( errMsg , ex );
143                }
144                catch( IllegalAccessException ex ) {
145//                      throw new RuntimeException( "アクセスが拒否されました" );
146                        String errMsg = "アクセスが拒否されました。["  + clsName + "]" ;
147                        throw new RuntimeException( errMsg , ex );
148                }
149                return obj;
150        }
151
152        /**
153         * クラス名より完全クラス名を検索します。
154         *
155         * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
156         *
157         * @param clsNm クラス名
158         *
159         * @return 完全クラス名
160         */
161        private String getQualifiedName( final String clsNm ) {
162                String clsName = null;
163                if( clsNm.indexOf( '.' ) >= 0 ) {
164                        clsName = clsNm;
165                }
166                else {
167                        synchronized( clsNameMap ) {
168                                clsName = clsNameMap.get( clsNm );
169                                if( clsName == null ) {
170                                        clsName = findFile( "", clsNm );
171                                }
172                                if( clsName == null ) {
173                                        clsName = findFileByCls( "", clsNm );
174                                }
175                                clsNameMap.put( clsNm, clsName );
176                        }
177                        if( clsName == null ) {
178                                throw new RuntimeException( "ソースファイルが存在しません。ファイル=[" + clsNm + "]" );
179                        }
180                }
181                return clsName;
182        }
183
184        /**
185         * クラス名に対応するJavaファイルを再帰的に検索します。
186         *
187         * @param path 既定パス
188         * @param nm クラス名
189         *
190         * @return 完全クラス名
191         */
192        private String findFile( final String path, final String nm ) {
193                String tmpSrcPath = srcDir + path;
194                File[] files = (new File( tmpSrcPath )).listFiles();
195                if( files != null && files.length > 0 ) {
196                        for( int i=0; i<files.length; i++ ) {
197                                if ( files[i].isDirectory() ) {
198                                        String rtn = findFile( path + files[i].getName() + File.separator, nm );
199                                        if( rtn != null && rtn.length() > 0 ) {
200                                                return rtn;
201                                        }
202                                }
203                                else if( ( nm + ".java" ).equals( files[i].getName() ) ) {
204                                        return path.replace( File.separatorChar, '.' ) + nm;
205                                }
206                        }
207                }
208                return null;
209        }
210
211        /**
212         * クラス名に対応するJavaファイルを再帰的に検索します。
213         *
214         * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
215         *
216         * @param path 既定パス
217         * @param nm クラス名
218         *
219         * @return 完全クラス名
220         */
221        private String findFileByCls( final String path, final String nm ) {
222                String tmpSrcPath = classDir + path;
223                File[] files = (new File( tmpSrcPath )).listFiles();
224                if( files != null && files.length > 0 ) {
225                        for( int i=0; i<files.length; i++ ) {
226                                if ( files[i].isDirectory() ) {
227                                        String rtn = findFile( path + files[i].getName() + File.separator, nm );
228                                        if( rtn != null && rtn.length() > 0 ) {
229                                                return rtn;
230                                        }
231                                }
232                                else if( ( nm + ".class" ).equals( files[i].getName() ) ) {
233                                        return path.replace( File.separatorChar, '.' ) + nm;
234                                }
235                        }
236                }
237                return null;
238        }
239
240        /**
241         * クラスをコンパイルします。
242         *
243         * @og.rev 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。
244         * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
245         *
246         * @param clsNm クラス名
247         */
248        private void compileClass( final String clsNm ) {
249                if( COMPILER == null ) {
250                        throw new RuntimeException( "コンパイラクラスが定義されていません。tools.jarが存在しない可能性があります" );
251                }
252
253                String srcFqn = srcDir + clsNm.replace( ".", File.separator ) + ".java";
254                File srcFile = new File( srcFqn );
255                String classFqn =  classDir + clsNm.replace( ".", File.separator ) + ".class";
256                File clsFile = new File ( classFqn );
257
258                // クラスファイルが存在する場合は、エラーにしない。
259                if( !srcFile.exists() ) {
260                        if( clsFile.exists() ) {
261                                return;
262                        }
263                        throw new RuntimeException( "ソースファイルが存在しません。ファイル=[" + srcFqn + "]" );
264                }
265
266                if( clsFile.exists() && srcFile.lastModified() <= clsFile.lastModified() ) {
267                        return;
268                }
269
270                if( !clsFile.getParentFile().exists() ) {
271                        if( !clsFile.getParentFile().mkdirs() ) {
272                                throw new RuntimeException( "ディレクトリが作成できませんでした。ファイル=[" + clsFile + "]" );
273                        }
274                }
275
276                StringWriter sw = new StringWriter();
277                File[] sourceFiles = { new File( srcFqn ) };
278                // 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。
279//              String[] cpOpts = new String[]{ "-d", classDir, "-classpath", classPath };
280                String[] cpOpts = new String[]{ "-d", classDir, "-classpath", classPath, "-encoding", "UTF-8" };
281
282                CompilationTask task = COMPILER.getTask(sw, FILE_MANAGER, null, Arrays
283                                .asList(cpOpts), null, FILE_MANAGER
284                                .getJavaFileObjects(sourceFiles));
285
286                boolean isOk = false;
287                // lockしておかないと、java.lang.IllegalStateExceptionが発生することがある
288                synchronized( this ) {
289                        isOk = task.call();
290                }
291                if( !isOk ) {
292                        throw new RuntimeException( "コンパイルに失敗しました。" + CR + sw.toString() );
293                }
294        }
295
296        /**
297         * クラスをロードします。
298         *
299         * @param       clsNm クラス名
300         *
301         * @return      ロードしたクラスオブジェクト
302         */
303        private Class<?> loadClass( final String clsNm ) {
304                String key = isHotDeploy ? clsNm : CONST_LOADER_KEY;
305
306                String classFqn =  classDir + clsNm.replace( ".", File.separator ) + ".class";
307                File clsFile = new File( classFqn );
308                if( !clsFile.exists() ) {
309                        throw new RuntimeException( "クラスファイルが存在しません。ファイル=[" + classFqn + "]" );
310                }
311                long lastClassModifyTime = clsFile.lastModified();
312
313                HybsURLClassLoader loader = null;
314                synchronized( loaderMap ) {
315                        loader = loaderMap.get( key );
316                        if( loader == null || lastClassModifyTime > loader.getCreationTime() ) {
317                                try {
318                                        loader = new HybsURLClassLoader( new URL[] { ( new File( classDir ).toURI().toURL() ) }, this.getClass().getClassLoader() );
319                                }
320                                catch( MalformedURLException ex ) {
321                                        throw new RuntimeException( "クラスロードのURL変換に失敗しました。ファイル=[" + classFqn + "]", ex );
322                                }
323                                loaderMap.put( key, loader );
324                        }
325                }
326
327                Class<?> cls;
328                try {
329                        cls = loader.loadClass( clsNm );
330                }
331                catch( ClassNotFoundException ex ) {
332//                      throw new RuntimeException( "クラスが存在しません。ファイル=[" + classFqn + "]" );
333                        String errMsg = "クラスが存在しません。ファイル=[" + classFqn + "]"  ;
334                        throw new RuntimeException( errMsg , ex );
335                }
336                return cls;
337        }
338
339        /**
340         * URLClassLoaderを拡張し、クラスローダーの生成時間を管理できるようにしています。
341         */
342        private static class HybsURLClassLoader {
343                final URLClassLoader loader;
344                final long creationTime;
345
346                HybsURLClassLoader( final URL[] urls, final ClassLoader clsLd ) {
347                        loader = new URLClassLoader( urls, clsLd );
348                        creationTime = System.currentTimeMillis();
349                }
350
351                HybsURLClassLoader( final URL[] urls ) {
352                        this( urls, null );
353                }
354
355                Class<?> loadClass( final String clsName ) throws ClassNotFoundException {
356                        return loader.loadClass( clsName );
357                }
358
359                long getCreationTime() {
360                        return creationTime;
361                }
362        }
363}