/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.util;

import static org.opengion.fukurou.system.HybsConst.CR;		// 6.1.0.0 (2014/12/26) refactoring
import org.opengion.fukurou.system.OgRuntimeException ;		// 6.4.2.0 (2016/01/29)

import java.io.File;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Arrays;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.Collections;								// 6.4.3.1 (2016/02/12) refactoring
import java.security.AccessController;						// 6.1.0.0 (2014/12/26) findBugs
import java.security.PrivilegedAction;						// 6.1.0.0 (2014/12/26) findBugs
import java.lang.reflect.InvocationTargetException;			// Ver7.0.0.0

import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import javax.tools.JavaCompiler.CompilationTask;

/**
 * AutoCompile機能、HotDeploy機能を実現するためのクラスローダーです。
 *
 * AutoCompile機能は、クラスの動的コンパイルを行います。
 * AutoCompile機能を有効にするには、コンストラクタで与えられるHybsLoaderConfigオブジェクトで、
 * AutoCompileフラグをtrueにしておく必要があります。
 *
 * HotDeploy機能は、クラスの動的ロードを行います。
 * HotDeploy機能を有効にするには、コンストラクタで与えられるHybsLoaderConfigオブジェクトで、
 * HotDeployフラグをtrueにしておく必要があります。
 *
 * (1)クラスの動的コンパイル
 *  {@link #loadClass(String)}メソッドが呼ばれた場合に、ソースディレクトより、対象となるソースファイルを
 *  検索し、クラスのコンパイルを行います。
 *  コンパイルが行われる条件は、「クラスファイルが存在しない」または「クラスファイルのタイムスタンプがソースファイルより古い」です。
 *
 *  コンパイルを行うには、JDKに含まれるtools.jarが存在している必要があります。
 *  tools.jarが見つからない場合、エラーとなります。
 *
 *  また、コンパイルのタスクのクラス(オブジェクト)は、JVMのシステムクラスローダー上のクラスに存在しています。
 *  このため、サーブレットコンテナで、通常読み込まれるWEB-INF/classes,WEB-INF/lib以下のクラスファイルも、
 *  そのままでは参照することができません。
 *  これらのクラスを参照する場合は、HybsLoaderConfigオブジェクトに対してクラスパスを設定しておく必要があります。
 *
 * (2)クラスロード
 *  クラスの動的ロードは、クラスローダーの入れ替えによって実現しています。
 *  HotDeploy機能を有効にした場合、読み込むクラス単位にURLClassLoaderを生成しています。
 *  クラスロードを行う際に、URLClassLoaderを新しく生成することで、クラスの再ロードを行っています。
 *  つまり、HotDeployにより読み込まれるそれぞれのクラスは、お互いに独立した(平行な位置に存在する)関係に
 *  なります。
 *  このため、あるHotDeployによりロードされたクラスAから、同じくHotDeployによりロードされたクラスBを直接参照
 *  することができません。
 *  この場合は、クラスBのインターフェースを静的なクラスローダー(クラスAから参照できる位置)に配置することで、クラスB
 *  のオブジェクトにアクセスすることができます。
 *
 * @og.rev 5.1.1.0 (2009/12/01) 新規作成
 * @og.group 業務ロジック
 *
 * @version 5.0
 * @author Hiroki Nakamura
 * @since JDK1.6,
 */
public class HybsLoader {

	// HotDeploy機能を使用しない場合のURLClossLoaderのキャッシュキー
	private static final String CONST_LOADER_KEY = "CONST_LOADER_KEY";

	private static final JavaCompiler COMPILER = ToolProvider.getSystemJavaCompiler();
	private static final StandardJavaFileManager FILE_MANAGER = COMPILER.getStandardFileManager(null, null, null);

