/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.
 */

/*
 * Copyright (C) The Apache Software Foundation. All rights reserved.
*/

package com.klopotek.utils.log;

import java.sql.*;
import java.util.*;
import org.apache.log4j.*;
import org.apache.log4j.helpers.*;
import org.apache.log4j.spi.*;


/**
This class encapsulate the logic which is necessary to log into a table.
Used by JDBCAppender

<p><b>Author : </b><A HREF="mailto:t.fenner@klopotek.de">Thomas Fenner</A></p>

@since 1.0
*/
public class JDBCLogger
{
	//All columns of the log-table
	private ArrayList logcols = null;
   //Only columns which will be provided by logging
   private String column_list = null;
   //Number of all columns
	private int num = 0;
   //Status for successful execution of method configure()
	private boolean isconfigured = false;
   //Status for ready to do logging with method append()
	private boolean ready = false;
   //This message will be filled with a error-string when method ready() failes, and can be got by calling getMsg()
   private String errormsg = "";

	private Connection con = null;
	private Statement stmt = null;
	private ResultSet rs = null;
   private String table = null;

   //Variables for static SQL-statement logging
   private String sql = null;
	private String new_sql = null;
   private String new_sql_part1 = null;
   private String new_sql_part2 = null;
   private static final String msg_wildcard = "@MSG@";
	private int msg_wildcard_pos = 0;

	/**
	Writes a message into the database table.
	Throws an exception, if an database-error occurs !
	*/
	public void append(String _msg) throws Exception
	{
		if(!ready) if(!ready()) throw new Exception("JDBCLogger::append(), Not ready to append !");

      if(sql != null)
      {
      	appendSQL(_msg);
         return;
      }

		LogColumn logcol;

		rs.moveToInsertRow();

		for(int i=0; i<num; i++)
		{
        	logcol = (LogColumn)logcols.get(i);

			if(logcol.logtype == LogType.MSG)
			{
				rs.updateObject(logcol.name, _msg);
			}
			else if(logcol.logtype == LogType.ID)
			{
				rs.updateObject(logcol.name, logcol.idhandler.getID());
			}
			else if(logcol.logtype == LogType.STATIC)
			{
				rs.updateObject(logcol.name, logcol.value);
			}
			else if(logcol.logtype == LogType.TIMESTAMP)
			{
				rs.updateObject(logcol.name, new Timestamp((new java.util.Date()).getTime()));
			}
		}

		rs.insertRow();
	}

	/**
	Writes a message into the database using a given sql-statement.
	Throws an exception, if an database-error occurs !
	*/
	public void appendSQL(String _msg) throws Exception
	{
		if(!ready) if(!ready()) throw new Exception("JDBCLogger::appendSQL(), Not ready to append !");

      if(sql == null) throw new Exception("JDBCLogger::appendSQL(), No SQL-Statement configured !");

      if(msg_wildcard_pos > 0)
      {
			new_sql = new_sql_part1 + _msg + new_sql_part2;
      }
		else new_sql = sql;

      try
      {
			stmt.executeUpdate(new_sql);
      }
      catch(Exception e)
      {
      	errormsg = new_sql;
         throw e;
		}
	}


	/**
	Configures this class, by reading in the structure of the log-table
	Throws an exception, if an database-error occurs !
	*/
	public void configureTable(String _table) throws Exception
	{
   	if(isconfigured) return;

		//Fill logcols with META-informations of the table-columns
		stmt = con.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
		rs = stmt.executeQuery("SELECT * FROM " + _table + " WHERE 1 = 2");

		LogColumn logcol;

		ResultSetMetaData rsmd = rs.getMetaData();

		num = rsmd.getColumnCount();

		logcols = new ArrayList(num);

		for(int i=1; i<=num; i++)
		{
			logcol = new LogColumn();
			logcol.name = rsmd.getColumnName(i).toUpperCase();
			logcol.type = rsmd.getColumnTypeName(i);
			logcol.nullable = (rsmd.isNullable(i) == rsmd.columnNullable);
         logcol.isWritable = rsmd.isWritable(i);
         if(!logcol.isWritable) logcol.ignore = true;
         logcols.add(logcol);
		}

      table = _table;

		isconfigured = true;
	}

