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

/**
 * Default implementation of the logger configurator.
 * 
 * Configures log4php based on a provided configuration file or array.
 * 
 * @package log4php
 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 * @version $Revision: 1212319 $
 * @since 2.2
 */
class LoggerConfiguratorDefault implements LoggerConfigurator
{
	/** XML configuration file format. */
	const FORMAT_XML = 'xml';
	
	/** PHP configuration file format. */
	const FORMAT_PHP = 'php';
	
	/** INI (properties) configuration file format. */
	const FORMAT_INI = 'ini';

	/** Defines which adapter should be used for parsing which format. */
	private $adapters = array(
		self::FORMAT_XML => 'LoggerConfigurationAdapterXML',
		self::FORMAT_INI => 'LoggerConfigurationAdapterINI',
		self::FORMAT_PHP => 'LoggerConfigurationAdapterPHP',
	);
	
	/** Default configuration; used if no configuration file is provided. */
	private static $defaultConfiguration = array(
        'threshold' => 'ALL',
        'rootLogger' => array(
            'level' => 'DEBUG',
            'appenders' => array('default'),
        ),
        'appenders' => array(
            'default' => array(
                'class' => 'LoggerAppenderEcho',
                'layout' => array(
                    'class' => 'LoggerLayoutTTCC',
                ),
            ),
        ),
	);
	
	/** Holds the appenders before they are linked to loggers. */
	private $appenders = array();
	
	/**
	 * Configures log4php based on the given configuration. The input can 
	 * either be a path to the config file, or a PHP array holding the 
	 * configuration. 
	 * 
	 * If no configuration is given, or if the given configuration cannot be
	 * parsed for whatever reason, a warning will be issued, and log4php
	 * will use the default configuration contained in 
	 * {@link $defaultConfiguration}.
	 * 
	 * @param LoggerHierarchy $hierarchy The hierarchy on which to perform 
	 * 		the configuration. 
	 * @param string|array $input Either path to the config file or the 
	 * 		configuration as an array. If not set, default configuration 
	 * 		will be used.
	 */
	public function configure(LoggerHierarchy $hierarchy, $input = null) {
		$config = $this->parse($input);
		$this->doConfigure($hierarchy, $config);
	}
	
	/**
	 * Parses the given configuration and returns the parsed configuration
	 * as a PHP array. Does not perform any configuration. 
	 * 
	 * If no configuration is given, or if the given configuration cannot be
	 * parsed for whatever reason, a warning will be issued, and the default 
	 * configuration will be returned ({@link $defaultConfiguration}).
	 * 
	 * @param string|array $input Either path to the config file or the 
	 * 		configuration as an array. If not set, default configuration 
	 * 		will be used.
	 * @return array The parsed configuration.
	 */
	public function parse($input)
	{
		// No input - use default configuration
		if (!isset($input)) {
			$config = self::$defaultConfiguration;
		}
		
		// Array input - contains configuration within the array
		else if (is_array($input)) {
			$config = $input;
		}
		
		// String input - contains path to configuration file
		else if (is_string($input)) {
			try {
				$config = $this->parseFile($input);
			} catch (LoggerException $e) {
				$this->warn("Configuration failed. " . $e->getMessage() . " Using default configuration.");
				$config = self::$defaultConfiguration;
			}
		}
		
		// Anything else is an error
		else {
			$this->warn("Invalid configuration param given. Reverting to default configuration.");
			$config = self::$defaultConfiguration;
		}
		
		return $config;
	}

	/** 
	 * Returns the default log4php configuration.
	 * @return array
	 */
	public static function getDefaultConfiguration() {
		return self::$defaultConfiguration;
	} 
	
	/**
	 * Loads the configuration file from the given URL, determines which
	 * adapter to use, converts the configuration to a PHP array and
	 * returns it.
	 *
	 * @param string $url Path to the config file.
	 * @return The configuration from the config file, as a PHP array.
	 * @throws LoggerException If the configuration file cannot be loaded, or
	 * 		if the parsing fails.
	 */
	private function parseFile($url) {
		
		if (!file_exists($url)) {
			throw new LoggerException("File not found at [$url].");
		}
		
		$type = $this->getConfigType($url);
		$adapterClass = $this->adapters[$type];

		$adapter = new $adapterClass();
		return $adapter->convert($url);
	}
	
	/** Determines configuration file type based on the file extension. */
	private function getConfigType($url) {
		$info = pathinfo($url);
		$ext = strtolower($info['extension']);
		
		switch($ext) {
			case 'xml':
				return self::FORMAT_XML;
			
			case 'ini':
			case 'properties':
				return self::FORMAT_INI;
			
			case 'php':
				return self::FORMAT_PHP;
				
			default:
				throw new LoggerException("Unsupported configuration file extension: $ext");
		}
	}
	