	/** 6.4.3.1 (2016/02/12) Collections.synchronizedMap で同期処理を行います。  */
	private final Map<String, HybsURLClassLoader> loaderMap = Collections.synchronizedMap( new WeakHashMap<>() );
	/** 6.4.3.1 (2016/02/12) Collections.synchronizedMap で同期処理を行います。  */
	private final Map<String, String> clsNameMap = Collections.synchronizedMap( new WeakHashMap<>() );
	private final String	srcDir			;
	private final String	classDir		;
	private final boolean	isHotDeploy		;
	private final boolean	isAutoCompile	;
	private final String	classPath		;

	/**
	 * HybsLoaderOptionを使用してHybsLoaderオブジェクトを生成します。
	 *
	 * @param option HybsLoaderを構築するための設定情報
	 */
	public HybsLoader( final HybsLoaderConfig option ) {
		srcDir			= option.getSrcDir()	;
		classDir		= option.getClassDir()	;
		isHotDeploy		= option.isHotDeploy()	;
		isAutoCompile	= option.isAutoCompile();
		classPath		= option.getClassPath()	;
	}

	/**
	 * 指定されたクラス名のクラスをロードします。
	 * クラス名については、クラス自身の名称のみを指定することができます。
	 * (パッケージ名を含めた完全な形のクラス名を指定することもできます)
	 *
	 * @param clsNm クラス名
	 *
	 * @return クラス
	 */
	public Class<?> load( final String clsNm ) {
		final String clsName = getQualifiedName( clsNm );
		if( isAutoCompile ) {
			compileClass( clsName );
		}
		final Class<?> cls = loadClass( clsName );

		return cls;
	}

	/**
	 * 指定されたクラス名のクラスをロードし、デフォルトコンストラクターを使用して
	 * インスタンスを生成します。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) Exceptionのエラーメッセージの修正(状態の出力)
	 * @og.rev 6.8.2.3 (2017/11/10) java9対応(cls.newInstance() → cls.getDeclaredConstructor().newInstance())
	 *
	 * @param clsName クラス名(Qualified Name)
	 *
	 * @return インスタンス
	 */
	public Object newInstance( final String clsName ) {
		final Class<?> cls = load( clsName );
		Object obj = null;
		try {
			obj = cls.getDeclaredConstructor().newInstance();		// Ver7.0.0.0
		}
		catch( final InstantiationException | InvocationTargetException | NoSuchMethodException ex ) {			// 6.8.2.3 (2017/11/10)
			final String errMsg = "インスタンスの生成に失敗しました。["  + clsName + "]" ;
			throw new OgRuntimeException( errMsg , ex );
		}
		catch( final IllegalAccessException ex ) {
			final String errMsg = "アクセスが拒否されました。["  + clsName + "]" ;
			throw new OgRuntimeException( errMsg , ex );
		}
		return obj;
	}

	/**
	 * クラス名より完全クラス名を検索します。
	 *
	 * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
	 * @og.rev 6.4.3.1 (2016/02/12) Collections.synchronizedMap に置き換え。
	 *
	 * @param clsNm クラス名
	 *
	 * @return 完全クラス名
	 */
	private String getQualifiedName( final String clsNm ) {
		String clsName = null;
		if( clsNm.indexOf( '.' ) >= 0 ) {
			clsName = clsNm;
		}
		else {
				clsName = clsNameMap.get( clsNm );
				if( clsName == null ) {
					clsName = findFile( "", clsNm );
				}
				if( clsName == null ) {
					clsName = findFileByCls( "", clsNm );
				}
				clsNameMap.put( clsNm, clsName );

			if( clsName == null ) {
				throw new OgRuntimeException( "ソースファイルが存在しません。ファイル=[" + clsNm + "]" );
			}
		}
		return clsName;
	}

	/**
	 * クラス名に対応するJavaファイルを再帰的に検索します。
	 *
	 * @param path 既定パス
	 * @param nm クラス名
	 *
	 * @return 完全クラス名
	 */
	private String findFile( final String path, final String nm ) {
		final String tmpSrcPath = srcDir + path;
		final File[] files = new File( tmpSrcPath ).listFiles();
		if( files != null && files.length > 0 ) {
			for( int i=0; i<files.length; i++ ) {
				if( files[i].isDirectory() ) {
					final String rtn = findFile( path + files[i].getName() + File.separator, nm );
					if( rtn != null && rtn.length() > 0 ) {
						return rtn;
					}
				}
				else if( ( nm + ".java" ).equals( files[i].getName() ) ) {
					return path.replace( File.separatorChar, '.' ) + nm;
				}
			}
		}
		return null;
	}

