/*
 * SqlTemplate.java
 *
 * Brazil project web application toolkit,
 * export version: 2.3 
 * Copyright (c) 2000-2009 Sun Microsystems, Inc.
 *
 * Sun Public License Notice
 *
 * The contents of this file are subject to the Sun Public License Version 
 * 1.0 (the "License"). You may not use this file except in compliance with 
 * the License. A copy of the License is included as the file "license.terms",
 * and also available at http://www.sun.com/
 * 
 * The Original Code is from:
 *    Brazil project web application toolkit release 2.3.
 * The Initial Developer of the Original Code is: suhler.
 * Portions created by suhler are Copyright (C) Sun Microsystems, Inc.
 * All Rights Reserved.
 * 
 * Contributor(s): cstevens, suhler.
 *
 * Version:  2.11
 * Created by suhler on 00/05/08
 * Last modified by suhler on 09/08/18 10:35:26
 *
 * Version Histories:
 *
 * 2.11 09/08/18-10:35:26 (suhler)
 *   lint
 *
 * 2.10 09/08/18-10:28:50 (suhler)
 *   redo to support both static and dynamic database connections
 *   D
 *
 * 2.9 09/08/11-15:23:57 (suhler)
 *   allow explicit connection?
 *
 * 2.8 08/07/24-16:49:25 (suhler)
 *   preserve token accumulation state
 *
 * 2.7 05/05/11-14:20:52 (suhler)
 *   - added type=query|system|update to <sql> to run executeQuery(),
 *   execute(), and executeUpdate() respectively
 *   - added timeout=[seconds] to <sql>
 *   - fixed "prefix" to eliminate redundant '.'s in property named
 *
 * 2.6 04/11/30-15:19:42 (suhler)
 *   fixed sccs version string
 *
 * 2.5 04/08/30-09:01:53 (suhler)
 *   "enum" became a reserved word, change to "enumer".
 *
 * 2.4 04/04/28-15:57:13 (suhler)
 *   added attributes for spedifying n/a values and 0 or 1 based indexing
 *
 * 2.3 03/08/21-12:46:28 (suhler)
 *   typo in "rowcount" property
 *
 * 2.2 03/07/07-14:45:16 (suhler)
 *   Merged changes between child workspace "/home/suhler/brazil/naws" and
 *   parent workspace "/net/mack.eng/export/ws/brazil/naws".
 *
 * 1.11.1.1 03/07/07-14:08:15 (suhler)
 *   use addClosingTag() convenience method
 *
 * 2.1 02/10/01-16:39:11 (suhler)
 *   version change
 *
 * 1.11 01/08/03-18:23:21 (suhler)
 *   remove training  ws from classnames before trying to instantiate
 *
 * 1.10 01/06/04-14:10:15 (suhler)
 *   package move
 *
 * 1.9 00/12/11-13:32:37 (suhler)
 *   add class=props for automatic property extraction
 *
 * 1.8 00/10/05-15:51:58 (cstevens)
 *   PropsTemplate.subst() and PropsTemplate.getProperty() moved to the Format
 *   class.
 *
 * 1.7 00/07/07-17:02:31 (suhler)
 *   remove System.out.println(s)
 *
 * 1.6 00/07/07-15:32:51 (suhler)
 *   doc fixes
 *
 * 1.5 00/07/06-15:49:42 (suhler)
 *   doc update
 *
 * 1.4 00/05/31-13:52:03 (suhler)
 *   name change
 *
 * 1.3 00/05/19-11:50:30 (suhler)
 *   redo of error processing - its still not right though
 *
 * 1.2 00/05/10-10:44:21 (suhler)
 *   doc updates
 *
 * 1.2 00/05/08-17:37:52 (Codemgr)
 *   SunPro Code Manager data about conflicts, renames, etc...
 *   Name history : 1 0 sql/SqlTemplate.java
 *
 * 1.1 00/05/08-17:37:51 (suhler)
 *   date and time created 00/05/08 17:37:51 by suhler
 *
 */

package sunlabs.brazil.sql;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Enumeration;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Hashtable;

import sunlabs.brazil.util.StringMap;
import sunlabs.brazil.util.Format;
import sunlabs.brazil.server.Server;
import sunlabs.brazil.template.Template;
import sunlabs.brazil.template.RewriteContext;

