/*
 * blancoDb
 * Copyright (C) 2004-2005 Yasuo Nakanishi
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 */
package blanco.db.collector;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.dom.DOMResult;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import blanco.commons.util.BlancoBigDecimalUtil;
import blanco.commons.util.BlancoStringUtil;
import blanco.commons.util.BlancoXmlUtil;
import blanco.db.conf.AbstractQuery;
import blanco.db.conf.BlancoDbMetadata;
import blanco.db.conf.CallQuery;
import blanco.db.conf.ExecuteQuery;
import blanco.db.conf.SelectQuery;
import blanco.db.definition.BlancoDbDefinition;
import blanco.db.definition.FieldFactory;
import blanco.db.definition.QueryCaller;
import blanco.db.definition.QueryInvoker;
import blanco.db.definition.QueryIterator;
import blanco.db.mapping.BlancoDbMappingUtil;
import blanco.db.resourcebundle.BlancoDbResourceBundle;
import blanco.db.util.BlancoDbQueryParserUtil;
import blanco.ig.expander.Type;
import blanco.ig.expander.Value;

/**
 * @author Yasuo Nakanishi
 */
public class BlancoDbCollector {
    /**
     * デバッグモードとして動作させるかどうか。通常はfalseで運用します。
     */
    private static final boolean IS_DEBUG = false;

    /**
     * blancoDbリソースバンドル情報へのアクセサ。
     */
    private final BlancoDbResourceBundle bundle = new BlancoDbResourceBundle();

    /**
     * 共通情報が蓄えられているエレメント名。
     */
    private static final String ELEMENT_COMMON = "blancodb-common";

    /**
     * SQL入力パラメータが蓄えられているエレメント名。
     */
    private static final String ELEMENT_INPARAMETERS = "blancodb-inparameters";

    /**
     * SQL出力パラメータが蓄えられているエレメント名。
     */
    private static final String ELEMENT_OUTPARAMETERS = "blancodb-outparameters";

    /**
     * SQL文が蓄えられているエレメント名。
     */
    private static final String ELEMENT_QUERY = "blancodb-query";

    /**
     * SQL定義書の中間XMLを入力に、SQL定義書情報を収集します。
     * 
     * @param metadata
     *            中間XMLファイル。
     * @param conn
     *            blancoデータベース接続オブジェクト。
     * @return blancoDb定義情報。
     * @throws SQLException
     *             SQL例外が発生した場合。
     * @throws SAXException
     *             SAX例外が発生した場合。
     * @throws IOException
     *             入出力例外が発生した場合。
     * @throws ParserConfigurationException
     *             パーサコンフィグレーション例外が発生した場合。
     * @throws TransformerException
     *             XML変換例外が発生した場合。
     */
    public BlancoDbDefinition collect(final BlancoDbMetadata metadata,
            final BlancoDbDatabaseConnection conn) throws SQLException,
            SAXException, IOException, ParserConfigurationException,
            TransformerException {
        // 収集後のblancoDb定義情報について初期化を行います。
        final BlancoDbDefinition blancoDbDef = new BlancoDbDefinition("NoName");
        blancoDbDef.setIteratorList(new ArrayList());
        blancoDbDef.setInvokerList(new ArrayList());
        blancoDbDef.setCallerList(new ArrayList());

        // XMLファイルをDOMとしてパースします。
        InputStream inStream = null;
        final DOMResult result;
        try {
            inStream = new BufferedInputStream(new FileInputStream(metadata
                    .getXml()));
            result = BlancoXmlUtil.transformStream2Dom(inStream);
        } finally {
            inStream.close();
        }

        // ルートノードを取得します。
        final Node nodeRootNode = result.getNode();
        if (nodeRootNode == null) {
            if (IS_DEBUG) {
                System.out.println("TRACE: XMLファイルではありません。");
            }
            return blancoDbDef;
        }

        final Element eleWorkbook = BlancoXmlUtil.getElement(nodeRootNode,
                "workbook");
        if (eleWorkbook == null) {
            return blancoDbDef;
        }

        // シートエレメントを展開します。
        final NodeList listSheet = eleWorkbook.getElementsByTagName("sheet");
        if (listSheet == null) {
            if (IS_DEBUG) {
                System.out.println("TRACE: シートが見つかりません。");
            }
            return blancoDbDef;
        }

        // おのおののシートエレメントについて処理を行います。
        final int nodeLength = listSheet.getLength();
        for (int index = 0; index < nodeLength; index++) {
            final Node nodeSheet = listSheet.item(index);
            if (nodeSheet instanceof Element == false) {
                continue;
            }
            final Element eleSheet = (Element) nodeSheet;
            expandSheet(eleSheet, conn, blancoDbDef);
        }

        return blancoDbDef;
    }

