<?php
/**
 * 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.
 *
 * @package log4php
 */

/**
 * Appends log events to a db table using PDO.
 *
 * Configurable parameters of this appender are:
 *
 * - user            - Sets the user of this database connection
 * - password        - Sets the password of this database connection
 * - createTable     - true, if the table should be created if necessary. false otherwise
 * - table           - Sets the table name (default: log4php_log)
 * - sql             - Sets the insert statement for a logging event. Defaults
 *                     to the correct one - change only if you are sure what you are doing.
 * - dsn             - Sets the DSN string for this connection
 *
 * If $sql is set then $table and $sql are used, else $table, $insertSql and $insertPattern.
 *
 * An example:
 *
 * {@example ../../examples/php/appender_pdo.php 19}
 * 
 * {@example ../../examples/resources/appender_pdo.properties 18}
 * 
 * @version $Revision: 806678 $
 * @package log4php
 * @subpackage appenders
 * @since 2.0
 */
class LoggerAppenderPDO extends LoggerAppender {

	/** 
	 * Create the log table if it does not exists (optional).
	 * @var string 
	 */
	protected $createTable = true;
	
	/** 
	 * Database user name.
	 * @var string 
	 */
	protected $user;
	
	/** 
	 * Database password
	 * @var string 
	 */
	protected $password;
	
	/** 
	 * DSN string for enabling a connection.
	 * @var string 
	 */
	protected $dsn;
	
	/** 
	 * A {@link LoggerPatternLayout} string used to format a valid insert query.
	 * @deprecated Use {@link $insertSql} and {@link $insertPattern} which properly handle quotes in the messages!
	 * @var string 
	 */
	protected $sql;
	
	/** 
	 * Can be set to a complete insert statement with ? that are replaced using {@link insertPattern}.
	 * @var string 
	 */
	protected $insertSql = "INSERT INTO __TABLE__ (timestamp, logger, level, message, thread, file, line) VALUES (?,?,?,?,?,?,?)";

	/** 
	 * A comma separated list of {@link LoggerPatternLayout} format strings that replace the "?" in {@link $sql}.
	 * @var string 
	 */
	protected $insertPattern = "%d,%c,%p,%m,%t,%F,%L";

	/** 
	 * Table name to write events. Used only for CREATE TABLE if {@link $createTable} is true.
	 * @var string 
	 */
	protected $table = 'log4php_log';
	
	/** 
	 * The PDO instance.
	 * @var PDO 
	 */
	protected $db = null;
	
	/** 
	 * Prepared statement for the INSERT INTO query.
	 * @var PDOStatement 
	 */
	protected $preparedInsert;

	/** 
	 * Set in activateOptions() and later used in append() to check if all conditions to append are true.
	 * @var boolean 
	 */
	protected $canAppend = true;
	
	/**
	 * This appender does not require a layout.
	 */
	protected $requiresLayout = false;
	
	/**
	 * Setup db connection.
	 * Based on defined options, this method connects to db defined in {@link $dsn}
	 * and creates a {@link $table} table if {@link $createTable} is true.
	 * @return boolean true if all ok.
	 * @throws a PDOException if the attempt to connect to the requested database fails.
	 */
	public function activateOptions() {
		try {
			if($this->user === null) {
				$this->db = new PDO($this->dsn);
			} else if($this->password === null) {
				$this->db = new PDO($this->dsn, $this->user);
			} else {
				$this->db = new PDO($this->dsn,$this->user,$this->password);
			}
			$this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
			
			// test if log table exists
			try {
				$result = $this->db->query('SELECT * FROM ' . $this->table . ' WHERE 1 = 0');
				$result->closeCursor(); 
			} catch (PDOException $e) {
				// It could be something else but a "no such table" is the most likely
				$result = false;
			}
			
			// create table if necessary
			if ($result == false and $this->createTable) {
				// The syntax should at least be compatible with MySQL, PostgreSQL, SQLite and Oracle.
				$query = "CREATE TABLE {$this->table} (".
							"timestamp varchar(32)," .
							"logger varchar(64)," .
							"level varchar(32)," .
							"message varchar(9999)," .
							"thread varchar(32)," .
							"file varchar(255)," .
							"line varchar(6))";
				$result = $this->db->query($query);
			}
		} catch (PDOException $e) {
			$this->canAppend = false;
			throw new LoggerException($e);
		}
		
		$this->layout = new LoggerLayoutPattern();
		
		//
		// Keep compatibility to legacy option $sql which already included the format patterns!
		//
		if (empty($this->sql)) {
			// new style with prepared Statment and $insertSql and $insertPattern
			// Maybe the tablename has to be substituted.
			$this->insertSql = preg_replace('/__TABLE__/', $this->table, $this->insertSql);
			$this->preparedInsert = $this->db->prepare($this->insertSql);
			$this->layout->setConversionPattern($this->insertPattern);
		} else {
			// Old style with format strings in the $sql query should be used.
		$this->layout->setConversionPattern($this->sql);
		}

		$this->canAppend = true;
		return true;
	}
	
