/*
 * Copyright (c) 2005- Shinji Kashihara.
 * All rights reserved. This program are made available under
 * the terms of the Eclipse Public License v1.0 which accompanies
 * this distribution, and is available at epl-v10.html.
 */
package jp.sourceforge.mergedoc.pleiades.aspect;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.net.URL;
import java.security.ProtectionDomain;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.NotFoundException;
import jp.sourceforge.mergedoc.pleiades.log.Logger;
import jp.sourceforge.mergedoc.pleiades.resource.Files;

/**
 * バイトコード変換を行うための抽象クラスです。
 * このクラスは状態を保持しますが、同期化されていません。
 * <p>
 * @author cypher256
 */
abstract public class AbstractTransformer implements ClassFileTransformer {

	/** ロガー */
	private static final Logger log = Logger.getLogger(AbstractTransformer.class);

	/** Javassist クラス・プール */
	private static final ClassPool classPool = new ClassPool(true);

	/** クラス・プールのクラスパスに追加済みロケーション・パス・セット */
	private static final Set<String> appendedLocationPathSet = Collections.synchronizedSet(new HashSet<String>());

	//-------------------------------------------------------------------------

	/**
	 * バイトコードを変換します。
	 * <p>
	 * @see ClassFileTransformer#transform(ClassLoader, String, Class,
	 *          java.security.ProtectionDomain, byte[])
	 */
	public byte[] transform(
			ClassLoader loader,
			String internalName,
			Class<?> classBeingRedefined,
			ProtectionDomain protectionDomain,
			byte[] bytecode) throws IllegalClassFormatException {

		long abstractTransformStart = System.nanoTime();
		String jarFileName = "";

		try {
			// JDK はバイトコード変換しない
			if (
				// null の場合がある
				internalName == null ||
				internalName.startsWith("java/") ||
				internalName.startsWith("javax/") ||
				internalName.startsWith("sun/") ||
				internalName.startsWith("com/sun/") ||
				internalName.startsWith("org/apache/commons")
			) {
				return null;
			}

			// トランスフォーマー内で使用するクラスはロード処理がループするため除外
			// pleiades のクラスを含めているのは計測処理埋め込みのため
			if (internalName.startsWith("jp/sourceforge/mergedoc/pleiades/aspect/resource/")) {
				if (
					internalName.endsWith("/AbstractClassCache") ||
					internalName.endsWith("/TransformedClassCache") ||
					internalName.endsWith("/ExcludeClassNameCache")
				) {
					return null;
				}
			}

			if (protectionDomain != null) {

				URL location = protectionDomain.getCodeSource().getLocation();
				if (location != null) {

					// Eclipse ヘルプ HTTP 500 エラー回避
					// jasper プラグインはバイトコード変換しない
					String locationPath = location.getPath();
					if (locationPath.contains("org.apache.jasper_")) {
						return null;
					}

					// クラス・ロケーションの取得 (jar ファイル名)
					int lastSlashPos = locationPath.lastIndexOf('/');
					if (lastSlashPos != -1) {
						jarFileName = locationPath.substring(lastSlashPos + 1);

						// バイトコード変換に必要な関連クラスパスをクラス・プールに追加。
						// パフォーマンスのために先に追加済みか検査する。
						if (!appendedLocationPathSet.contains(jarFileName)) {

							if (isClassPathTarget(locationPath, jarFileName)) {
								addClassPath(locationPath, jarFileName);
							}
						}
					}
				}
			}

		} catch (Throwable e) {

			String msg = "バイトコード変換事前処理に失敗しました。" + internalName;
			log.error(e, msg);
			throw new IllegalClassFormatException(msg + " 原因:" + e);

		} finally {

			Analyses.end(AbstractTransformer.class, "transform", abstractTransformStart);
		}

		long transformStart = System.nanoTime();

		try {
			// バイトコード変換テンプレート・メソッドの呼び出し
			String className = internalName.replace('/', '.');
			String classId = jarFileName + "/" + className;
			return transform(classId, className, bytecode);

		} catch (Throwable e) {

			StringBuilder msg = new StringBuilder();
			msg.append("バイトコード変換に失敗しました。");
			msg.append(jarFileName);
			msg.append("/");
			msg.append(internalName);
			msg.append(" ");
			msg.append(e);
			msg.append(" ");
			msg.append(e.getStackTrace()[0]);
			
			handleException(e, msg.toString());
			return null;

		} finally {

			Analyses.end(getClass(), "transform", transformStart);
		}
	}

	/**
	 * バイトコード変換の例外が発生した場合の処理を行います。
	 * @param e 例外
	 * @param msg メッセージ
	 */
	protected void handleException(Throwable e, String msg) {

		// プラグイン開発で ViewPart などを使用していると発生する場合あり
		//log.error(e, msg);
		//throw new IllegalClassFormatException(msg + " 原因:" + e);

		log.debug(msg.toString());
	}

	/**
	 * クラスパス追加対象か判定します。
	 * <p>
	 * @param fullPath フルパス
	 * @param 追加対象の場合は true
	 */
	protected boolean isClassPathTarget(String fullPath, String jarFileName) {
		return false;
	}

	/**
	 * クラスパスを追加します。
	 * <p>
	 * @param fullPath フルパス (スラッシュ区切り)
	 * @param jarFileName JAR ファイル名
	 * @throws NotFoundException 指定されたパスが見つからない場合
	 */
	protected void addClassPath(String fullPath, String jarFileName) throws NotFoundException {
		
		appendedLocationPathSet.add(jarFileName);
		String classPath = Files.decodeURL(fullPath);
		classPool.appendClassPath(classPath);
	}

	/**
	 * バイトコードを変換するためのテンプレート・メソッドです。
	 * <p>
	 * @param classId クラス識別子
	 * @param className クラス名
	 * @param bytecode バイトコード
	 * @return 変換後のバイトコード。変換する必要が無い場合は null。
	 * @throws CannotCompileException クラスがコンパイル出来ない場合
	 * @throws NotFoundException クラスが見つからない場合
	 * @throws IOException 入出力例外が発生した場合
	 */
	abstract protected byte[] transform(
			String classId,
			String className,
			byte[] bytecode
	) throws CannotCompileException, NotFoundException, IOException;

	/**
	 * バイトコードを指定して CtClass オブジェクトを作成します。
	 * <p>
	 * @param bytecode バイトコード
	 * @return CtClass オブジェクト
	 * @throws IOException 入出力例外が発生した場合
	 * @throws NotFoundException クラスが見つからない場合
	 */
	protected CtClass createCtClass(byte[] bytecode) throws IOException, NotFoundException {

		long start = System.nanoTime();

		CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(bytecode));
		ctClass.detach();

		Analyses.end(AbstractTransformer.class, "createCtClass", start);
		return ctClass;
	}

	/**
	 * 指定された CtClass オブジェクトが持つすべてのコンストラクタおよび
	 * メソッド記述子をログに出力します。
	 * <p>
	 * @param ctClass CtClass オブジェクト
	 */
	protected void logDescriptor(CtClass ctClass) {

		for (CtBehavior behavior : ctClass.getDeclaredBehaviors()) {
			log.info("DESCRIPTOR: " +
					ctClass.getName() + "#" +
					behavior.getName() + " " +
					behavior.getMethodInfo().getDescriptor());
		}
	}
}