/**
 * Sample Template class for running SQL queries via jdbc and
 * placing the results into the request properties for further processing.
 * <p>
 * Foreach session, a connection is made to one or more sql database via jdbc.
 * Session reconnection is attempted if the server connection breaks.
 * An SQL query is issued, with the results populating the request properties.
 * <p>
 * There are two ways to use this template (and they may be used together): static and 
 * dynamic.  For static configurations, the jdbc URL's and additional properties are defined
 * in the configuration file, and database connections are represented by "tokens".  For the
 * dynamic onfigurations, the jdbc URL's are passed as attributes to the sql tag.
 * <p>
 * Configuration options:
 * <dl class=props>
 * <dt>drivers
 * <dd>A list of java classes that will be used as JDBC drivers for the
 * database connectors.  At least one driver should be provided.
 * <dt>databases
 * <dd>A list of tokens that represent databases that may be accessed by this template.
 * For each token, the properties:
 * <dl>
 * <dt><code>token.url</code>
 * <dd>Specifies a JDBC URL
 * to use to connect to the database. 
 * <dt><code>token.sqlPrefix</code>
 * <dd>Contains a list of tokens that represent
 * additional information that should be provided for a connection, in the
 * form of one or more "token.name", "token.value" pairs.
 * <dt><code>token.user</code>, <code>token.passwd</code>
 * <dd>The user and password associated with this connection.
 * If "user" is specified, then the tokens in "sqlPrefix" are ignored.
 * </dl>
 * For a discussion of how the tag attributes and how the results results map to properties,
 * {@link #tag_sql see below}.
 *
 * @author		Stephen Uhler
 * @version		2.11
 */

public class SqlTemplate extends Template  {
    Hashtable connections = new Hashtable();	// our database connections
    boolean initialized = false;

    /**
     * Gather up all the databases and their credentials.
     */

    public boolean
    init(RewriteContext hr) {
	super.init(hr);
	hr.addClosingTag("sql");
        if (initialized) {
            return (true);
	}
	initialized = true;
	hr.request.log(Server.LOG_DIAGNOSTIC, hr.prefix,
		"One time initialization");
        Properties props = hr.request.props;
	StringTokenizer st;

	// load any required drivers

	String drivers=hr.get("drivers", null);

	// (look at "driver" for backward compatibility)
	if (drivers == null) {
	    drivers=hr.get("driver", null);
	}

	if (drivers != null) {
	    st = new StringTokenizer(drivers);
	    while (st.hasMoreTokens()) {
		try {
		    String cls = st.nextToken();
		    Class.forName(cls);
		    hr.request.log(Server.LOG_DIAGNOSTIC, hr.prefix,
			"loading driver: (" + cls + ")");
				
		} catch (ClassNotFoundException e) {
		    hr.request.log(Server.LOG_WARNING,hr.prefix,e.getMessage());
		    continue;
		}
	    }
	} else {
	    hr.request.log(Server.LOG_WARNING, hr.prefix, 
		"No database drivers specified!");
	}

	// Create the static database connections

	String databases = hr.get("databases", hr.prefix);
	st = new StringTokenizer(hr.get("databases", hr.prefix));
	while (st.hasMoreTokens()) {
	    String token = trimDot(st.nextToken());
	    String url = props.getProperty(token + ".url");
	    String user = props.getProperty(token + ".user");
	    String passwd = props.getProperty(token + ".passwd");
	    Properties sqlProps = null;	// extra properties for connection

	    if (url == null) {
		hr.request.log(Server.LOG_WARNING, token,
			"missing url parameter");
		continue;
	    }

	    /*
	     * Get the extra properties passed to each sql connection
	     */

	    String pre = props.getProperty(token + ".sqlPrefix");
	    if (pre != null) {
		Enumeration enumer = props.propertyNames();
		int len= pre.length();
		sqlProps = new Properties();
		while(enumer.hasMoreElements()) {
		    String key = (String) enumer.nextElement();
		    if (key.startsWith(pre)) {
			sqlProps.put(key.substring(len),
				props.getProperty(key));
		    }
		}
	    }

	    // stash the connection info.

	    ConInfo con;
	    if (user != null) {
		con = new ConInfo(url, user, passwd);
	    } else {
		con = new ConInfo(url, sqlProps);
	    }
	    hr.request.log(Server.LOG_DIAGNOSTIC, token,
		    "Got sql connection for " + url + ": " + con);
	    connections.put(token, con);
	    // System.out.println("connections: " + con);
	}
	return (true);
    }

