/*
 * Helma License Notice
 *
 * The contents of this file are subject to the Helma License
 * Version 2.0 (the "License"). You may not use this file except in
 * compliance with the License. A copy of the License is available at
 * http://adele.helma.org/download/helma/license.txt
 *
 * Copyright 1998-2006 Helma Software. All Rights Reserved.
 *
 * $RCSfile: Database.js,v $
 * $Author: hannes $
 * $Revision: 10001 $
 * $Date: 2009-11-30 15:42:24 +0100 (Mon, 30. Nov 2009) $
 */

/**
 * @fileoverview Properties and methods of the helma.Database prototype.
 * <br /><br />
 * To use this optional module, its repository needs to be added to the 
 * application, for example by calling app.addRepository('modules/helma/Database.js')
 */

if (!global.helma) {
    global.helma = {};
}

/**
 * Constructor for Database objects, providing access through relational
 * databases through JDBC. It is usually simpler to use one of the factory
 * methods {@link #createInstance} or {@link #getInstance}.
 * @class <p>This class provides access to  a relational database through JDBC.
 * There are two convenient ways to create instances of this class.</p>
 *
 * <p>The first is to use {@link #getInstance helma.Database.getInstance()}
 * to obtain a connection to a DB that is defined in the application's
 * db.properties and managed by Helma. The second way is to define and create
 * a database connection locally using
 * {@link #createInstance helma.Database.createInstance()} and passing it
 * all necessary parameters.</p>
 *
 * <p>This class provides two ways of interaction:
 * The {@link #query} method allows to issue SQL queries, returning a result set.
 * The {@link #execute} provides a way to issue SQL statements that do not
 * return a result set.</p>
 *
 * <p>Database connections allocated by this class are be managed and eventually
 * disposed by Helma.</p>
 *
 * @param {DbSource} source instance of a helma.objectmodel.db.DbSource
 * @constructor
 */
helma.Database = function(source) {
    var Types = java.sql.Types;
    var DbSource = Packages.helma.objectmodel.db.DbSource;

    if (typeof(source) == "string")
        source = app.getDbSource(source);
    if (!(source instanceof DbSource))
        throw "helma.Database requires a helma.objectmodel.db.DbSource argument";

    /**
     * Get the java.sql.Connection for this Database instance. This can be used
     * to operate on the connection directly, without going through the helma.Database
     * class.
     * @return {java.sql.Connection} the JDBC connection
     */
    this.getConnection = function() {
        return source.getConnection();
    };

    /**
     * Returns the lower case name of the underlying database product.
     * @return {String} the name of the DB product
     */
    this.getProductName = function() {
        return source.getConnection().getMetaData().getDatabaseProductName().toLowerCase();
    };

    /**
     * Returns true if this is an Oracle database.
     * @return {boolean} true if this is an Oracle database.
     */
    this.isOracle = function() {
        return source.isOracle();
    };

    /**
     * Returns true if this is a MySQL database.
     * @return {boolean} true if this is an MySQL database. 
     */
    this.isMySql = function() {
        return source.isMySQL();
    };

    /**
     * Returns true if this is a PostgreSQL database.
     * @return {boolean} true if this is a PostgreSQL database.
     */
    this.isPostgreSql = function() {
        return source.isPostgreSQL();
    };

    /**
     * Executes the given SQL statement. The result set is returned
     * as JavaScript Array containing a JavaScript Object for each result.
     * @param {String} sql an SQL query statement
     * @return {Array} an Array containing the result set
     */
    this.query = function(sql) {
        var isLogSqlEnabled = (getProperty("logSQL", "false").toLowerCase() == "true");
        var logTimeStart = isLogSqlEnabled ? java.lang.System.currentTimeMillis() : 0;
        var connection = source.getConnection();
        connection.setReadOnly(true);
        var statement = connection.createStatement();
        var resultSet = statement.executeQuery(sql);
        var metaData = resultSet.getMetaData();
        var max = metaData.getColumnCount();
        var types = [];
        for (var i=1; i <= max; i++) {
            types[i] = metaData.getColumnType(i);
        }
        var result = [];
        while (resultSet.next()) {
            var row = {}
            for (var i=1; i<=max; i+=1) {
                switch (types[i]) {
                    case Types.BIT:
                    case Types.BOOLEAN:
                        row[metaData.getColumnLabel(i)] = resultSet.getBoolean(i);
                        break;
                    case Types.TINYINT:
                    case Types.BIGINT:
                    case Types.SMALLINT:
                    case Types.INTEGER:
                        row[metaData.getColumnLabel(i)] = resultSet.getLong(i);
                        break;
                    case Types.REAL:
                    case Types.FLOAT:
                    case Types.DOUBLE:
                    case Types.DECIMAL:
                    case Types.NUMERIC:
                        row[metaData.getColumnLabel(i)] = resultSet.getDouble(i);
                        break;
                    case Types.VARBINARY:
                    case Types.BINARY:
                    case Types.LONGVARBINARY:
                    case Types.LONGVARCHAR:
                    case Types.CHAR:
                    case Types.VARCHAR:
                    case Types.CLOB:
                    case Types.OTHER:
                        row[metaData.getColumnLabel(i)] = resultSet.getString(i);
                        break;
                    case Types.DATE:
                    case Types.TIME:
                    case Types.TIMESTAMP:
                        row[metaData.getColumnLabel(i)] = resultSet.getTimestamp(i);
                        break;
                    case Types.NULL:
                        row[metaData.getColumnLabel(i)] = null;
                        break;
                    default:
                        row[metaData.getColumnLabel(i)] = resultSet.getString(i);
                        break;
                }
            }
            result[result.length] = row;
        }
        var logTimeStop = isLogSqlEnabled ? java.lang.System.currentTimeMillis() : 0;
        if (isLogSqlEnabled) {
            var tableName = metaData.getColumnCount() > 0 ? metaData.getTableName(1) : null;
            app.getLogger("helma." + app.name + ".sql").info("SQL DIRECT_QUERY " + (tableName || "-") + " " + (logTimeStop - logTimeStart) + ": " + sql);
        }
        try {
            statement.close();
            resultSet.close();
        } catch (error) {
            // ignore
        }
        return result;
    };

    /**
     * Executes the given SQL statement, which may be an INSERT, UPDATE,
     * or DELETE statement or an SQL statement that returns nothing,
     * such as an SQL data definition statement. The return value is an integer that
     * indicates the number of rows that were affected by the statement.
     * @param {String} sql an SQL statement
     * @return {int} either the row count for INSERT, UPDATE or
     * DELETE statements, or 0 for SQL statements that return nothing
     */
    this.execute = function(sql) {
        var isLogSqlEnabled = (getProperty("logSQL", "false").toLowerCase() == "true");
        var logTimeStart = isLogSqlEnabled ? java.lang.System.currentTimeMillis() : 0;
        var connection = source.getConnection();
        connection.setReadOnly(false);
        var statement = connection.createStatement();
        var result;
        try {
            result = statement.executeUpdate(sql);
        } finally {
            try {
                statement.close();
            } catch (error) {
                // ignore
            }
        }
        var logTimeStop = isLogSqlEnabled ? java.lang.System.currentTimeMillis() : 0;
        if (isLogSqlEnabled) {
            app.getLogger("helma." + app.name + ".sql").info("SQL DIRECT_EXECUTE - " + (logTimeStop - logTimeStart) + ": " + sql);
        }
        return result;
    };

    /**
     * Return the name of the Helma DbSource object.
     * @return {String} the DbSource name
     */
    this.getName = function() {
        return source.getName();
    };

    /**
     * Return the name of the JDBC driver used by this Database instance.
     * @return {String} the JDBC driver name
     */
    this.getDriverName = function() {
        return source.getDriverName();
    };

    /**
     * @ignore
     */
    this.toString = function() {
        return "[helma.Database " + this.getName() + "]";
    };

    for (var i in this)
        this.dontEnum(i);

    return this;
};