	/**
	 * クラス名に対応するJavaファイルを再帰的に検索します。
	 *
	 * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
	 *
	 * @param path 既定パス
	 * @param nm クラス名
	 *
	 * @return 完全クラス名
	 */
	private String findFileByCls( final String path, final String nm ) {
		final String tmpSrcPath = classDir + path;
		final File[] files = new File( tmpSrcPath ).listFiles();
		if( files != null && files.length > 0 ) {
			for( int i=0; i<files.length; i++ ) {
				if( files[i].isDirectory() ) {
					final String rtn = findFile( path + files[i].getName() + File.separator, nm );
					if( rtn != null && rtn.length() > 0 ) {
						return rtn;
					}
				}
				else if( ( nm + ".class" ).equals( files[i].getName() ) ) {
					return path.replace( File.separatorChar, '.' ) + nm;
				}
			}
		}
		return null;
	}

	/**
	 * クラスをコンパイルします。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。
	 * @og.rev 5.2.1.0 (2010/10/01) クラスファイルが存在する場合は、エラーにしない。
	 *
	 * @param clsNm クラス名
	 */
	private void compileClass( final String clsNm ) {
		if( COMPILER == null ) {
			throw new OgRuntimeException( "コンパイラクラスが定義されていません。tools.jarが存在しない可能性があります" );
		}

		final String srcFqn = srcDir + clsNm.replace( ".", File.separator ) + ".java";
		final File srcFile = new File( srcFqn );
		final String classFqn =  classDir + clsNm.replace( ".", File.separator ) + ".class";
		final File clsFile = new File ( classFqn );

		// クラスファイルが存在する場合は、エラーにしない。
		if( !srcFile.exists() ) {
			if( clsFile.exists() ) {
				return;
			}
			throw new OgRuntimeException( "ソースファイルが存在しません。ファイル=[" + srcFqn + "]" );
		}

		if( clsFile.exists() && srcFile.lastModified() <= clsFile.lastModified() ) {
			return;
		}

		// 6.0.0.1 (2014/04/25) These nested if statements could be combined
		if( !clsFile.getParentFile().exists() && !clsFile.getParentFile().mkdirs() ) {
			throw new OgRuntimeException( "ディレクトリが作成できませんでした。ファイル=[" + clsFile + "]" );
		}

		final StringWriter sw = new StringWriter();
		final File[] sourceFiles = { new File( srcFqn ) };
		// 5.1.8.0 (2010/07/01) ソースファイルのエンコードは、UTF-8にする。
		final String[] cpOpts = new String[]{ "-d", classDir, "-classpath", classPath, "-encoding", "UTF-8" };

		final CompilationTask task = COMPILER.getTask(sw, FILE_MANAGER, null, Arrays
				.asList(cpOpts), null, FILE_MANAGER
				.getJavaFileObjects(sourceFiles));

		boolean isOk = false;
		// lockしておかないと、java.lang.IllegalStateExceptionが発生することがある
		synchronized( this ) {
			isOk = task.call();
		}
		if( !isOk ) {
			throw new OgRuntimeException( "コンパイルに失敗しました。" + CR + sw.toString() );
		}
	}