    /**
     * 与えられたシートエレメントを展開します。
     * 
     * @param eleSheet
     *            シートエレメント。
     * @param conn
     *            blancoデータベース接続オブジェクト。
     * @param blancoDbDef
     *            blancoDb定義情報。
     */
    private void expandSheet(final Element eleSheet,
            final BlancoDbDatabaseConnection conn,
            final BlancoDbDefinition blancoDbDef) {
        // 最初に共通情報を展開します。
        final AbstractQuery abstractQuery = expandCommon(eleSheet);
        if (abstractQuery == null) {
            // SQL定義書としてふさわしくないので、処理をスキップします。
            if (IS_DEBUG) {
                System.out.println("TRACE: SQL定義書としてはふさわしくないと判断します。");
            }
            return;
        }

        // SQL入力パラメータを展開します。
        expandInParameter(eleSheet, abstractQuery);

        // SQL出力パラメータを展開します。
        expandOutParameter(eleSheet, abstractQuery);

        // SQL文を展開します。
        expandQuery(eleSheet, abstractQuery);

        // ひととおりの情報をそろえた上で、収集した情報の妥当性チェックをおこないます。
        if (abstractQuery.getQuery() == null
                || abstractQuery.getQuery().length() == 0) {
            // SQL文が取得できないものはエラー扱いします。
            throw new IllegalArgumentException(bundle
                    .getXml2javaclassErr001(abstractQuery.getName()));
        }

        // 収集情報を Iterator, Invoker, Callerといったクラスのオブジェクトへと詰め替えます。
        if (abstractQuery instanceof SelectQuery) {
            try {
                // QueryIteratorの場合には、データベースに接続して情報を収集します。
                getQueryFields(conn.getConnection(),
                        (SelectQuery) abstractQuery);
            } catch (SQLException e) {
                throw new IllegalArgumentException(bundle
                        .getXml2javaclassErr002(abstractQuery.getName(), e
                                .getSQLState(), BlancoBigDecimalUtil
                                .toBigDecimal(e.getErrorCode()), e.toString()));
            }
            blancoDbDef.addQueryIterator(new QueryIterator(
                    (SelectQuery) abstractQuery));
        } else if (abstractQuery instanceof ExecuteQuery) {
            blancoDbDef.addQueryInvoker(new QueryInvoker(
                    (ExecuteQuery) abstractQuery));
        } else if (abstractQuery instanceof CallQuery) {
            blancoDbDef.addQueryCaller(new QueryCaller(
                    (CallQuery) abstractQuery));
        } else {
            throw new IllegalArgumentException(bundle.getXml2javaclassErr003(
                    abstractQuery.toString(), abstractQuery.getClass()
                            .toString()));
        }

        // 最後にblancoDb定義情報に対して収集後の情報を追加します。
        if (abstractQuery instanceof SelectQuery) {
            blancoDbDef.addQueryIterator(new QueryIterator(
                    (SelectQuery) abstractQuery));
        } else if (abstractQuery instanceof ExecuteQuery) {
            blancoDbDef.addQueryInvoker(new QueryInvoker(
                    (ExecuteQuery) abstractQuery));
        } else if (abstractQuery instanceof CallQuery) {
            blancoDbDef.addQueryCaller(new QueryCaller(
                    (CallQuery) abstractQuery));
        }
    }

