/*
 * Copyright 2004-2015 the Seasar Foundation and the Others.
 *
 * 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.seasar.framework.jpa.impl;

import java.io.File;
import java.net.URL;
import java.util.List;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javax.persistence.spi.ClassTransformer;
import javax.persistence.spi.PersistenceUnitInfo;

import org.seasar.framework.jpa.PersistenceClassTransformer;
import org.seasar.framework.jpa.util.ChildFirstClassLoader;
import org.seasar.framework.jpa.util.ClassLoaderEvent;
import org.seasar.framework.jpa.util.ClassLoaderListener;
import org.seasar.framework.jpa.util.ClassTransformerUtil;
import org.seasar.framework.util.ClassLoaderUtil;
import org.seasar.framework.util.ClassUtil;
import org.seasar.framework.util.FileUtil;
import org.seasar.framework.util.JarFileUtil;
import org.seasar.framework.util.URLUtil;
import org.seasar.framework.util.tiger.CollectionsUtil;

import static org.seasar.framework.util.tiger.IterableAdapter.*;

/**
 * 永続クラスをトランスフォームします。
 * 
 * @author taedium
 */
public class PersistenceClassTransformerImpl implements
        PersistenceClassTransformer {

    /** トランスフォームした永続クラスをロードする対象から除外するクラスローダのクラス名 */
    protected Set<String> ignoreLoaderClassNames = CollectionsUtil.newHashSet();

    /**
     * インスタンスを構築します。
     * 
     */
    public PersistenceClassTransformerImpl() {
    }

    /**
     * トランスフォームした永続クラスをロードする対象から除外するクラスローダのクラス名を追加します。
     * 
     * @param ignoreLoaderClassName
     *            トランスフォームした永続クラスをロードする対象から除外するクラスローダのクラス名
     */
    public void addIgnoreLoaderClassName(final String ignoreLoaderClassName) {
        ignoreLoaderClassNames.add(ignoreLoaderClassName);
    }

    public void transform(final PersistenceUnitInfo unitInfo) {
        final List<ClassTransformer> transformers = PersistenceUnitInfoImpl.class
                .cast(unitInfo).getTransformers();
        final ClassLoader classLoader = unitInfo.getClassLoader();
        final ClassLoader targetLoader = getTargetClassLoader(classLoader);
        final ChildFirstClassLoader tempLoader = new ChildFirstClassLoader(
                targetLoader);
        tempLoader.addClassLoaderListener(new ClassLoaderListener() {

            public void classFinded(final ClassLoaderEvent event) {
                final String className = event.getClassName();
                byte[] bytes = event.getBytecode();
                for (final ClassTransformer transformer : transformers) {
                    bytes = transform(transformer, targetLoader, className,
                            bytes);
                }
                ClassLoaderUtil.defineClass(targetLoader, className, bytes, 0,
                        bytes.length);
            }
        });
        loadPersistenceClasses(unitInfo, tempLoader);
    }

    public Class<?> transform(final PersistenceUnitInfo unitInfo,
            final String className, final byte[] bytecode) {
        final List<ClassTransformer> transformers = PersistenceUnitInfoImpl.class
                .cast(unitInfo).getTransformers();
        final ClassLoader classLoader = unitInfo.getClassLoader();
        final ClassLoader targetLoader = getTargetClassLoader(classLoader);
        byte[] bytes = bytecode;
        for (final ClassTransformer transformer : transformers) {
            bytes = transform(transformer, targetLoader, className, bytes);
        }
        return ClassLoaderUtil.defineClass(targetLoader, className, bytes, 0,
                bytes.length);
    }

    /**
     * 永続ユニット情報で管理されるクラスを指定のクラスローダにロードします。
     * 
     * @param unitInfo
     *            永続ユニット情報
     * @param loader
     *            クラスローダ
     */
    protected void loadPersistenceClasses(final PersistenceUnitInfo unitInfo,
            final ClassLoader loader) {
        for (final String className : unitInfo.getManagedClassNames()) {
            loadClass(loader, className);
        }
        for (final URL jarFileUrl : unitInfo.getJarFileUrls()) {
            loadClass(loader, jarFileUrl);
        }
        if (!unitInfo.excludeUnlistedClasses()) {
            final URL rootUrl = unitInfo.getPersistenceUnitRootUrl();
            if ("file".equals(rootUrl.getProtocol())) {
                loadClass(loader, URLUtil.toFile(rootUrl), null);
            } else {
                loadClass(loader, FileUtil.toURL(new File(JarFileUtil
                        .toJarFilePath(rootUrl))));
            }
        }
    }

    /**
     * クラスをロードします。
     * <p>
     * クラスが見つからない場合はクラス名をパッケージ名として解釈し、<code>package-info</code>クラスをロードします。
     * </p>
     * 
     * @param loader
     *            クラスローダ
     * @param className
     *            クラス名
     */
    protected void loadClass(final ClassLoader loader, final String className) {
        try {
            loader.loadClass(className);
        } catch (final ClassNotFoundException e) {
            ClassLoaderUtil.loadClass(loader, className + ".package-info");
        }
    }

    /**
     * Jarファイルからクラスをロードします。
     * 
     * @param loader
     *            クラスローダ
     * @param jarFileUrl
     *            JarファイルのURL
     */
    protected void loadClass(final ClassLoader loader, final URL jarFileUrl) {
        final JarFile jarFile = JarFileUtil.create(jarFileUrl.getPath());
        try {
            for (final JarEntry entry : iterable(jarFile.entries())) {
                final String entryName = entry.getName();
                if (entry.isDirectory() || !entryName.endsWith(".class")) {
                    continue;
                }
                final String className = removeExtension(entryName).replace(
                        '/', '.');
                loadClass(loader, className);
            }
        } finally {
            JarFileUtil.close(jarFile);
        }
    }

    /**
     * ファイルシステムからクラスをロードします。
     * 
     * @param loader
     *            クラスローダ
     * @param dir
     *            ディレクトリ
     * @param path
     *            クラスパスの基点から現在のディレクトリまでのパス (ピリオド区切り)
     */
    protected void loadClass(final ClassLoader loader, final File dir,
            final String path) {
        for (final File file : dir.listFiles()) {
            final String fileName = file.getName();
            if (file.isDirectory()) {
                loadClass(loader, file, ClassUtil.concatName(path, fileName));
            } else if (fileName.endsWith(".class")) {
                final String className = ClassUtil.concatName(path,
                        removeExtension(fileName));
                loadClass(loader, className);
            }
        }
    }

    /**
     * 永続クラスのバイト列をトランスフォームしたバイト列を返します。
     * 
     * @param transformer
     *            トランスフォーマ
     * @param classLoader
     *            変換されるクラスを定義するローダ
     * @param className
     *            クラス名
     * @param bytes
     *            クラスファイル形式のバイト列
     * @return 変換されたクラスファイル形式のバイト列。変換されなかった場合は引数のバイト列
     */
    protected byte[] transform(final ClassTransformer transformer,
            final ClassLoader classLoader, final String className,
            final byte[] bytes) {
        final byte[] transformed = ClassTransformerUtil.transform(transformer,
                classLoader, className.replace('.', '/'), null, null, bytes);
        return transformed == null ? bytes : transformed;
    }

    /**
     * トランスフォームした永続クラスをロードする対象のクラスローダを返します。
     * 
     * @param originLoader
     *            原点となるクラスローダ
     * @return トランスフォームした永続クラスをロードする対象のクラスローダ
     */
    protected ClassLoader getTargetClassLoader(final ClassLoader originLoader) {
        ClassLoader loader = originLoader;
        while (loader != null
                && ignoreLoaderClassNames.contains(loader.getClass().getName())) {
            loader = loader.getParent();
        }
        if (loader != null) {
            return loader;
        }
        return originLoader;
    }

    /**
     * ファイル名から拡張子を取り除いた名前を返します。
     * 
     * @param name
     *            ファイル名
     * @return ファイル名から拡張子を取り除いた名前
     */
    protected String removeExtension(final String name) {
        return name.substring(0, name.lastIndexOf('.'));
    }

}