	/**
	Configures this class, by storing and parsing the given sql-statement.
	Throws an exception, if somethings wrong !
	*/
	public void configureSQL(String _sql) throws Exception
	{
   	if(isconfigured) return;

		if(!isConnected()) throw new Exception("JDBCLogger::configureSQL(), Not connected to database !");

		if(_sql == null || _sql.trim().equals("")) throw new Exception("JDBCLogger::configureSQL(), Invalid SQL-Statement !");

		sql = _sql.trim();

      stmt = con.createStatement();

		msg_wildcard_pos = sql.indexOf(msg_wildcard);

      if(msg_wildcard_pos > 0)
      {
			new_sql_part1 = sql.substring(0, msg_wildcard_pos-1) + "'";
         //between the message...
         new_sql_part2 = "'" + sql.substring(msg_wildcard_pos+msg_wildcard.length());
		}

		isconfigured = true;
	}

	/**
   Sets a connection. Throws an exception, if the connection is not open !
	*/
	public void setConnection(Connection _con) throws Exception
	{
		con = _con;

		if(!isConnected()) throw new Exception("JDBCLogger::setConnection(), Given connection isnt connected to database !");
	}


	/**
	Sets a columns logtype (LogTypes) and value, which depends on that logtype.
	Throws an exception, if the given arguments arent correct !
   */
	public void setLogType(String _name, int _logtype, Object _value) throws Exception
	{
		if(!isconfigured) throw new Exception("JDBCLogger::setLogType(), Not configured !");

		//setLogType() makes only sense for further configuration of configureTable()
      if(sql != null) return;

      _name = _name.toUpperCase();

		if(_name == null || !(_name.trim().length() > 0)) throw new Exception("JDBCLogger::setLogType(), Missing argument name !");
		if(!LogType.isLogType(_logtype)) throw new Exception("JDBCLogger::setLogType(), Invalid logtype '" + _logtype + "' !");
		if((_logtype != LogType.MSG && _logtype != LogType.EMPTY) && _value == null) throw new Exception("JDBCLogger::setLogType(), Missing argument value !");

  		LogColumn logcol;

		for(int i=0; i<num; i++)
		{
        	logcol = (LogColumn)logcols.get(i);

			if(logcol.name.equals(_name))
			{
         	if(!logcol.isWritable) throw new Exception("JDBCLogger::setLogType(), Column " + _name + " is not writeable !");

				//Column gets the message
				if(_logtype == LogType.MSG)
            {
            	logcol.logtype = _logtype;
               return;
				}
				//Column will be provided by JDBCIDHandler::getID()
				else if(_logtype == LogType.ID)
				{
					logcol.logtype = _logtype;

               try
               {
               	//Try to cast directly Object to JDBCIDHandler
						logcol.idhandler = (JDBCIDHandler)_value;
               }
               catch(Exception e)
               {
               	try
                  {
                  	//Assuming _value is of class string which contains the classname of a JDBCIDHandler
							logcol.idhandler = (JDBCIDHandler)(Class.forName((String)_value).newInstance());
                  }
                  catch(Exception e2)
                  {
							throw new Exception("JDBCLogger::setLogType(), Cannot cast value of class " + _value.getClass() + " to class JDBCIDHandler !");
                  }
               }

               return;
				}

				//Column will be statically defined with Object _value
				else if(_logtype == LogType.STATIC)
				{
					logcol.logtype = _logtype;
					logcol.value = _value;
               return;
				}

				//Column will be provided with a actually timestamp
				else if(_logtype == LogType.TIMESTAMP)
				{
					logcol.logtype = _logtype;
               return;
				}

            //Column will be fully ignored during process.
            //If this column is not nullable, the column has to be filled by a database trigger,
            //else a database error occurs !
            //Columns which are not nullable, but should be not filled, must be explicit assigned with LogType.EMPTY,
            //else a value is required !
				else if(_logtype == LogType.EMPTY)
				{
					logcol.logtype = _logtype;
					logcol.ignore = true;
               return;
				}
			}
		}
	}