    /**
     * 与えられた共通エレメントを解析して情報を展開します。
     * 
     * @param elementSheet
     *            シートオブジェクト。
     * @return 抽象クエリオブジェクト。
     */
    private AbstractQuery expandCommon(final Element eleSheet) {
        final Element elementCommon = BlancoXmlUtil.getElement(eleSheet,
                ELEMENT_COMMON);
        if (elementCommon == null) {
            // ELEMENT_COMMONが見つからない場合には、このシートをスキップします。
            if (IS_DEBUG) {
                System.out.println("TRACE: " + ELEMENT_COMMON + "が見つかりません。");
            }

            return null;
        }

        final String name = BlancoStringUtil.null2Blank(BlancoXmlUtil
                .getTextContent(elementCommon, "name"));
        if (name.length() == 0) {
            // この定義書は処理の必要がありません。処理しないこととします。
            // 従来はquery-typeが無指定の場合にも処理をスキップしていましたが、現在はエラー扱いとします。
            if (IS_DEBUG) {
                System.out.println("TRACE: " + ELEMENT_COMMON
                        + "にnameが見つかりません。");
            }
            return null;
        }

        final String description = BlancoStringUtil.null2Blank(BlancoXmlUtil
                .getTextContent(elementCommon, "description"));
        final String queryType = BlancoStringUtil.null2Blank(BlancoXmlUtil
                .getTextContent(elementCommon, "query-type"));
        final boolean isSingle = BlancoStringUtil.null2Blank(
                BlancoXmlUtil.getTextContent(elementCommon, "single")).equals(
                "true");
        final String scroll = BlancoStringUtil.null2Blank(BlancoXmlUtil
                .getTextContent(elementCommon, "scroll"));
        final boolean updatable = BlancoStringUtil.null2Blank(
                BlancoXmlUtil.getTextContent(elementCommon, "updatable"))
                .equals("true");

        // クエリ型にあわせてオブジェクトを新規作成します。
        AbstractQuery abstractQuery = null;
        if (queryType.equals("iterator")) {
            abstractQuery = new SelectQuery(name);
            ((SelectQuery) abstractQuery).setScrollInterface(scroll);
            ((SelectQuery) abstractQuery)
                    .setEnableUpdatableInterface(updatable);
        } else if (queryType.equals("invoker")) {
            abstractQuery = new ExecuteQuery(name);
        } else if (queryType.equals("caller")) {
            abstractQuery = new CallQuery(name);
        } else {
            // 処理できません。中断します。
            throw new IllegalArgumentException("サポートしないクエリタイプ[" + queryType
                    + "]が与えられました。処理中断します。");
        }

        // クエリ型を通じて共通の情報を設定します。
        abstractQuery.setDescription(description);
        abstractQuery.setSingle(isSingle);

        return abstractQuery;
    }

    /**
     * 与えられたシートを解析して情報を展開します。
     * 
     * @param elementSheet
     *            シートオブジェクト。
     * @param abstractQuery
     *            抽象クエリオブジェクト。
     */
    private void expandInParameter(final Element elementSheet,
            final AbstractQuery abstractQuery) {
        final Element elementBlancoDbInparameters = BlancoXmlUtil.getElement(
                elementSheet, ELEMENT_INPARAMETERS);
        if (elementBlancoDbInparameters == null) {
            return;
        }

        final NodeList nodeList = elementBlancoDbInparameters
                .getElementsByTagName("inparameter");
        if (nodeList == null) {
            if (IS_DEBUG) {
                System.out.println("TRACE: SQL入力パラメータはありません。");
            }
            return;
        }
        final int nodeLength = nodeList.getLength();
        for (int index = 0; index < nodeLength; index++) {
            final Node nodeLook = nodeList.item(index);
            if (nodeLook instanceof Element == false) {
                continue;
            }
            final String no = BlancoXmlUtil.getTextContent((Element) nodeLook,
                    "no");
            final String name = BlancoXmlUtil.getTextContent(
                    (Element) nodeLook, "name");
            final String type = BlancoXmlUtil.getTextContent(
                    (Element) nodeLook, "type");
            // 現在、descriptionは格納先がありません。
            // final String description = BlancoXmlUtil.getTextContent(
            // (Element) nodeLook, "description");

            final String paramNoString = (no == null ? "" : " No.[" + no + "] ");
            if (name == null || name.length() == 0) {
                throw new IllegalArgumentException(bundle
                        .getXml2javaclassErr004(abstractQuery.getName(),
                                paramNoString, type));
            }
            if (type == null || type.length() == 0) {
                throw new IllegalArgumentException(bundle
                        .getXml2javaclassErr005(abstractQuery.getName(),
                                paramNoString, name));
            }

            final Value value = new Value(new Type(type), name);
            abstractQuery.addInParameter(value);
        }
    }