/**
 * Create a new Database instance using the given parameters.
 * <p>Some of the parameters support shortcuts for known database products.
 * The <code>url</code> parameter recognizes the values "mysql", "oracle" and
 * "postgresql". For those databases, it is also possible to pass just
 * <code>hostname</code> or <code>hostname:port</code> as <code>url</code>
 * parameters instead of the full JDBC URL.</p>
 * @param {String} driver the class name of the JDBC driver. As
 * shortcuts, the values "mysql", "oracle" and "postgresql" are
 * recognized.
 * @param {String} url the JDBC URL.
 * @param {String} name the name of the database to use
 * @param {String} user the the username
 * @param {String} password the password
 * @return {helma.Database} a helma.Database instance
 */
helma.Database.createInstance = function(driver, url, name, user, password) {
    var DbSource = Packages.helma.objectmodel.db.DbSource;

    if (!driver || !url || !name)
        throw("Insufficient arguments to create helma.db.Connection");
    if (typeof password != "string")
        password = "";

    var MYSQL = "mysql";
    var ORACLE = "oracle";
    var POSTGRESQL = "postgresql";
    var JDBC = "jdbc:";
    var DRIVER_MYSQL = "com.mysql.jdbc.Driver";
    var DRIVER_ORACLE = "oracle.jdbc.driver.OracleDriver";
    var DRIVER_POSTGRESQL = "org.postgresql.Driver";

    if (driver == MYSQL) {
        driver = DRIVER_MYSQL;
        if (url.indexOf(JDBC) != 0)
            url = "jdbc:mysql://" + url + "/" + name;
    } else if (driver == ORACLE) {
        driver = DRIVER_ORACLE;
        if (url.indexOf(JDBC) != 0)
            url = "jdbc:oracle:thin:@" + url + ":" + name;
    } else if (driver == POSTGRESQL) {
        driver = DRIVER_POSTGRESQL;
        if (url.indexOf(JDBC) != 0)
            url = "jdbc:postgresql://" + url + "/" + name;
    }
    var props = new Packages.helma.util.ResourceProperties();
    props.put(name + ".url", url);
    props.put(name + ".driver", driver);
    if (user) {
        props.put(name + ".user", user)
    }
    if (password) {
        props.put(name + ".password", password);
    }
    return new helma.Database(new DbSource(name, props));
}

/**
 * Get a Database instance using the Database source defined in the
 * application's db.properties file with the given name.
 * @param {String} name the name of the DB source as defined in db.properties
 * @return {helma.Database} a helma.Database instance
 */
helma.Database.getInstance = function(name) {
    return new helma.Database(app.getDbSource(name));
}

/**
 * @ignore
 */
helma.Database.toString = function() {
    return "[helma.Database]";
};

/**
 * @ignore
 */
helma.Database.example = function() {
    var type = "mysql";
    var host = "localhost";
    var user = "root";
    var pw = "";
    var name = "mysql";
    var db = new helma.Database(type, host, user, pw, name);
    var result = db.query("select count(*) from db");
    res.write(result.toSource());
    return;
};


helma.lib = "Database";
helma.dontEnum(helma.lib);
for (var i in helma[helma.lib])
    helma[helma.lib].dontEnum(i);
for (var i in helma[helma.lib].prototype)
    helma[helma.lib].prototype.dontEnum(i);
delete helma.lib;
