/*
 * 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.InputStream;
import java.net.URL;
import java.util.List;

import javax.persistence.spi.PersistenceUnitInfo;
import javax.persistence.spi.PersistenceUnitTransactionType;
import javax.sql.DataSource;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.validation.Schema;

import org.seasar.extension.datasource.impl.SingletonDataSourceProxy;
import org.seasar.extension.j2ee.JndiResourceLocator;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.annotation.tiger.Binding;
import org.seasar.framework.container.annotation.tiger.BindingType;
import org.seasar.framework.env.Env;
import org.seasar.framework.jpa.PersistenceUnitInfoFactory;
import org.seasar.framework.util.InputStreamUtil;
import org.seasar.framework.util.SAXParserFactoryUtil;
import org.seasar.framework.util.SchemaUtil;
import org.seasar.framework.util.StringUtil;
import org.seasar.framework.util.URLUtil;
import org.seasar.framework.util.tiger.CollectionsUtil;
import org.seasar.framework.xml.SaxHandler;
import org.seasar.framework.xml.SaxHandlerParser;
import org.seasar.framework.xml.TagHandler;
import org.seasar.framework.xml.TagHandlerContext;
import org.seasar.framework.xml.TagHandlerRule;
import org.xml.sax.Attributes;

/**
 * 指定された<code>META-INF/persistence.xml</code>を読み込んで
 * {@link PersistenceUnitInfo 永続ユニット情報}を作成するファクトリの実装クラスです。
 * 
 * @author koichik
 */