    /**
     * Replace the SQL query with the appropriate request properties.
     * Look for the following parameters:
     * <i>(NOTE - This interface is preliminary, and subject to change)</i>.
     * <dl>
     * <dt>debug	<dd>Include diagnostics in html comments
     * <dt>prefix	<dd>prefix to prepend to all results.
     *			    Defaults to the database token
     * <dt>database	<dd>the database to use.  This is either one of the database
     *			tokens used with the "databases" configuration
     *			parameter, or a fully qualified jdbc url (e.g.
     *			"jdbc:..." to use for this database.
     *			if the jdbc url form is used, the driver must have already
     *			been specified, and no optional parameters can be
     *			provided for the connection.
     * <dt>max		<dd>The max # of rows returned (default=100)
     * <dt>na		<dd>Value to reurn for NULL.  Defaults to "n/a"
     * <dt>type		<dd>The type of SQL command, one of "query", "system",
     *			or "update".  these values map to the JDBC calls
     *			executeQuery(), execute() and executeUpdate()
     *			respectively. Defaults to "query".
     * <dt>timeout	<dd>The number of seconds to wait for the query
     *			to finish.  Defaults to "0": wait forever
     * <dt>eval		<dd>If present, do ${...} to entire query. (see
     *			{@link sunlabs.brazil.util.Format#getProperty getProperty}).
     * <dt>zeroIndex	<dd>if true, row counts start at 0, not 1
     * <dt>index	<dd>If present, use column 1 as part of the name.
     *			    Otherwise, an index name is invented.
     * <dt>noTable	<dd>If true, the table name is not encoded as
     *			part of the result
     * <dt>close	<dd>If true, the database copnnection will be closed after the query conpletes.
     * </dl>
     * For all queries, the following properties (with the prefix prepended)
     * are set:
     * <dl>
     * <dt>columncount		<dd>The number of columns returned
     * <dt>rowcount		<dd>The number of rows returned 
     * </dl>
     * Foreach entry in the resultant table, its property is:
     * <code>${prefix}.${table_name}.${columname}.${key}</code>.  If
     * the <code>index</code> parameter is set, the key is the value of
     * the first column returned. Otherwise the key is the row number, 
     * and the additional property <code>${prefix}.rows</code> contains a
     * list of all the row numbers returned.
     */

    public void
    tag_sql(RewriteContext hr) {
	debug = hr.isTrue("debug");
	boolean eval  = hr.isTrue("eval");
	String type =  hr.get("type", "query");
	boolean useIndex  = hr.isTrue("index");
	boolean zeroIndex = hr.isTrue("zeroIndex");
        Properties props = hr.request.props;
	String na = hr.get("na", "n/a");
	String database = hr.get("database", hr.prefix);
	boolean shouldClose = hr.isTrue("close");
	boolean noTable = hr.isTrue("notable");
	String user = hr.get("user");
	String passwd = hr.get("passwd");

	String pre = hr.get("prefix");
	if (pre == null) {
	    pre = database==null && !database.startsWith("jdbc:") ?
		hr.prefix : database;
	}
        debug(hr);

	boolean was = hr.accumulate(false);
	hr.nextToken();
	String query = hr.getBody();
	hr.accumulate(was);
	hr.nextToken();	// eat the </sql>

	if (!pre.equals("") && pre.endsWith(".") == false) {
	    pre+= ".";
	}

	ConInfo conInfo = (ConInfo) connections.get(database);
	if (conInfo == null && database.startsWith("jdbc:")) {
	    if (user != null) {
		conInfo = new ConInfo(database, user, passwd);
	    } else {
		conInfo = new ConInfo(database, null);
	    }
	    connections.put(database, conInfo);
	    hr.request.log(Server.LOG_DIAGNOSTIC, pre,
		    "new dynamic sql connection " +  conInfo);
	}
	if (conInfo == null) {
	    String msg = "database undefined: (" + database + ")";
	    debug(hr, msg);
	    props.put(pre + "error", msg);
	    return;
	}
	int max = Format.stringToInt(hr.get("max", null), 100);
	int timeout = Format.stringToInt(hr.get("timeout", "0"), 0);

	if (eval) {
	    query = Format.subst(props, query);
	}
	debug(hr, query);

	Statement stmt = null;
	ResultSet result = null;
	int updateCount = -1;
	ResultSetMetaData meta = null;

	/*
	 * Now run the query, stuffing the results into the properties.
	 * This is pretty stupid right now.  The first column is used as
	 * the "index" if useIndex is set.  Otherwise a counter is used.
	 */

	try {
	    stmt = doSQL(conInfo.getConnection(), query, type, timeout);
	    result = stmt.executeQuery(query);
	    if (result != null) {
		meta = result.getMetaData();
	    }
	    updateCount = stmt.getUpdateCount();
	    props.put(pre + "updateCount", "" + updateCount);
	    if (meta == null || result==null) {
	       return;
	    }

	    int rows = 0;
	    int count = meta.getColumnCount();
	    StringBuffer list = null;
	    props.put(pre + "columncount", "" + count);

	    // column names

            StringBuffer columns = new StringBuffer();
	    String delim="";
	    for (int i=0;i<count;i++) {
		String cn = na;
		try {
		    cn = meta.getColumnName(i);
		} catch (SQLException e) {}
		props.put(pre + "columnName." + (i+1), cn);
		columns.append(delim).append(i+1);
		delim=" ";
	    }
	    props.put(pre + "columns", columns.toString());


	    if (!useIndex) {
		list = new StringBuffer();
	    }
	    while (result != null && result.next() && rows++ < max) {
		String first;	// name of property row
		if (useIndex) {
		    first = result.getString(1);
		} else {
		    first = "" + (zeroIndex ? rows-1 : rows);
		    list.append(first).append(" ");
		}
		for(int i=(useIndex?2:1); i<=count; i++) {
		    String name = deriveName(pre, meta, first, i, noTable);
		    String value = result.getString(i);
		    if (value == null) {
			value = na;
		    }
		    props.put(name,value);
		    // debug(hr,name + "=" + value);
		}
	    }
	    if (list != null) {
		props.put(pre + "rows", list.toString());
	    }
	    props.put(pre + "rowcount", "" + rows);
	} catch (SQLException e) {
	    props.put(pre + "error", e.getMessage());
	    props.put(pre + "state", e.getSQLState());
	    debug(hr, "Failed: " + e.getMessage() + "/" + e.getSQLState());
	    props.put(pre + "rowcount", "0");
	}

	if (shouldClose) {
	    conInfo.close();
	    debug(hr, "connection closed");
	}
    }