	/**
	 * Constructs the logger hierarchy based on configuration.
	 * 
	 * @param LoggerHierarchy $hierarchy
	 * @param array $config
	 */
	private function doConfigure(LoggerHierarchy $hierarchy, $config) {
		if (isset($config['threshold'])) {
			$threshold = LoggerLevel::toLevel($config['threshold']);
			if (isset($threshold)) {
				$hierarchy->setThreshold($threshold);
			} else {
				$this->warn("Invalid threshold value [{$config['threshold']}] specified. Ignoring threshold definition.");
			}
		}
		
		// Configure appenders and add them to the appender pool
		if (isset($config['appenders']) && is_array($config['appenders'])) {
			foreach($config['appenders'] as $name => $appenderConfig) {
				$this->configureAppender($name, $appenderConfig);
			}
		}
		
		// Configure root logger 
		if (isset($config['rootLogger'])) {
			$this->configureRootLogger($hierarchy, $config['rootLogger']);
		}
		
		// Configure loggers
		if (isset($config['loggers']) && is_array($config['loggers'])) {
			foreach($config['loggers'] as $loggerName => $loggerConfig) {
				$this->configureOtherLogger($hierarchy, $loggerName, $loggerConfig);
			}
		}

		// Configure renderers
		if (isset($config['renderers']) && is_array($config['renderers'])) {
			foreach($config['renderers'] as $rendererConfig) {
				$this->configureRenderer($hierarchy, $rendererConfig);
			}
		}
	}
	
	private function configureRenderer(LoggerHierarchy $hierarchy, $config) {
		if (!isset($config['renderingClass'])) {
			$this->warn("Rendering class not specified. Skipping renderers definition.");
			return;			
		}
		
		$renderingClass = $config['renderingClass'];
		if (!class_exists($renderingClass)) {
			$this->warn("Nonexistant rendering class [$renderingClass] specified. Skipping renderers definition.");
			return;
		}
		
		$renderingClassInstance = new $renderingClass();
		if (!$renderingClassInstance instanceof LoggerRendererObject) {
			$this->warn("Invalid class [$renderingClass] given. Not a valid LoggerRenderer class. Skipping renderers definition.");
			return;			
		}
	
		if (!isset($config['renderedClass'])) {
			$this->warn("Rendered class not specified for rendering Class [$renderingClass]. Skipping renderers definition.");
			return;			
		}
		
		$renderedClass = $config['renderedClass'];
		if (!class_exists($renderedClass)) {
			$this->warn("Nonexistant rendered class [$renderedClass] specified for renderer [$renderingClass]. Skipping renderers definition.");
			return;
		}		

		$hierarchy->getRendererMap()->addRenderer($renderedClass, $renderingClassInstance);
	}
	
	/** 
	 * Configures an appender based on given config and saves it to 
	 * {@link $appenders} array so it can be later linked to loggers. 
	 * @param string $name Appender name. 
	 * @param array $config Appender configuration options.
	 */
	private function configureAppender($name, $config) {

		// TODO: add this check to other places where it might be useful
		if (!is_array($config)) {
			$type = gettype($config);
			$this->warn("Invalid configuration provided for appender [$name]. Expected an array, found <$type>. Skipping appender definition.");
			return;
		}
		
		// Parse appender class
		$class = $config['class'];
		if (empty($class)) {
			$this->warn("No class given for appender [$name]. Skipping appender definition.");
			return;
		}
		if (!class_exists($class)) {
			$this->warn("Invalid class [$class] given for appender [$name]. Class does not exist. Skipping appender definition.");
			return;
		}
		
		// Instantiate the appender
		$appender = new $class($name);
		if (!($appender instanceof LoggerAppender)) {
			$this->warn("Invalid class [$class] given for appender [$name]. Not a valid LoggerAppender class. Skipping appender definition.");
			return;
		}
		
		// Parse the appender threshold
		if (isset($config['threshold'])) {
			$threshold = LoggerLevel::toLevel($config['threshold']);
			if ($threshold instanceof LoggerLevel) {
				$appender->setThreshold($threshold);
			} else {
				$this->warn("Invalid threshold value [{$config['threshold']}] specified for appender [$name]. Ignoring threshold definition.");
			}
		}
		
		// Parse the appender layout
		if ($appender->requiresLayout() && isset($config['layout'])) {
			$this->createAppenderLayout($appender, $config['layout']);
		}
		
		// Parse filters
		if (isset($config['filters']) && is_array($config['filters'])) {
			foreach($config['filters'] as $filterConfig) {
				$this->createAppenderFilter($appender, $filterConfig);
			}
		}
		
		// Set options if any
		if (isset($config['params'])) {
			$this->setOptions($appender, $config['params']);
		}

		// Activate and save for later linking to loggers
		$appender->activateOptions();
		$this->appenders[$name] = $appender;
	}
	
