<?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
 */

/**
 * Converts ini configuration files to a PHP array.
 * 
 * These used to be called "properties" files (inherited from log4j), and that 
 * file extension is still supported. 
 *
 * @package log4php
 * @subpackage configurators
 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 * @version $Revision$
 * @since 2.2
 */
class LoggerConfigurationAdapterINI implements LoggerConfigurationAdapter {
	
	/** Name to assign to the root logger. */
	const ROOT_LOGGER_NAME = "root";

	/** Prefix used for defining logger additivity. */
	const ADDITIVITY_PREFIX = "log4php.additivity.";
	
	/** Prefix used for defining logger threshold. */
	const THRESHOLD_PREFIX = "log4php.threshold";
	
	/** Prefix used for defining the root logger. */
	const ROOT_LOGGER_PREFIX = "log4php.rootLogger";
	
	/** Prefix used for defining a logger. */
	const LOGGER_PREFIX = "log4php.logger.";
	
	/** Prefix used for defining an appender. */
	const APPENDER_PREFIX = "log4php.appender.";
	
	/** Prefix used for defining a renderer. */
	const RENDERER_PREFIX = "log4php.renderer.";
	
	/** Holds the configuration. */
	private $config = array();
	
	/**
	 * Loads and parses the INI configuration file.
	 * 
	 * INI_SCANNER_RAW is used here because otherwise parse_ini_file() will 
	 * try to parse all values, with some strange results. For example, "true"
	 * will become "1", while "false" and "null" will become "" (empty string). 
	 * 
	 * @see http://php.net/manual/en/function.parse-ini-file.php
	 * 
	 * @param string $url Path to the config file.
	 * @throws LoggerException
	 */
	private function load($url) {
		if (!file_exists($url)) {
			throw new LoggerException("File [$url] does not exist.");
		}
		
		$properties = @parse_ini_file($url, true);
		if ($properties === false) {
			$error = error_get_last();
			throw new LoggerException("Error parsing configuration file: {$error['message']}");
		}
		
		return $properties;
	}
	
	/**
	* Converts the provided INI configuration file to a PHP array config.
	*
	* @param string $path Path to the config file.
	* @throws LoggerException If the file cannot be loaded or parsed.
	*/
	public function convert($path) {
		// Load the configuration
		$properties = $this->load($path);
		
		// Parse threshold
		if (isset($properties[self::THRESHOLD_PREFIX])) {
			$this->config['threshold'] = $properties[self::THRESHOLD_PREFIX]; 
		}
		
		// Parse root logger
		if (isset($properties[self::ROOT_LOGGER_PREFIX])) {
			$this->parseLogger($properties[self::ROOT_LOGGER_PREFIX], self::ROOT_LOGGER_NAME);
		}
		
		$appenders = array();
		
		foreach($properties as $key => $value) {
			// Parse loggers
			if ($this->beginsWith($key, self::LOGGER_PREFIX)) {
				$name = substr($key, strlen(self::LOGGER_PREFIX));
				$this->parseLogger($value, $name);
			}
			
			// Parse additivity
			if ($this->beginsWith($key, self::ADDITIVITY_PREFIX)) {
				$name = substr($key, strlen(self::ADDITIVITY_PREFIX));
				$this->config['loggers'][$name]['additivity'] = $value;
			}
			
			// Parse appenders
			else if ($this->beginsWith($key, self::APPENDER_PREFIX)) {
				$this->parseAppender($key, $value);
			}
			
			// Parse renderers
			else if ($this->beginsWith($key, self::RENDERER_PREFIX)) {
				$this->parseRenderer($key, $value);
			}
		}
		
		return $this->config;
	}
	
	
	/**
	 * Parses a logger definition.
	 * 
	 * Loggers are defined in the following manner:
	 * <pre>
	 * log4php.logger.<name> = [<level>], [<appender-ref>, <appender-ref>, ...] 
	 * </pre>
	 * 
	 * @param string $value The configuration value (level and appender-refs).
	 * @param string $name Logger name. 
	 */
	private function parseLogger($value, $name) {
		// Value is divided by commas
		$parts = explode(',', $value);
		if (empty($value) || empty($parts)) {
			return;
		}

		// The first value is the logger level 
		$level = array_shift($parts);
		
		// The remaining values are appender references 
		$appenders = array();
		while($appender = array_shift($parts)) {
			$appender = trim($appender);
			if (!empty($appender)) {
				$appenders[] = trim($appender);
			}
		}

		// Find the target configuration 
		if ($name == self::ROOT_LOGGER_NAME) {
			$this->config['rootLogger']['level'] = trim($level);
			$this->config['rootLogger']['appenders'] = $appenders;
		} else {
			$this->config['loggers'][$name]['level'] = trim($level);
			$this->config['loggers'][$name]['appenders'] = $appenders;
		}
	}
	