	/**
	 * Appends a new event to the database.
	 * 
	 * @throws LoggerException If the pattern conversion or the INSERT statement fails.
	 */
	public function append(LoggerLoggingEvent $event) {
		// TODO: Can't activateOptions() simply throw an Exception if it encounters problems?
		if ( ! $this->canAppend) return;

			try {
			if (empty($this->sql)) {
				// new style with prepared statement
				$params = $this->layout->formatToArray($event);
				$this->preparedInsert->execute($params);
			} else {
				// old style
				$query = $this->layout->format($event);
				$this->db->exec($query);
			}
			} catch (Exception $e) {
				throw new LoggerException($e);
			}
		}
	
	/**
	 * Closes the connection to the logging database
	 */
	public function close() {
		if($this->closed != true) {
			if ($this->db !== null) {
				$this->db = null;
			}
			$this->closed = true;
		}
	}
	
	/**
	 * Sets the username for this connection. 
	 * Defaults to ''
	 */
	public function setUser($user) {
		$this->setString('user', $user);
	}
	
	/**
	 * Sets the password for this connection. 
	 * Defaults to ''
	 */
	public function setPassword($password) {
		$this->setString('password', $password);
	}
	
	/**
	 * Indicator if the logging table should be created on startup,
	 * if its not existing.
	 */
	public function setCreateTable($flag) {
		$this->setBoolean('createTable', $flag);
	}
   
   	/**
	 * Sets the SQL string into which the event should be transformed.
	 * Defaults to:
	 * 
	 * INSERT INTO $this->table 
	 * ( timestamp, logger, level, message, thread, file, line) 
	 * VALUES 
	 * ('%d','%c','%p','%m','%t','%F','%L')
	 * 
	 * It's not necessary to change this except you have customized logging'
	 *
	 * @deprecated See {@link setInsertSql} and {@link setInsertPattern}.
	 */
	public function setSql($sql) {
		$this->setString('sql', $sql);
	}
	
	/**
	 * Sets the SQL INSERT string to use with {@link $insertPattern}.
	 *
	 * @param $sql		  A complete INSERT INTO query with "?" that gets replaced.
	 */
	public function setInsertSql($sql) {
		$this->setString('insertSql', $sql);
	}

	/**
	 * Sets the {@link LoggerLayoutPattern} format strings for {@link $insertSql}.
	 *
	 * It's not necessary to change this except you have customized logging.
	 *
	 * @param $pattern		  Comma separated format strings like "%p,%m,%C"
	 */
	public function setInsertPattern($pattern) {
		$this->setString('insertPattern', $pattern);
	}

	/**
	 * Sets the tablename to which this appender should log.
	 * Defaults to log4php_log
	 */
	public function setTable($table) {
		$this->setString('table', $table);
	}
	
	/**
	 * Sets the DSN string for this connection. In case of
	 * SQLite it could look like this: 'sqlite:appenders/pdotest.sqlite'
	 */
	public function setDSN($dsn) {
		$this->setString('dsn', $dsn);
	}
	
	/**
	 * Sometimes databases allow only one connection to themselves in one thread.
	 * SQLite has this behaviour. In that case this handle is needed if the database
	 * must be checked for events.
	 *
	 * @return PDO
	 */
	public function getDatabaseHandle() {
		return $this->db;
	}
}