    public Statement
    doSQL(Connection con, String query, String type, int timeout)
	    throws SQLException {
	Statement stmt = con.createStatement();
	if (timeout > 0) {
	    stmt.setQueryTimeout(timeout);
	}
	if (type.equals("query")) {
	    stmt.executeQuery(query);
	} else if (type.equals("update")) {
	    stmt.executeUpdate(query);
	} else if (type.equals("system")) {
	    stmt.execute(query);
	}
	return stmt;
    }


    /**
     * Convenience method for deriving props names
     */

    String
    deriveName(String prefix, ResultSetMetaData meta, String suffix,
	    int i, boolean noTable) {
	String table = "";
	String column = "";
	if (!noTable) {
	    try {
		table = meta.getTableName(i);
	    } catch (SQLException e) {}
	}
	try {
	    column = meta.getColumnName(i);
	} catch (SQLException e) {}
	if (!column.equals("")) column += ".";
	if (!table.equals("")) table += ".";
	String result =  prefix + table + column + suffix;
	return result;
    }

    public void
    tag_slash_sql(RewriteContext hr) {
	hr.killToken();
    }

    String trimDot(String s) {
	return s.endsWith(".") ? s.substring(0, s.length()-1) : s;
    }

    /**
     * Stuff needed to create an SQL connection.
     */

    static class ConInfo {
	String url;	// the url for the connection
	Properties props;	// the extra properties for the connection
	Connection con;		// the resultant connection
	String user, passwd;	// name and password (if required)

	ConInfo(String url, Properties props) {
	    this.url=url;
	    this.props = props;
	    this.con=null;
	    user=null;
	    passwd=null;
	}

	ConInfo(String url, String user, String passwd) {
	    this.url=url;
	    this.props = null;
	    this.con=null;
	    this.user = user;
	    this.passwd = passwd;
	}


	/**
	 * Connect to the database
	 */

	Connection getConnection()  throws SQLException {
	    if (con==null || con.isClosed()) {
		try {
		    // System.out.println("Connecting to ...: " + url);
		    if (user != null) {
			con = DriverManager.getConnection(url, user, passwd);
		    } else {
			con = DriverManager.getConnection(url, props);
		    }
		    // System.out.println("Connection created: " + con);
		} catch (SQLException e) {
		    throw(e);
		} catch (Exception e) {
		    // System.out.println("Exception in SQL setup: " + e);
		    throw new SQLException("SQL setup error: " + e);
		}
	    }
	    return con;
	}

	public void close() {
	    try {
		con.close();
	    } catch (SQLException e) {}
	}

	public String toString() {
	    return "[" + url + " (" +
		(user != null ? user + "/" + passwd : props.toString()) +
		") " + con + "]";
	}
    }
}