	/**
	Return true, if this class is ready to append(), else false.
	When not ready, a reason-String is stored in the instance-variable msg.
	*/
	public boolean ready()
	{
   	if(ready) return true;

		if(!isconfigured){ errormsg = "Not ready to append ! Call configure() first !"; return false;}

      //No need to doing the whole rest...
      if(sql != null)
      {
      	ready = true;
         return true;
      }

		boolean msgcol_defined = false;

		LogColumn logcol;

		for(int i=0; i<num; i++)
		{
      	logcol = (LogColumn)logcols.get(i);

         if(logcol.ignore || !logcol.isWritable) continue;
			if(!logcol.nullable && logcol.logtype == LogType.EMPTY)
         {
         	errormsg = "Not ready to append ! Column " + logcol.name + " is not nullable, and must be specified by setLogType() !";
            return false;
         }
			if(logcol.logtype == LogType.ID && logcol.idhandler == null)
         {
         	errormsg = "Not ready to append ! Column " + logcol.name + " is specified as an ID-column, and a JDBCIDHandler has to be set !";
            return false;
         }
			else if(logcol.logtype == LogType.STATIC && logcol.value == null)
         {
         	errormsg = "Not ready to append ! Column " + logcol.name + " is specified as a static field, and a value has to be set !";
            return false;
         }
         else if(logcol.logtype == LogType.MSG) msgcol_defined = true;
		}

      if(!msgcol_defined) return false;

      //create the column_list
		for(int i=0; i<num; i++)
		{
      	logcol = (LogColumn)logcols.get(i);

			if(logcol.ignore || !logcol.isWritable) continue;

         if(logcol.logtype != LogType.EMPTY)
         {
				if(column_list == null)
            {
            	column_list = logcol.name;
            }
            else column_list += ", " + logcol.name;
         }
		}

      try
      {
			rs = stmt.executeQuery("SELECT " + column_list + " FROM " + table + " WHERE 1 = 2");
		}
      catch(Exception e)
      {
			errormsg = "Not ready to append ! Cannot select columns '" + column_list + "' of table " + table + " !";
      	return false;
      }

		ready = true;

		return true;
	}

	/**
	Return true, if this class is configured, else false.
	*/
	public boolean isConfigured(){ return isconfigured;}

	/**
	Return true, if this connection is open, else false.
	*/
	public boolean isConnected()
   {
   	try
      {
   		return (con != null && !con.isClosed());
      }
      catch(Exception e){return false;}
   }

	/**
	Return the internal error message stored in instance variable msg.
	*/
   public String getErrorMsg(){String r = new String(errormsg); errormsg = null; return r;}
}


/**
This class encapsulate all by class JDBCLogger needed data around a column
*/
class LogColumn
{
	//column name
	String name = null;
   //column type
	String type = null;
   //not nullability means that this column is mandatory
	boolean nullable = false;
   //isWritable means that the column can be updated, else column is only readable
   boolean isWritable = false;
   //if ignore is true, this column will be ignored by building sql-statements.
   boolean ignore = false;

	//Must be filled for not nullable columns ! In other case it is optional.
	int logtype = LogType.EMPTY;
	Object value = null;				//Generic storage for typewrapper-classes Long, String, etc...
	JDBCIDHandler idhandler = null;
}


/**
This class contains all constants which are necessary to define a columns log-type.
*/
class LogType
{
	//A column of this type will receive the message.
	public static final int MSG = 1;

	//A column of this type will be a unique identifier of the logged row.
	public static final int ID = 2;

	//A column of this type will contain a static, one-time-defined value.
	public static final int STATIC = 3;

  	//A column of this type will be filled with an actual timestamp depending by the time the logging will be done.
	public static final int TIMESTAMP = 4;

	//A column of this type will contain no value and will not be included in logging insert-statement.
   //This could be a column which will be filled not by creation but otherwhere...
	public static final int EMPTY = 5;


	public static boolean isLogType(int _lt)
	{
		if(_lt == MSG || _lt == STATIC || _lt == ID || _lt == TIMESTAMP || _lt == EMPTY) return true;

		return false;
	}

   public static int parseLogType(String _lt)
   {
		if(_lt.equals("MSG")) return MSG;
		if(_lt.equals("ID")) return ID;
		if(_lt.equals("STATIC")) return STATIC;
		if(_lt.equals("TIMESTAMP")) return TIMESTAMP;
		if(_lt.equals("EMPTY")) return EMPTY;

      return -1;
   }
}