	/**
	 * Parses layout config, creates the layout and links it to the appender.
	 * @param LoggerAppender $appender
	 * @param array $config Layout configuration.
	 */
	private function createAppenderLayout(LoggerAppender $appender, $config) {
		$name = $appender->getName();
		$class = $config['class'];
		if (empty($class)) {
			$this->warn("Layout class not specified for appender [$name]. Reverting to default layout.");
			return;
		}
		if (!class_exists($class)) {
			$this->warn("Nonexistant layout class [$class] specified for appender [$name]. Reverting to default layout.");
			return;
		}
		
		$layout = new $class();
		if (!($layout instanceof LoggerLayout)) {
			$this->warn("Invalid layout class [$class] sepcified for appender [$name]. Reverting to default layout.");
			return;
		}
		
		if (isset($config['params'])) {
			$this->setOptions($layout, $config['params']);
		}
		
		$layout->activateOptions();
		$appender->setLayout($layout);
	}
	
	/**
	 * Parses filter config, creates the filter and adds it to the appender's 
	 * filter chain.
	 * @param LoggerAppender $appender
	 * @param array $config Filter configuration.
	 */
	private function createAppenderFilter(LoggerAppender $appender, $config) {
		$name = $appender->getName();
		$class = $config['class'];
		if (!class_exists($class)) {
			$this->warn("Nonexistant filter class [$class] specified on appender [$name]. Skipping filter definition.");
			return;
		}
	
		$filter = new $class();
		if (!($filter instanceof LoggerFilter)) {
			$this->warn("Invalid filter class [$class] sepcified on appender [$name]. Skipping filter definition.");
			return;
		}
	
		if (isset($config['params'])) {
			$this->setOptions($filter, $config['params']);
		}
	
		$filter->activateOptions();
		$appender->addFilter($filter);
	}
	
	/** 
	 * Configures the root logger
	 * @see configureLogger() 
	 */
	private function configureRootLogger(LoggerHierarchy $hierarchy, $config) {
		$logger = $hierarchy->getRootLogger();
		$this->configureLogger($logger, $config);
	}

	/**
	 * Configures a logger which is not root.
	 * @see configureLogger()
	 */
	private function configureOtherLogger(LoggerHierarchy $hierarchy, $name, $config) {
		// Get logger from hierarchy (this creates it if it doesn't already exist)
		$logger = $hierarchy->getLogger($name);
		$this->configureLogger($logger, $config);
	}
	
	/**
	 * Configures a logger. 
	 * 
	 * @param Logger $logger The logger to configure
	 * @param array $config Logger configuration options.
	 */
	private function configureLogger(Logger $logger, $config) {
		$loggerName = $logger->getName();
		
		// Set logger level
		if (isset($config['level'])) {
			$level = LoggerLevel::toLevel($config['level']);
			if (isset($level)) {
				$logger->setLevel($level);
			} else {
				$default = $logger->getLevel();
				$this->warn("Invalid level value [{$config['level']}] specified for logger [$loggerName]. Ignoring level definition.");
			}
		}
		
		// Link appenders to logger
		if (isset($config['appenders'])) {
			foreach($config['appenders'] as $appenderName) {
				if (isset($this->appenders[$appenderName])) {
					$logger->addAppender($this->appenders[$appenderName]);
				} else {
					$this->warn("Nonexistnant appender [$appenderName] linked to logger [$loggerName].");
				}
			}
		}
		
		// Set logger additivity
		if (isset($config['additivity'])) {
			$additivity = LoggerOptionConverter::toBoolean($config['additivity'], null);
			if (is_bool($additivity)) {
				$logger->setAdditivity($additivity);
			} else {
				$this->warn("Invalid additivity value [{$config['additivity']}] specified for logger [$loggerName]. Ignoring additivity setting.");
			}
		}
	}

	/**
	 * Helper method which applies given options to an object which has setters
	 * for these options (such as appenders, layouts, etc.).
	 * 
	 * For example, if options are:
	 * <code>
	 * array(
	 * 	'file' => '/tmp/myfile.log',
	 * 	'append' => true
	 * )
	 * </code>
	 * 
	 * This method will call:
	 * <code>
	 * $object->setFile('/tmp/myfile.log')
	 * $object->setAppend(true)
	 * </code>
	 * 
	 * If required setters do not exist, it will produce a warning. 
	 * 
	 * @param mixed $object The object to configure.
	 * @param unknown_type $options
	 */
	private function setOptions($object, $options) {
		foreach($options as $name => $value) {
			$setter = "set$name";
			if (method_exists($object, $setter)) {
				$object->$setter($value);
			} else {
				$class = get_class($object);
				$this->warn("Nonexistant option [$name] specified on [$class]. Skipping.");
			}
		}
	}
	
	/** Helper method to simplify error reporting. */
	private function warn($message) {
		trigger_error("log4php: $message", E_USER_WARNING);
	}
}