    /**
     * 与えられたシートを解析して情報を展開します。
     * 
     * @param elementSheet
     *            シートオブジェクト。
     * @param abstractQuery
     *            抽象クエリオブジェクト。
     */
    private void expandOutParameter(final Element elementSheet,
            final AbstractQuery abstractQuery) {
        final Element elementBlancoDbOutparameters = BlancoXmlUtil.getElement(
                elementSheet, ELEMENT_OUTPARAMETERS);
        if (elementBlancoDbOutparameters == null) {
            if (IS_DEBUG) {
                System.out.println("TRACE: SQL出力パラメータはありません。");
            }
            return;
        }

        final NodeList nodeList = elementBlancoDbOutparameters
                .getElementsByTagName("outparameter");
        if (nodeList == null) {
            return;
        }
        final int nodeLength = nodeList.getLength();
        for (int index = 0; index < nodeLength; index++) {
            final Node nodeLook = nodeList.item(index);
            if (nodeLook instanceof Element == false) {
                continue;
            }
            final String no = BlancoXmlUtil.getTextContent((Element) nodeLook,
                    "no");
            final String name = BlancoXmlUtil.getTextContent(
                    (Element) nodeLook, "name");
            final String type = BlancoXmlUtil.getTextContent(
                    (Element) nodeLook, "type");
            // 現在、descriptionは格納先がありません。
            // final String description = BlancoXmlUtil.getTextContent(
            // (Element) nodeLook, "description");

            final String paramNoString = (no == null ? "" : " No.[" + no + "] ");
            if (name == null || name.length() == 0) {
                throw new IllegalArgumentException(bundle
                        .getXml2javaclassErr006(abstractQuery.getName(),
                                paramNoString, type));
            }
            if (type == null || type.length() == 0) {
                throw new IllegalArgumentException(bundle
                        .getXml2javaclassErr007(abstractQuery.getName(),
                                paramNoString, name));
            }
            if (abstractQuery instanceof CallQuery == false) {
                throw new IllegalArgumentException(bundle
                        .getXml2javaclassErr008(abstractQuery.getName(),
                                paramNoString, name));
            }

            final Value value = new Value(new Type(type), name);
            ((CallQuery) abstractQuery).addOutParameter(value);
        }
    }

    /**
     * 与えられたシートを解析して情報を展開します。
     * 
     * @param elementSheet
     *            シートオブジェクト。
     * @param abstractQuery
     *            抽象クエリオブジェクト。
     */
    private void expandQuery(final Element elementSheet,
            final AbstractQuery abstractQuery) {
        final Element elementBlancoDbInparameters = BlancoXmlUtil.getElement(
                elementSheet, ELEMENT_QUERY);
        if (elementBlancoDbInparameters == null) {
            return;
        }

        final NodeList nodeList = elementBlancoDbInparameters
                .getElementsByTagName("query-line");
        if (nodeList == null) {
            return;
        }
        final int nodeLength = nodeList.getLength();
        for (int index = 0; index < nodeLength; index++) {
            final String queryLine = BlancoStringUtil.null2Blank(BlancoXmlUtil
                    .getTextContent(nodeList.item(index)));

            String query = abstractQuery.getQuery();
            if (query == null || query.length() == 0) {
                query = "";
            } else {
                // 文字列がすでに存在している場合にのみ改行を付与します。
                query = query + "\n";
            }
            abstractQuery.setQuery(query + queryLine);
        }
    }

    /**
     * SQL文を実行した結果の検索結果列一覧を取得して、列情報を記憶します。
     * 
     * JDBCの機能を利用してSQL文を試し打ちし、検索結果の列一覧を取得するという、blancoDbの核ともいえる機能の実装箇所です。
     * 
     * @param conn
     *            blancoデータベース接続オブジェクト。
     * @param selectQuery
     *            QueryIteratorのための情報。
     * @throws SQLException
     *             SQL例外が発生した場合。
     */
    private final void getQueryFields(final Connection conn,
            final SelectQuery selectQuery) throws SQLException {
        // SQL文をパースするためのユーティリティ。
        final BlancoDbQueryParserUtil parserUtil = new BlancoDbQueryParserUtil(
                selectQuery.getQuery());

        PreparedStatement statement = null;
        ResultSet resultSet = null;

        final FieldFactory fieldFactory = new FieldFactory();
        try {
            statement = conn.prepareStatement(parserUtil.getNaturalSqlString());

            // ここでSQL入力パラメータへのバインド処理を実施します。
            // 2005.05.20 t.iga
            for (Iterator iteParameter = selectQuery.getInParameterIterator(); iteParameter
                    .hasNext();) {
                final Value val = (Value) iteParameter.next();
                int[] listCol = parserUtil.getSqlParameters(val.getName());
                if (listCol == null) {
                    throw new IllegalArgumentException(bundle
                            .getXml2javaclassErr009(selectQuery.getName(), val
                                    .getName()));
                }
                for (int iteSame = 0; iteSame < listCol.length; iteSame++) {
                    final int index = listCol[iteSame];
                    BlancoDbMappingUtil.processPreparedStatementWithSomeValue(
                            val.getType().getName(), statement, index);
                }
            }

            resultSet = statement.executeQuery();

            final ResultSetMetaData meta = resultSet.getMetaData();
            for (int i = 0; i < meta.getColumnCount(); i++) {
                selectQuery
                        .addField(fieldFactory.createQueryField(i + 1, meta));
            }
        } finally {
            if (resultSet != null) {
                resultSet.close();
            }
            if (statement != null) {
                statement.close();
            }
        }
    }
}