	/**
	 * クラスをロードします。
	 *
	 * @og.rev 6.4.3.1 (2016/02/12) Collections.synchronizedMap に置き換え。
	 *
	 * @param	clsNm クラス名
	 *
	 * @return	ロードしたクラスオブジェクト
	 */
	private Class<?> loadClass( final String clsNm ) {

		final String classFqn =  classDir + clsNm.replace( ".", File.separator ) + ".class";
		final File clsFile = new File( classFqn );
		if( !clsFile.exists() ) {
			throw new OgRuntimeException( "クラスファイルが存在しません。ファイル=[" + classFqn + "]" );
		}
		final long lastModifyTime = clsFile.lastModified();		// 6.0.2.5 (2014/10/31) refactoring

		HybsURLClassLoader loader = null;
			// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point.
			final String key = isHotDeploy ? clsNm : CONST_LOADER_KEY;
			loader = loaderMap.get( key );
			if( loader == null || lastModifyTime > loader.getCreationTime() ) {		// 6.0.2.5 (2014/10/31) refactoring
				try {
					// 6.3.9.1 (2015/11/27) In J2EE, getClassLoader() might not work as expected.  Use Thread.currentThread().getContextClassLoader() instead.(PMD)
					loader = new HybsURLClassLoader( new URL[] { new File( classDir ).toURI().toURL() }, Thread.currentThread().getContextClassLoader() );
				}
				catch( final MalformedURLException ex ) {
					throw new OgRuntimeException( "クラスロードのURL変換に失敗しました。ファイル=[" + classFqn + "]", ex );
				}
				loaderMap.put( key, loader );
			}

		Class<?> cls;
		try {
			cls = loader.loadClass( clsNm );
		}
		catch( final ClassNotFoundException ex ) {
			final String errMsg = "クラスが存在しません。ファイル=[" + classFqn + "]"  ;
			throw new OgRuntimeException( errMsg , ex );
		}
		return cls;
	}

	/**
	 * このオブジェクトの内部表現を、文字列にして返します。
	 *
	 * @og.rev 6.1.0.0 (2014/12/26) refactoring
	 *
	 * @return  オブジェクトの内部表現
	 * @og.rtnNotNull
	 */
	@Override
	public String toString() {
		return "srcDir=" + srcDir + " , classDir=" + classDir ;
	}

	/**
	 * URLClassLoaderを拡張し、クラスローダーの生成時間を管理できるようにしています。
	 */
	private static final class HybsURLClassLoader {		// 6.3.9.1 (2015/11/27) final を追加
		private final URLClassLoader loader;
		private final long creationTime;

		/**
		 * URL配列 を引数に取るコンストラクタ
		 *
		 * @param  urls URL配列
		 */
		HybsURLClassLoader( final URL[] urls ) {
			this( urls, null );
		}

		/**
		 * URL配列と、クラスローダーを引数に取るコンストラクタ
		 *
		 * @param  urls URL配列
		 * @param  clsLd クラスローダー
		 */
		HybsURLClassLoader( final URL[] urls, final ClassLoader clsLd ) {
			// 6.1.0.0 (2014/12/26) findBugs: Bug type DP_CREATE_CLASSLOADER_INSIDE_DO_PRIVILEGED (click for details)
			//  new org.opengion.fukurou.util.HybsLoader$HybsURLClassLoader(URL[], ClassLoader) は、doPrivileged ブロックの中でクラスローダ java.net.URLClassLoader を作成するべきです。 
			loader = AccessController.doPrivileged(
						new PrivilegedAction<URLClassLoader>() {
							/**
							 * 特権を有効にして実行する PrivilegedAction<T> の run() メソッドです。
							 *
							 * このメソッドは、特権を有効にしたあとに AccessController.doPrivileged によって呼び出されます。 
							 *
							 * @return  URLClassLoaderオブジェクト
							 * @og.rtnNotNull
							 */
							public URLClassLoader run() {
								return new URLClassLoader( urls, clsLd );
							}
						}
					);
			creationTime = System.currentTimeMillis();
		}

		/**
		 * クラスをロードします。
		 *
		 * @param	clsName	クラス名の文字列
		 * @return	Classオブジェクト
		 */
		/* default */ Class<?> loadClass( final String clsName ) throws ClassNotFoundException {
			return loader.loadClass( clsName );
		}

		/**
		 * 作成時間を返します。
		 *
		 * @return	作成時間
		 */
		/* default */ long getCreationTime() {
			return creationTime;
		}
	}
}