	/**
	 * Parses an configuration line pertaining to an appender.
	 * 
	 * Parses the following patterns:
	 * 
	 * Appender class:
	 * <pre>
	 * log4php.appender.<name> = <class>
	 * </pre>
	 * 
	 * Appender parameter:
	 * <pre>
	 * log4php.appender.<name>.<param> = <value>
	 * </pre>
	 * 
 	 * Appender threshold:
	 * <pre>
	 * log4php.appender.<name>.threshold = <level>
	 * </pre>
	 * 
 	 * Appender layout:
	 * <pre>
	 * log4php.appender.<name>.layout = <layoutClass>
	 * </pre>
	 * 
	 * Layout parameter:
	 * <pre>
	 * log4php.appender.<name>.layout.<param> = <value>
	 * </pre> 
	 * 
	 * For example, a full appender config might look like:
	 * <pre>
	 * log4php.appender.myAppender = LoggerAppenderConsole
	 * log4php.appender.myAppender.threshold = info
	 * log4php.appender.myAppender.target = stdout
	 * log4php.appender.myAppender.layout = LoggerLayoutPattern
	 * log4php.appender.myAppender.layout.conversionPattern = "%d %c: %m%n"
	 * </pre>
	 * 
	 * After parsing all these options, the following configuration can be 
	 * found under $this->config['appenders']['myAppender']:
	 * <pre>
	 * array(
	 * 	'class' => LoggerAppenderConsole,
	 * 	'threshold' => info,
	 * 	'params' => array(
	 * 		'target' => 'stdout'
	 * 	),
	 * 	'layout' => array(
	 * 		'class' => 'LoggerAppenderConsole',
	 * 		'params' => array(
	 * 			'conversionPattern' => '%d %c: %m%n'
	 * 		)
	 * 	)
	 * )
	 * </pre>
	 * 
	 * @param string $key
	 * @param string $value
	 */
	private function parseAppender($key, $value) {

		// Remove the appender prefix from key
		$subKey = substr($key, strlen(self::APPENDER_PREFIX));
		
		// Divide the string by dots
		$parts = explode('.', $subKey);
		$count = count($parts);
		
		// The first part is always the appender name
		$name = trim($parts[0]);
		
		// Only one part - this line defines the appender class 
		if ($count == 1) {
			$this->config['appenders'][$name]['class'] = $value;
			return;
		}
		
		// Two parts - either a parameter, a threshold or layout class
		else if ($count == 2) {
			
			if ($parts[1] == 'layout') {
				$this->config['appenders'][$name]['layout']['class'] = $value;
				return;
			} else if ($parts[1] == 'threshold') {
				$this->config['appenders'][$name]['threshold'] = $value;
				return;
			} else {
				$this->config['appenders'][$name]['params'][$parts[1]] = $value;
				return;
			}
		}
		
		// Three parts - this can only be a layout parameter
		else if ($count == 3) {
			if ($parts[1] == 'layout') {
				$this->config['appenders'][$name]['layout']['params'][$parts[2]] = $value;
				return;
			}
		}
		
		trigger_error("log4php: Don't know how to parse the following line: \"$key = $value\". Skipping.");
	}

	/**
	 * Parses a renderer definition.
	 * 
	 * Renderers are defined as:
	 * <pre>
	 * log4php.renderer.<renderedClass> = <renderingClass> 
	 * </pre>
	 * 
	 * @param string $key log4php.renderer.<renderedClass>
	 * @param string $value <renderingClass>
	 */
	private function parseRenderer($key, $value) {
		// Remove the appender prefix from key
		$renderedClass = substr($key, strlen(self::APPENDER_PREFIX));
		$renderingClass = $value;
		
		$this->config['renderers'][] = compact('renderedClass', 'renderingClass');
	}
	
	/** Helper method. Returns true if $str begins with $sub. */
	private function beginsWith($str, $sub) {
		return (strncmp($str, $sub, strlen($sub)) == 0);
	}
	
	
}

?>