public class PersistenceUnitInfoFactoryImpl implements
        PersistenceUnitInfoFactory {

    /** デフォルトの永続ユニットプロバイダクラス名 (Hibernate EntityManager) */
    public static final String DEFAULT_PROVIDER = "org.hibernate.ejb.HibernatePersistence";

    /** デフォルトのデータソース名 */
    public static final String DEFAULT_DATASOURCE = "jdbc/dataSource";

    /** <code>persistence.xml</code>のパス名 */
    public static final String PERSISTENCE_XML = "META-INF/persistence.xml";

    /** <code>persistence.xml</code>を検証するXML Schemaのパス名 */
    public static final String PERSISTENCE_SCHEMA_NAME = "persistence_1_0.xsd";

    /** 永続ユニットルートURLのコンテキストキー */
    public static final String PERSISTENCE_UNIT_ROOT_URL = "persistenceUnitRootUrl";

    /** <code>persistence.xml</code>をロードするクラスローダ */
    protected ClassLoader classLoader;

    /** <code>persistence.xml</code>を検証するXML Schema */
    protected Schema persistenceXmlSchema;

    /** S2コンテナ */
    @Binding(bindingType = BindingType.MUST)
    protected S2Container container;

    /** データソースのプロクシを使う場合は<code>true</code> */
    protected boolean useDataSourceProxy = Env.getValue().startsWith("ut");

    /** デフォルトの永続ユニットプロバイダクラス名 */
    protected String defaultProviderClassName = DEFAULT_PROVIDER;

    /** JTA用のデフォルトのデータソース名 */
    protected String defaultJtaDataSource = DEFAULT_DATASOURCE;

    /** 非JTA用のデフォルトのデータソース名 */
    protected String defaultNonJtaDataSource = DEFAULT_DATASOURCE;

    /**
     * コンテキストクラスローダを使用してインスタンスを構築します。
     * 
     */
    public PersistenceUnitInfoFactoryImpl() {
        this(Thread.currentThread().getContextClassLoader());
    }

    /**
     * 指定のクラスローダを使用してインスタンスを構築します。
     * 
     * @param classLoader
     *            <code>persistence.xml</code>をロードするクラスローダ
     */
    public PersistenceUnitInfoFactoryImpl(final ClassLoader classLoader) {
        this.classLoader = classLoader;
        final URL schemaUrl = classLoader.getResource(PERSISTENCE_SCHEMA_NAME);
        persistenceXmlSchema = SchemaUtil.newW3cXmlSchema(schemaUrl);
    }

    /**
     * データソースのプロクシを使う場合は<code>true</code>、それ以外の場合は<code>false</code>を設定します。
     * 
     * @param useDataSourceProxy
     *            データソースのプロクシを使う場合は<code>true</code>
     */
    @Binding(bindingType = BindingType.MAY)
    public void setUseDataSourceProxy(final boolean useDataSourceProxy) {
        this.useDataSourceProxy = useDataSourceProxy;
    }

    /**
     * デフォルトの永続ユニットプロバイダクラス名を設定します。
     * 
     * @param defaultProviderClassName
     *            デフォルトの永続ユニットプロバイダクラス名
     */
    @Binding(bindingType = BindingType.MAY)
    public void setDefaultProviderClassName(
            final String defaultProviderClassName) {
        this.defaultProviderClassName = defaultProviderClassName;
    }

    /**
     * JTA用のデフォルトのデータソース名を設定します。
     * 
     * @param defaultJtaDataSource
     *            JTA用のデフォルトのデータソース名
     */
    @Binding(bindingType = BindingType.MAY)
    public void setDefaultJtaDataSource(final String defaultJtaDataSource) {
        this.defaultJtaDataSource = defaultJtaDataSource;
    }

    /**
     * 非JTA用のデフォルトのデータソース名を設定します。
     * 
     * @param defaultNonJtaDataSource
     *            非JTA用のデフォルトのデータソース名
     */
    @Binding(bindingType = BindingType.MAY)
    public void setDefaultNonJtaDataSource(final String defaultNonJtaDataSource) {
        this.defaultNonJtaDataSource = defaultNonJtaDataSource;
    }

    public List<PersistenceUnitInfo> createPersistenceUnitInfo(
            final URL persistenceXmlUrl) {
        return createPersistenceUnitInfo(persistenceXmlUrl,
                toPersistenceUnitRootUrl(persistenceXmlUrl));
    }

    @SuppressWarnings("unchecked")
    public List<PersistenceUnitInfo> createPersistenceUnitInfo(
            final URL persistenceXmlUrl, final URL persistenceUnitRootUrl) {
        final SaxHandlerParser parser = createSaxHandlerParser(persistenceUnitRootUrl);
        final InputStream is = URLUtil.openStream(persistenceXmlUrl);
        try {
            return (List<PersistenceUnitInfo>) parser.parse(is,
                    persistenceUnitRootUrl.toExternalForm());
        } finally {
            InputStreamUtil.close(is);
        }
    }

    /**
     * <code>META-INF/persistence.xml</code>のURLから永続ユニットのルートURLを求めて返します。
     * 
     * @param url
     *            <code>META-INF/persistence.xml</code>のURL
     * @return 永続ユニットのルートURL
     */
    protected static URL toPersistenceUnitRootUrl(final URL url) {
        final String s = url.toExternalForm();
        if (!s.endsWith(PERSISTENCE_XML)) {
            throw new IllegalArgumentException(s);
        }
        final String rootUrl = s.substring(0, s.length()
                - PERSISTENCE_XML.length());
        return URLUtil.create(url, rootUrl);
    }

    /**
     * XML Schemaを使用して妥当性を検証する{@link SaxHandlerParser}を作成します。
     * 
     * @param persistenceUnitRootUrl
     *            永続ユニットのルートURL
     * @return XML Schemaを使用して妥当性を検証する{@link SaxHandlerParser}
     */
    protected SaxHandlerParser createSaxHandlerParser(
            final URL persistenceUnitRootUrl) {
        final SaxHandler handler = new SaxHandler(
                new PersistenceXmlTagHandlerRule());
        final TagHandlerContext ctx = handler.getTagHandlerContext();
        ctx.addParameter(PERSISTENCE_UNIT_ROOT_URL, persistenceUnitRootUrl);

        final SAXParserFactory factory = SAXParserFactoryUtil.newInstance();
        factory.setNamespaceAware(true);
        factory.setSchema(persistenceXmlSchema);
        final SAXParser saxParser = SAXParserFactoryUtil.newSAXParser(factory);

        return new SaxHandlerParser(handler, saxParser);
    }

    private class PersistenceXmlTagHandlerRule extends TagHandlerRule {

        private static final long serialVersionUID = 1L;

        private PersistenceXmlTagHandlerRule() {
            addTagHandler("/persistence", new PersistenceTagHandler());
            addTagHandler("persistence-unit", new PersistenceUnitTagHandler());
            addTagHandler("provider", new ProviderTagHandler());
            addTagHandler("jta-data-source", new JtaDataSourceTagHandler());
            addTagHandler("non-jta-data-source",
                    new NonJtaDataSourceTagHandler());
            addTagHandler("mapping-file", new MappingFileTagHandler());
            addTagHandler("jar-file", new JarFileTagHandler());
            addTagHandler("class", new ClassTagHandler());
            addTagHandler("exclude-unlisted-classes",
                    new ExcludeUnlistedClassesTagHandler());
            addTagHandler("property", new PropertyTagHandler());
        }

    }

    private class DefaultTagHandler extends TagHandler {

        private static final long serialVersionUID = 1L;

        /**
         * 永続ユニット情報を返します。
         * 
         * @param context
         *            コンテキスト
         * @return 永続ユニット情報
         */
        protected PersistenceUnitInfoImpl getPersistenceUnitInfo(
                final TagHandlerContext context) {
            return PersistenceUnitInfoImpl.class.cast(context.peek());
        }

        /**
         * データソースを返します。
         * 
         * @param name
         *            データソースのJNDI名
         * @return データソース
         */
        protected DataSource getDataSource(final String name) {
            if (useDataSourceProxy) {
                return new SingletonDataSourceProxy(name);
            }
            final String componentName = JndiResourceLocator.resolveName(name);
            return DataSource.class.cast(container.getComponent(componentName));
        }

        /**
         * 永続ユニットのルートURLを返します。
         * 
         * @param context
         *            コンテキスト
         * @return 永続ユニットのルートURL
         */
        protected URL getPersistenceUnitRootURL(final TagHandlerContext context) {
            return URL.class.cast(context
                    .getParameter(PERSISTENCE_UNIT_ROOT_URL));
        }

    }

    private class PersistenceTagHandler extends DefaultTagHandler {

        private static final long serialVersionUID = 1L;

        @Override
        public void start(final TagHandlerContext context,
                final Attributes attributes) {
            final List<PersistenceUnitInfo> list = CollectionsUtil
                    .newArrayList();
            context.push(list);
        }

    }

    private class PersistenceUnitTagHandler extends DefaultTagHandler {

        private static final long serialVersionUID = 1L;

        @Override
        public void start(final TagHandlerContext context,
                final Attributes attributes) {
            final URL persistenceUnitRootUrl = URL.class.cast(context
                    .getParameter(PERSISTENCE_UNIT_ROOT_URL));
            final PersistenceUnitInfoImpl info = new PersistenceUnitInfoImpl(
                    classLoader, persistenceUnitRootUrl);
            info.setPersistenceUnitName(attributes.getValue("name"));
            final String transactionType = attributes
                    .getValue("transaction-type");
            if (!StringUtil.isEmpty(transactionType)) {
                info.setTransactionType(PersistenceUnitTransactionType
                        .valueOf(transactionType));
            }
            context.push(info);
        }

        @SuppressWarnings("unchecked")
        @Override
        public void end(final TagHandlerContext context, final String body) {
            final PersistenceUnitInfoImpl info = PersistenceUnitInfoImpl.class
                    .cast(context.pop());
            if (StringUtil.isEmpty(info.getPersistenceProviderClassName())) {
                info.setPersistenceProviderClassName(defaultProviderClassName);
            }
            if (info.getJtaDataSource() == null) {
                info.setJtaDataSource(getDataSource(defaultJtaDataSource));
            }
            if (info.getNonJtaDataSource() == null) {
                info
                        .setNonJtaDataSource(getDataSource(defaultNonJtaDataSource));
            }
            final List<PersistenceUnitInfo> list = List.class.cast(context
                    .peek());
            list.add(info);
        }

    }

    private class ProviderTagHandler extends DefaultTagHandler {

        private static final long serialVersionUID = 1L;

        @Override
        public void end(final TagHandlerContext context, final String body) {
            getPersistenceUnitInfo(context).setPersistenceProviderClassName(
                    body);
        }

    }

    private class JtaDataSourceTagHandler extends DefaultTagHandler {

        private static final long serialVersionUID = 1L;

        @Override
        public void end(final TagHandlerContext context, final String body) {
            getPersistenceUnitInfo(context).setJtaDataSource(
                    getDataSource(body));
        }

    }

    private class NonJtaDataSourceTagHandler extends DefaultTagHandler {

        private static final long serialVersionUID = 1L;

        @Override
        public void end(final TagHandlerContext context, final String body) {
            getPersistenceUnitInfo(context).setNonJtaDataSource(
                    getDataSource(body));
        }

    }

    private class MappingFileTagHandler extends DefaultTagHandler {

        private static final long serialVersionUID = 1L;

        @Override
        public void end(final TagHandlerContext context, final String body) {
            getPersistenceUnitInfo(context).addMappingFileNames(body);
        }

    }

    @SuppressWarnings("unchecked")
    private class JarFileTagHandler extends DefaultTagHandler {

        private static final long serialVersionUID = 1L;

        @Override
        public void end(final TagHandlerContext context, final String body) {
            final URL url = URLUtil.create(getPersistenceUnitRootURL(context),
                    body);
            getPersistenceUnitInfo(context).addJarFileUrls(url);
        }

    }

    private class ClassTagHandler extends DefaultTagHandler {

        private static final long serialVersionUID = 1L;

        @Override
        public void end(final TagHandlerContext context, final String body) {
            getPersistenceUnitInfo(context).addManagedClassNames(body);
        }

    }

    private class ExcludeUnlistedClassesTagHandler extends DefaultTagHandler {

        private static final long serialVersionUID = 1L;

        @Override
        public void end(final TagHandlerContext context, final String body) {
            final String trimed = body.trim();
            final boolean value = "true".equals(trimed) || "1".equals(trimed);
            getPersistenceUnitInfo(context).setExcludeUnlistedClasses(value);
        }

    }

    private class PropertyTagHandler extends DefaultTagHandler {

        private static final long serialVersionUID = 1L;

        @Override
        public void start(final TagHandlerContext context,
                final Attributes attributes) {
            final String name = attributes.getValue("name");
            final String value = attributes.getValue("value");
            getPersistenceUnitInfo(context).addProperties(name, value);
        }

    }

}
