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

/**
 * Appender for writing to MongoDB.
 *
 * This class was originally contributed by Vladimir Gorej.
 *
 * ## Configurable parameters: ##
 *
 * - **host** - Server on which mongodb instance is located.
 * - **port** - Port on which the instance is bound.
 * - **databaseName** - Name of the database to which to log.
 * - **collectionName** - Name of the target collection within the given database.
 * - **username** - Username used to connect to the database.
 * - **password** - Password used to connect to the database.
 * - **timeout** - For how long the driver should try to connect to the database (in milliseconds).
 *
 * @version $Revision: 1346363 $
 * @package log4php
 * @subpackage appenders
 * @since 2.1
 * @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
 * @link http://logging.apache.org/log4php/docs/appenders/mongodb.html Appender documentation
 * @link http://github.com/log4mongo/log4mongo-php Vladimir Gorej's original submission.
 * @link http://www.mongodb.org/ MongoDB website.
 */
class LoggerAppenderMongoDB extends LoggerAppender {

	// ******************************************
	// ** Constants                            **
	// ******************************************

	/** Default prefix for the {@link $host}. */
	const DEFAULT_MONGO_URL_PREFIX = 'mongodb://';

	/** Default value for {@link $host}, without a prefix. */
	const DEFAULT_MONGO_HOST = 'localhost';

	/** Default value for {@link $port} */
	const DEFAULT_MONGO_PORT = 27017;

	/** Default value for {@link $databaseName} */
	const DEFAULT_DB_NAME = 'log4php_mongodb';

	/** Default value for {@link $collectionName} */
	const DEFAULT_COLLECTION_NAME = 'logs';

	/** Default value for {@link $timeout} */
	const DEFAULT_TIMEOUT_VALUE = 3000;

	// ******************************************
	// ** Configurable parameters              **
	// ******************************************

	/** Server on which mongodb instance is located. */
	protected $host;

	/** Port on which the instance is bound. */
	protected $port;

	/** Name of the database to which to log. */
	protected $databaseName;

	/** Name of the collection within the given database. */
	protected $collectionName;

	/** Username used to connect to the database. */
	protected $userName;

	/** Password used to connect to the database. */
	protected $password;

	/** Timeout value used when connecting to the database (in milliseconds). */
	protected $timeout;

	// ******************************************
	// ** Member variables                     **
	// ******************************************

	/**
	 * Connection to the MongoDB instance.
	 * @var Mongo
	 */
	protected $connection;

	/**
	 * The collection to which log is written.
	 * @var MongoCollection
	 */
	protected $collection;

	public function __construct($name = '') {
		parent::__construct($name);
		$this->host = self::DEFAULT_MONGO_URL_PREFIX . self::DEFAULT_MONGO_HOST;
		$this->port = self::DEFAULT_MONGO_PORT;
		$this->databaseName = self::DEFAULT_DB_NAME;
		$this->collectionName = self::DEFAULT_COLLECTION_NAME;
		$this->timeout = self::DEFAULT_TIMEOUT_VALUE;
		$this->requiresLayout = false;
	}

	/**
	 * Setup db connection.
	 * Based on defined options, this method connects to the database and
	 * creates a {@link $collection}.
	 */
	public function activateOptions() {
		try {
			$this->connection = new Mongo(sprintf('%s:%d', $this->host, $this->port), array('timeout' => $this->timeout));
			$db	= $this->connection->selectDB($this->databaseName);
			if ($this->userName !== null && $this->password !== null) {
				$authResult = $db->authenticate($this->userName, $this->password);
				if ($authResult['ok'] == floatval(0)) {
					throw new Exception($authResult['errmsg'], $authResult['ok']);
				}
			}
			$this->collection = $db->selectCollection($this->collectionName);
		} catch (MongoConnectionException $ex) {
			$this->closed = true;
			$this->warn(sprintf('Failed to connect to mongo deamon: %s', $ex->getMessage()));
		} catch (InvalidArgumentException $ex) {
			$this->closed = true;
			$this->warn(sprintf('Error while selecting mongo database: %s', $ex->getMessage()));
		} catch (Exception $ex) {
			$this->closed = true;
			$this->warn('Invalid credentials for mongo database authentication');
		}
	}

	/**
	 * Appends a new event to the mongo database.
	 *
	 * @param LoggerLoggingEvent $event
	 */
	public function append(LoggerLoggingEvent $event) {
		try {
			if ($this->collection != null) {
				$this->collection->insert($this->format($event));
			}
		} catch (MongoCursorException $ex) {
			$this->warn(sprintf('Error while writing to mongo collection: %s', $ex->getMessage()));
		}
	}

	/**
	 * Converts the logging event into an array which can be logged to mongodb.
	 *
	 * @param LoggerLoggingEvent $event
	 * @return array The array representation of the logging event.
	 */
	protected function format(LoggerLoggingEvent $event) {
		$timestampSec = (int) $event->getTimestamp();
		$timestampUsec = (int) (($event->getTimestamp() - $timestampSec) * 1000000);

		$document = array(
			'timestamp' => new MongoDate($timestampSec, $timestampUsec),
			'level' => $event->getLevel()->toString(),
			'thread' => (int) $event->getThreadName(),
			'message' => $event->getMessage(),
			'loggerName' => $event->getLoggerName()
		);

		$locationInfo = $event->getLocationInformation();
		if ($locationInfo != null) {
			$document['fileName'] = $locationInfo->getFileName();
			$document['method'] = $locationInfo->getMethodName();
			$document['lineNumber'] = ($locationInfo->getLineNumber() == 'NA') ? 'NA' : (int) $locationInfo->getLineNumber();
			$document['className'] = $locationInfo->getClassName();
		}

		$throwableInfo = $event->getThrowableInformation();
		if ($throwableInfo != null) {
			$document['exception'] = $this->formatThrowable($throwableInfo->getThrowable());
		}

		return $document;
	}

	/**
	 * Converts an Exception into an array which can be logged to mongodb.
	 *
	 * Supports innner exceptions (PHP >= 5.3)
	 *
	 * @param Exception $ex
	 * @return array
	 */
	protected function formatThrowable(Exception $ex) {
		$array = array(
			'message' => $ex->getMessage(),
			'code' => $ex->getCode(),
			'stackTrace' => $ex->getTraceAsString(),
		);

		if (method_exists($ex, 'getPrevious') && $ex->getPrevious() !== null) {
			$array['innerException'] = $this->formatThrowable($ex->getPrevious());
		}

		return $array;
	}

	/**
	 * Closes the connection to the logging database
	 */
	public function close() {
		if($this->closed != true) {
			$this->collection = null;
			if ($this->connection !== null) {
				$this->connection->close();
				$this->connection = null;
			}
			$this->closed = true;
		}
	}

	/**
	 * Sets the value of {@link $host} parameter.
	 * @param string $host
	 */
	public function setHost($host) {
		if (!preg_match('/^mongodb\:\/\//', $host)) {
			$host = self::DEFAULT_MONGO_URL_PREFIX . $host;
		}
		$this->host = $host;
	}

	/**
	 * Returns the value of {@link $host} parameter.
	 * @return string
	 */
	public function getHost() {
		return $this->host;
	}

	/**
	 * Sets the value of {@link $port} parameter.
	 * @param int $port
	 */
	public function setPort($port) {
		$this->setPositiveInteger('port', $port);
	}

	/**
	 * Returns the value of {@link $port} parameter.
	 * @return int
	 */
	public function getPort() {
		return $this->port;
	}

	/**
	 * Sets the value of {@link $databaseName} parameter.
	 * @param string $databaseName
	 */
	public function setDatabaseName($databaseName) {
		$this->setString('databaseName', $databaseName);
	}

	/**
	 * Returns the value of {@link $databaseName} parameter.
	 * @return string
	 */
	public function getDatabaseName() {
		return $this->databaseName;
	}

	/**
	 * Sets the value of {@link $collectionName} parameter.
	 * @param string $collectionName
	 */
	public function setCollectionName($collectionName) {
		$this->setString('collectionName', $collectionName);
	}

	/**
	 * Returns the value of {@link $collectionName} parameter.
	 * @return string
	 */
	public function getCollectionName() {
		return $this->collectionName;
	}

	/**
	 * Sets the value of {@link $userName} parameter.
	 * @param string $userName
	 */
	public function setUserName($userName) {
		$this->setString('userName', $userName, true);
	}

	/**
	 * Returns the value of {@link $userName} parameter.
	 * @return string
	 */
	public function getUserName() {
		return $this->userName;
	}

	/**
	 * Sets the value of {@link $password} parameter.
	 * @param string $password
	 */
	public function setPassword($password) {
		$this->setString('password', $password, true);
	}

	/**
	 * Returns the value of {@link $password} parameter.
	 * @return string
	 */
	public function getPassword() {
		return $this->password;
	}

	/**
	 * Sets the value of {@link $timeout} parameter.
	 * @param int $timeout
	 */
	public function setTimeout($timeout) {
		$this->setPositiveInteger('timeout', $timeout);
	}

	/**
	 * Returns the value of {@link $timeout} parameter.
	 * @return int
	 */
	public function getTimeout() {
		return $this->timeout;
	}
	/**
	 * Returns the mongodb connection.
	 * @return Mongo
	 */
	public function getConnection() {
		return $this->connection;
	}

	/**
	 * Returns the active mongodb collection.
	 * @return MongoCollection
	 */
	public function getCollection() {
		return $this->collection;
	}
}


<?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: 1394956 $
 * @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'
            ),
        ),
	);

	/** 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);
			}
		}

		if (isset($config['defaultRenderer'])) {
			$this->configureDefaultRenderer($hierarchy, $config['defaultRenderer']);
		}
	}

	private function configureRenderer(LoggerHierarchy $hierarchy, $config) {
		if (empty($config['renderingClass'])) {
			$this->warn("Rendering class not specified. Skipping renderer definition.");
			return;
		}

		if (empty($config['renderedClass'])) {
			$this->warn("Rendered class not specified. Skipping renderer definition.");
			return;
		}

		// Error handling performed by RendererMap
		$hierarchy->getRendererMap()->addRenderer($config['renderedClass'], $config['renderingClass']);
	}

	private function configureDefaultRenderer(LoggerHierarchy $hierarchy, $class) {
		if (empty($class)) {
			$this->warn("Rendering class not specified. Skipping default renderer definition.");
			return;
		}

		// Error handling performed by RendererMap
		$hierarchy->getRendererMap()->setDefaultRenderer($class);
	}

	/**
	 * 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 {
				$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'])) {
			try {
				$additivity = LoggerOptionConverter::toBooleanEx($config['additivity'], null);
				$logger->setAdditivity($additivity);
			} catch (Exception $ex) {
				$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:
	 *
	 * array(
	 * 	'file' => '/tmp/myfile.log',
	 * 	'append' => true
	 * )
	 *
	 *
	 * This method will call:
	 *
	 * $object->setFile('/tmp/myfile.log')
	 * $object->setAppend(true)
	 *
	 *
	 * 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);
	}
}


<?php
namespace FACTFinder\Adapter;

use FACTFinder\Loader as FF;

/**
 * Base class for all adapters which support the response of records with ids only.
 */
abstract class ConfigurableResponse extends AbstractAdapter
{
    /**
     * @var bool
     */
    protected $idsOnly = false;

    /**
     * @param string $loggerClass Class name of logger to use. The class should
     *        implement FACTFinder\Util\LoggerInterface.
     * @param \FACTFinder\Core\ConfigurationInterface $configuration
     *        Configuration object to use.
     * @param \FACTFinder\Core\Server\Request $request The request object from
     *        which to obtain the server data.
     * @param \FACTFinder\Core\Client\UrlBuilder $urlBuilder
     *        Client URL builder object to use.
     * @param \FACTFinder\Core\encodingConverter $encodingConverter
     *        Encoding converter object to use
     */
    public function __construct(
        $loggerClass,
        \FACTFinder\Core\ConfigurationInterface $configuration,
        \FACTFinder\Core\Server\Request $request,
        \FACTFinder\Core\Client\UrlBuilder $urlBuilder,
        \FACTFinder\Core\AbstractEncodingConverter $encodingConverter = null
    ) {
        parent::__construct($loggerClass, $configuration, $request,
                            $urlBuilder, $encodingConverter);
    }

    /**
     * Set this to true to only retrieve the IDs of products instead
     * of full Record objects.
     * @param $idsOnly bool
     */
    public function setIdsOnly($idsOnly)
    {
        if($this->idsOnly && !$idsOnly)
            $this->upToDate = false;

        $this->idsOnly = $idsOnly;
        $parameters = $this->request->getParameters();
        $parameters['idsOnly'] = $idsOnly ? 'true' : 'false';
    }
 }


<?php
namespace FACTFinder\Adapter;

use FACTFinder\Loader as FF;

class Search extends PersonalisedResponse
{

    /**
     * @var FACTFinder\Util\LoggerInterface
     */
    private $log;

    /**
     * @var FACTFinder\Data\Result
     */
    private $result;

    /**
     * @var FACTFinder\Data\SingleWordSearchItem[]
     */
    private $singleWordSearch;

    /**
     * @var FACTFinder\Data\AfterSearchNavigation
     */
    private $afterSearchNavigation;

    /**
     * @var FACTFinder\Data\ResultsPerPageOptions
     */
    private $resultsPerPageOptions;

    /**
     * @var FACTFinder\Data\Paging
     */
    private $paging;

    /**
     * @var FACTFinder\Data\Sorting
     */
    private $sorting;

    /**
     * @var FACTFinder\Data\SortingItems
     */
    private $sortingItems;

    /**
     * @var FACTFinder\Data\BreadCrumbTrail
     */
    private $breadCrumbTrail;

    /**
     * @var FACTFinder\Data\CampaignIterator
     */
    private $campaigns;

    public function __construct(
        $loggerClass,
        \FACTFinder\Core\ConfigurationInterface $configuration,
        \FACTFinder\Core\Server\Request $request,
        \FACTFinder\Core\Client\UrlBuilder $urlBuilder,
        \FACTFinder\Core\AbstractEncodingConverter $encodingConverter = null
    ) {
        parent::__construct($loggerClass, $configuration, $request,
                            $urlBuilder, $encodingConverter);

        $this->log = $loggerClass::getLogger(__CLASS__);

        $this->request->setAction('Search.ff');
        $this->parameters['format'] = 'json';

        $this->useJsonResponseContentProcessor();
    }

    /**
     * Overwrite the query on the request.
     * @param string $query
     */
    public function setQuery($query)
    {
        $this->parameters['query'] = $query;
    }

    /**
     * @return \FACTFinder\Data\Result
     */
    public function getResult()
    {
        if (is_null($this->result) || !$this->upToDate) {
            $this->request->resetLoaded();
            $this->result = $this->createResult();
            $this->upToDate = true;
        }

        return $this->result;
    }

    /**
     * @return \FACTFinder\Data\Result
     */
    private function createResult()
    {
        //init default values
        $records      = array();
        $resultCount = 0;

        $jsonData = $this->getResponseContent();

        if ($this->isValidResponse($jsonData)) {
            $searchResultData = $jsonData['searchResult'];

            if (!empty($searchResultData['records']))
            {
                $resultCount = $searchResultData['resultCount'];

                foreach ($searchResultData['records'] as $recordData)
                {
                    $position = $recordData['position'];

                    $record = FF::getInstance('Data\Record',
                        (string)$recordData['id'],
                        $recordData['record'],
                        $recordData['searchSimilarity'],
                        $position,
                        isset($recordData['seoPath']) ? $recordData['seoPath'] : '',
                        $recordData['keywords']
                    );

                    $records[] = $record;
                }
            }
        }

        return FF::getInstance(
            'Data\Result',
            $records,
            $resultCount
        );
    }

    /**
     * @return \FACTFinder\Data\SingleWordSearchItem[]
     */
    public function getSingleWordSearch()
    {
        if (is_null($this->singleWordSearch))
            $this->singleWordSearch = $this->createSingleWordSearch();

        return $this->singleWordSearch;
    }

    /**
     * @return \FACTFinder\Data\SingleWordSearchItem[]
     */
    private function createSingleWordSearch()
    {
        $singleWordSearch = array();

        $jsonData = $this->getResponseContent();
        if ($this->isValidResponse($jsonData) && !empty($jsonData['searchResult']['singleWordResults']))
        {
            foreach ($jsonData['searchResult']['singleWordResults'] as $swsData)
            {
                $item = FF::getInstance(
                    'Data\SingleWordSearchItem',
                    $swsData['word'],
                    $this->convertServerQueryToClientUrl(
                        $swsData['searchParams']
                    ),
                    $swsData['recordCount']
                );

                foreach ($swsData['previewRecords'] as $recordData)
                {
                    $item->addPreviewRecord(FF::getInstance('Data\Record',
                        (string)$recordData['id'],
                        $recordData['record'],
                        $recordData['searchSimilarity'],
                        $recordData['position'],
                        '',
                        $recordData['keywords']
                    ));
                }

                $singleWordSearch[] = $item;
            }
        }

        return $singleWordSearch;
    }

    /**
     * @return \FACTFinder\Data\SearchStatus
     */
    public function getStatus()
    {
        $searchStatusEnum = FF::getClassName('Data\SearchStatus');
        $status = $searchStatusEnum::NoResult();

        $jsonData = $this->getResponseContent();
        if ($this->isValidResponse($jsonData))
        {
            switch($jsonData['searchResult']['resultStatus'])
            {
            case 'nothingFound':
                $status = $searchStatusEnum::EmptyResult();
                break;
            case 'resultsFound':
                $status = $searchStatusEnum::RecordsFound();
                break;
            }
        }
        return $status;
    }

    /**
     * @return \FACTFinder\Data\ArticleNumberSearchStatus
     */
    public function getArticleNumberStatus()
    {
        $articleNumberSearchStatusEnum = FF::getClassName('Data\ArticleNumberSearchStatus');
        $status = $articleNumberSearchStatusEnum::IsNoArticleNumberSearch();

        $jsonData = $this->getResponseContent();
        if ($this->isValidResponse($jsonData))
        {
            switch ($jsonData['searchResult']['resultArticleNumberStatus'])
            {
            case 'resultsFound':
                $status = $articleNumberSearchStatusEnum::IsArticleNumberResultFound();
                break;
            case 'nothingFound':
                $status = $articleNumberSearchStatusEnum::IsNoArticleNumberResultFound();
                break;
            }
        }
        return $status;
    }

    /**
     * @return bool
     */
    public function isSearchTimedOut()
    {
        $jsonData = $this->getResponseContent();
        if ($this->isValidResponse($jsonData))
        {
            return $jsonData['searchResult']['timedOut'];
        }
        return true;
    }

    /**
     * @return \FACTFinder\Data\AfterSearchNavigation
     */
    public function getAfterSearchNavigation()
    {
        if (is_null($this->afterSearchNavigation))
            $this->afterSearchNavigation = $this->createAfterSearchNavigation();

        return $this->afterSearchNavigation;
    }

    /**
     * @return \FACTFinder\Data\AfterSearchNavigation
     */
    private function createAfterSearchNavigation()
    {
        $jsonData = $this->getResponseContent();

        $filterGroups = array();

        if ($this->isValidResponse($jsonData) && isset($jsonData['searchResult']['groups'])) {
            foreach ($jsonData['searchResult']['groups'] as $groupData)
                $filterGroups[] = $this->createFilterGroup($groupData);
        }

        return FF::getInstance(
            'Data\AfterSearchNavigation',
            $filterGroups
        );
    }

    /**
     * @param mixed[] $groupData An associative array corresponding to the JSON
     *        for a single filter group.
     * @return \FACTFinder\Data\FilterGroup
     */
    private function createFilterGroup($groupData)
    {
        $elements = array_merge(
            $groupData['selectedElements'],
            $groupData['elements']
        );

        $filterStyleEnum = FF::getClassName('Data\FilterStyle');
        switch ($groupData['filterStyle'])
        {
        case 'SLIDER':
            $filterStyle = $filterStyleEnum::Slider();
            break;
        case 'TREE':
            $filterStyle = $filterStyleEnum::Tree();
            break;
        case 'MULTISELECT':
            $filterStyle = $filterStyleEnum::MultiSelect();
            break;
        default:
            $filterStyle = $filterStyleEnum::Regular();
            break;
        }

        $filters = array();
        foreach ($elements as $filterData)
        {
            if ($filterStyle == $filterStyleEnum::Slider())
                $filters[] = $this->createSliderFilter($filterData);
            else
                $filters[] = $this->createFilter($filterData);
        }

        $filterSelectionType = null;
        $filterSelectionTypeEnum = FF::getClassName('Data\FilterSelectionType');
        if (isset($groupData['selectionType']))
        {
            switch ($groupData['selectionType'])
            {
            case 'multiSelectOr':
                $filterSelectionType = $filterSelectionTypeEnum::MultiSelectOr();
                break;
            case 'multiSelectAnd':
                $filterSelectionType = $filterSelectionTypeEnum::MultiSelectAnd();
                break;
            case 'singleShowUnselected':
                $filterSelectionType = $filterSelectionTypeEnum::SingleShowUnselected();
                break;
            default:
                $filterSelectionType = $filterSelectionTypeEnum::SingleHideUnselected();
                break;
            }
        }

        $filterType = null;
        $filterTypeEnum = FF::getClassName('Data\FilterType');
        if (isset($groupData['type']))
        {
            switch ($groupData['type'])
            {
            case 'number':
                $filterType = $filterTypeEnum::Number();
                break;
            default:
                $filterType = $filterTypeEnum::Text();
                break;
            }
        }

        return FF::getInstance(
            'Data\FilterGroup',
            $filters,
            $groupData['name'],
            $filterStyle,
            $groupData['detailedLinks'],
            $groupData['unit'],
            $filterSelectionType,
            $filterType,
            $groupData['showPreviewImages']
        );
    }

    /**
     * @param mixed[] $filterData An associative array corresponding to the JSON
     *        for a single filter.
     * @return \FACTFinder\Data\Filter
     */
    private function createFilter(array $filterData)
    {
        $filterLink = $this->convertServerQueryToClientUrl(
            $filterData['searchParams']
        );

        return FF::getInstance(
            'Data\Filter',
            $filterData['name'],
            $filterLink,
            $filterData['selected'],
            $filterData['associatedFieldName'],
            $filterData['recordCount'],
            $filterData['clusterLevel'],
            $filterData['previewImageURL'] ?: ''
        );
    }

    /**
     * @param mixed[] $filterData An associative array corresponding to the JSON
     *        for a single slider filter.
     * @return \FACTFinder\Data\SliderFilter
     */
    private function createSliderFilter(array $filterData)
    {
        // For sliders, FACT-Finder appends a filter parameter without value to
        // the 'searchParams' field, which is to be filled with the selected
        // minimum and maximum like 'filterValue=min-max'.
        // We split that parameter off, and treat it separately to ensure that
        // it stays the last parameter when converted to a client URL.
        preg_match(
            '/
            (.*)            # match and capture as much of the query as possible
            [?&]filter      # match "?filter" or "&filter" literally
            ([^&=]*)        # group 2, the field name
            =(?=$|&)        # make sure there is a "=" followed by the end of
                            # the string or another parameter
            (.*)            # match the remainder of the query
            /x',
            $filterData['searchParams'],
            $matches
        );

        if(!empty($matches)) {
         $query = $matches[1] . $matches[3];
            $fieldName = $matches[2];
        } else {
            // The URL of searchParams was not as expected, propably the current filter was not
            // added as an empty parameter. Therefore no need to remove it and we can use full searchParams URL.
            $query = $filterData['searchParams'];
            $fieldName = $filterData['associatedFieldName'];
        }

        if (urldecode($fieldName) != $filterData['associatedFieldName'])
            $this->log->warn('Filter parameter of slider does not correspond '
                           . 'to transmitted "associatedFieldName". Parameter: '
                           . "$fieldName. Field name: "
                           . $filterData['associatedFieldName'] . '.');

        $filterLink = $this->convertServerQueryToClientUrl($query);

        return FF::getInstance(
            'Data\SliderFilter',
            $filterLink,
            $fieldName,
            $filterData['absoluteMinValue'],
            $filterData['absoluteMaxValue'],
            $filterData['selectedMinValue'],
            $filterData['selectedMaxValue']
        );
    }

    /**
     * @return \FACTFinder\Data\ResultsPerPageOptions
     */
    public function getResultsPerPageOptions()
    {
        if (is_null($this->resultsPerPageOptions))
            $this->resultsPerPageOptions = $this->createResultsPerPageOptions();

        return $this->resultsPerPageOptions;
    }

    /**
     * @return \FACTFinder\Data\ResultsPerPageOptions
     */
    public function createResultsPerPageOptions()
    {
        $options = array();

        $defaultOption = null;
        $selectedOption = null;

        $jsonData = $this->getResponseContent();

        if ($this->isValidResponse($jsonData))
        {
            $rppData = $jsonData['searchResult']['resultsPerPageList'];
            if (!empty($rppData))
            {
                foreach ($rppData as $optionData)
                {
                    $optionLink = $this->convertServerQueryToClientUrl(
                        $optionData['searchParams']
                    );

                    $option = FF::getInstance(
                        'Data\Item',
                        $optionData['value'],
                        $optionLink,
                        $optionData['selected']
                    );

                    if ($optionData['default'])
                        $defaultOption = $option;
                    if ($optionData['selected'])
                        $selectedOption = $option;

                    $options[] = $option;
                }
            }
            return FF::getInstance(
                'Data\ResultsPerPageOptions',
                $options,
                $defaultOption,
                $selectedOption
            );
        }
        return null;
    }

    /**
     * @return \FACTFinder\Data\Paging
     */
    public function getPaging()
    {
        if (is_null($this->paging))
            $this->paging = $this->createPaging();

        return $this->paging;
    }

    /**
     * @return \FACTFinder\Data\Paging
     */
    private function createPaging()
    {
        $pages = array();

        $jsonData = $this->getResponseContent();

        if ($this->isValidResponse($jsonData))
        {
            $pagingData = $jsonData['searchResult']['paging'];
            if (!empty($pagingData))
            {
                $currentPage = null;
                $pageCount = $pagingData['pageCount'];

                foreach ($pagingData['pageLinks'] as $pageData)
                {
                    $page = $this->createPageItem($pageData);

                    if ($pageData['currentPage'])
                        $currentPage = $page;

                    $pages[] = $page;
                }
            }

            if (!$currentPage)
                $currentPage = FF::getInstance(
                    'Data\Page',
                    $pagingData['currentPage'],
                    $pagingData['currentPage'],
                    '#',
                    true
            );
            return FF::getInstance(
                'Data\Paging',
                $pages,
                $pageCount,
                $currentPage,
                $this->createPageItem($pagingData['firstLink']),
                $this->createPageItem($pagingData['lastLink']),
                $this->createPageItem($pagingData['previousLink']),
                $this->createPageItem($pagingData['nextLink'])
            );
        }
        return null;
    }

    /**
     * @param mixed[] $pageData An associative array corresponding to the JSON
     *        for a single page link.
     * @return \FACTFinder\Data\Item
     */
    private function createPageItem(array $pageData = null)
    {
        if (is_null($pageData))
            return null;

        $pageLink = $this->convertServerQueryToClientUrl(
            $pageData['searchParams']
        );

        return FF::getInstance(
            'Data\Page',
            $pageData['number'],
            $pageData['caption'],
            $pageLink,
            $pageData['currentPage']
        );
    }

    /**
     * @return \FACTFinder\Data\Sorting
     */
    public function getSorting()
    {
        if (is_null($this->sorting))
            $this->sorting = $this->createSorting();

        return $this->sorting;
    }

    /**
     * @return \FACTFinder\Data\Sorting
     */
    private function createSorting()
    {
        $sortOptions = array();

        $jsonData = $this->getResponseContent();

        if ($this->isValidResponse($jsonData))
        {
            $sortingData = $jsonData['searchResult']['sortsList'];
            if (!empty($sortingData))
            {
                foreach ($sortingData as $optionData)
                {
                    $optionLink = $this->convertServerQueryToClientUrl(
                        $optionData['searchParams']
                    );

                    $sortOptions[] = FF::getInstance(
                        'Data\Item',
                        $optionData['description'],
                        $optionLink,
                        $optionData['selected']
                    );
                }
            }

            return FF::getInstance(
                'Data\Sorting',
                $sortOptions
            );
        }
        return null;
    }

    /**
     * @return \FACTFinder\Data\SortingItems
     */
    public function getSortingItems()
    {
        if (is_null($this->sortingItems))
            $this->sortingItems = $this->createSortingItems();

        return $this->sortingItems;
    }

    /**
     * @return \FACTFinder\Data\SortingItems
     */
    private function createSortingItems()
    {
        $sortOptions = array();

        $jsonData = $this->getResponseContent();

        if ($this->isValidResponse($jsonData))
        {
            $sortingData = $jsonData['searchResult']['sortsList'];
            if (!empty($sortingData))
            {
                $orderEnum = FF::getClassName('Data\SortingDirection');
                foreach ($sortingData as $optionData)
                {
                    $optionLink = $this->convertServerQueryToClientUrl(
                        $optionData['searchParams']
                    );
                    if (isset($optionData['order']))
                    {
                        switch ($optionData['order'])
                        {
                            case 'asc':
                                $order = $orderEnum::Ascending();
                                break;
                            case 'desc':
                            default:
                                $order = $orderEnum::Descending();
                                break;
                        }
                    }

                    $sortOptions[] = FF::getInstance(
                        'Data\SortingItem',
                        $optionData['name'],
                        $order,
                        $optionData['description'],
                        $optionLink,
                        $optionData['selected']
                    );
                }
            }

            return FF::getInstance(
                'Data\SortingItems',
                $sortOptions
            );
        }
        return null;
    }

    /**
     * @return \FACTFinder\Data\BreadCrumbTrail
     */
    public function getBreadCrumbTrail()
    {
        if (is_null($this->breadCrumbTrail))
            $this->breadCrumbTrail = $this->createBreadCrumbTrail();

        return $this->breadCrumbTrail;
    }

    /**
     * @return \FACTFinder\Data\BreadCrumbTrail
     */
    private function createBreadCrumbTrail()
    {
        $breadCrumbs = array();

        $jsonData = $this->getResponseContent();

        if ($this->isValidResponse($jsonData))
        {
            $breadCrumbTrailData = $jsonData['searchResult']['breadCrumbTrailItems'];
            if (!empty($breadCrumbTrailData))
            {
                $i = 1;
                foreach ($breadCrumbTrailData as $breadCrumbData)
                {
                    $breadCrumbLink = $this->convertServerQueryToClientUrl(
                        $breadCrumbData['searchParams']
                    );

                    $breadCrumbTypeEnum = FF::getClassName('Data\BreadCrumbType');
                    switch ($breadCrumbData['type'])
                    {
                    case 'filter':
                        $type = $breadCrumbTypeEnum::Filter();
                        break;
                    case 'advisor':
                        $type = $breadCrumbTypeEnum::Advisor();
                        break;
                    default:
                        $type = $breadCrumbTypeEnum::Search();
                        break;
                    }

                    $breadCrumbs[] = FF::getInstance(
                        'Data\BreadCrumb',
                        $breadCrumbData['text'],
                        $breadCrumbLink,
                        $i == count($breadCrumbTrailData),
                        $type,
                        $breadCrumbData['associatedFieldName']
                    );

                    ++$i;
                }
            }
        }

        return FF::getInstance(
            'Data\BreadCrumbTrail',
            $breadCrumbs
        );
    }

    /**
     * @return \FACTFinder\Data\CampaignIterator
     */
    public function getCampaigns()
    {
        if (is_null($this->campaigns))
            $this->campaigns = $this->createCampaigns();

        return $this->campaigns;
    }

    /**
     * @return \FACTFinder\Data\CampaignIterator
     */
    private function createCampaigns()
    {
        $campaigns = array();
        $jsonData = $this->getResponseContent();

        if ($this->isValidResponse($jsonData) && isset($jsonData['searchResult']['campaigns'])) {
            foreach ($jsonData['searchResult']['campaigns'] as $campaignData) {
                $campaign = $this->createEmptyCampaignObject($campaignData);

                $this->fillCampaignObject($campaign, $campaignData);

                $campaigns[] = $campaign;
            }
        }

        $campaignIterator = FF::getInstance(
            'Data\CampaignIterator',
            $campaigns
        );
        return $campaignIterator;
    }

    /**
     * @param mixed[] $campaignData An associative array corresponding to the
     *        JSON for a single campaign.
     * @return \FACTFinder\Data\Campaign
     */
    private function createEmptyCampaignObject(array $campaignData)
    {
        return FF::getInstance(
            'Data\Campaign',
            $campaignData['name'],
            $campaignData['category'],
            $campaignData['target']['destination']
        );
    }

    /**
     * @param \FACTFinder\Data\Campaign $campaign The campaign object to be
     *        filled.
     * @param mixed[] $campaignData An associative array corresponding to the
     *        JSON for that campaign.
     */
    private function fillCampaignObject(
        \FACTFinder\Data\Campaign $campaign,
        array $campaignData
    ) {
        switch ($campaignData['flavour'])
        {
        case 'FEEDBACK':
            $this->fillCampaignWithFeedback($campaign, $campaignData);
            $this->fillCampaignWithPushedProducts($campaign, $campaignData);
            break;
        case 'ADVISOR':
            $this->fillCampaignWithAdvisorData($campaign, $campaignData);
            break;
        }
    }

    /**
     * @param \FACTFinder\Data\Campaign $campaign The campaign object to be
     *        filled.
     * @param mixed[] $campaignData An associative array corresponding to the
     *        JSON for that campaign.
     */
    protected function fillCampaignWithFeedback(
        \FACTFinder\Data\Campaign $campaign,
        array $campaignData
    ) {
        if (!empty($campaignData['feedbackTexts']))
        {
            $feedback = array();

            foreach ($campaignData['feedbackTexts'] as $feedbackData)
            {
                // If present, add the feedback to both the label and the ID.
                $html = $feedbackData['html'];
                $text = $feedbackData['text'];
                if (!$html)
                {
                    $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
                }

                $label = $feedbackData['label'];
                if ($label !== '')
                    $feedback[$label] = $text;

                $id = $feedbackData['id'];
                if ($id !== null)
                    $feedback[$id] = $text;
            }

            $campaign->addFeedback($feedback);
        }
    }

    /**
     * @param \FACTFinder\Data\Campaign $campaign The campaign object to be
     *        filled.
     * @param mixed[] $campaignData An associative array corresponding to the
     *        JSON for that campaign.
     */
    private function fillCampaignWithPushedProducts(
        \FACTFinder\Data\Campaign $campaign,
        array $campaignData
    ) {
        if (!empty($campaignData['pushedProductsRecords']))
        {
            $pushedProducts = array();

            foreach ($campaignData['pushedProductsRecords'] as $recordData)
            {
                $pushedProducts[] = FF::getInstance(
                    'Data\Record',
                    (string)$recordData['id'],
                    $recordData['record']
                );
            }

            $campaign->addPushedProducts($pushedProducts);
        }
    }

    /**
     * @param \FACTFinder\Data\Campaign $campaign The campaign object to be
     *        filled.
     * @param mixed[] $campaignData An associative array corresponding to the
     *        JSON for that campaign.
     */
    private function fillCampaignWithAdvisorData(
        \FACTFinder\Data\Campaign $campaign,
        array $campaignData
    ) {
        $activeQuestions = array();

        foreach ($campaignData['activeQuestions'] as $questionData)
            $activeQuestions[] = $this->createAdvisorQuestion($questionData);

        $campaign->addActiveQuestions($activeQuestions);

        // Fetch advisor tree if it exists
        $advisorTree = array();

        foreach ($campaignData['activeQuestions'] as $questionData)
            $activeQuestions[] = $this->createAdvisorQuestion($questionData,
                                                               true);

        $campaign->addToAdvisorTree($advisorTree);
    }

    /**
     * @param mixed[] $questionData An associative array corresponding to the
     *        JSON for a single advisor question.
     * @param bool $recursive If this is set the entire advisor tree below this
     *        question will be created. Otherwise, follow-up questions of
     *        answers are omitted.
     */
    private function createAdvisorQuestion($questionData, $recursive = false)
    {
        $answers = array();

        foreach ($questionData['answers'] as $answerData)
            $answers[] = $this->createAdvisorAnswer($answerData, $recursive);

        return FF::getInstance('Data\AdvisorQuestion',
            $questionData['text'],
            $answers
        );
    }

    /**
     * @param mixed[] $answerData An associative array corresponding to the
     *        JSON for a single advisor answer.
     * @param bool $recursive If this is set the entire advisor tree below the
     *        subquestion of this ansewr will be created as well.
     */
    private function createAdvisorAnswer($answerData, $recursive = false)
    {
        $params =  $this->convertServerQueryToClientUrl(
            $answerData['params']
        );

        $followUpQuestions = array();
        if ($recursive)
            foreach ($answerData['questions'] as $questionData)
                $followUpQuestions[] = $this->createAdvisorQuestion(
                    $questionData,
                    true
                );

        return FF::getInstance('Data\AdvisorAnswer',
            $answerData['text'],
            $params,
            $followUpQuestions
        );
    }

    /**
     * Value for parameter "followSearch" for followups on initial search like filters, pagination, ...
     * Either from search results searchParams, request parameters or from search results "simiFirstRecord".
     * Returns 0 if no parameter "followSearch" could be acquired.
     *
     * @return int
     */
    public function getFollowSearchValue()
    {
        $jsonData = $this->getResponseContent();
        //use searchParams of result if available
        if ($this->isValidResponse($jsonData) && isset($jsonData['searchResult']['searchParams'])) {
            $parameters = FF::getInstance(
                'Util\Parameters',
                $jsonData['searchResult']['searchParams']
            );
        //fallback to current request
        } else {
            $parameters = $this->parameters;
        }
        $searchParameters = FF::getInstance(
            'Data\SearchParameters',
            $parameters
        );
        $sorting = $searchParameters->getSortings();
        // check if followSearch was set in request data or sent by FF in result searchParams
        if($searchParameters->getFollowSearch() !== 0) {
            $followSearch =  $searchParameters->getFollowSearch();
        // use simiFirstRecord only if result was not sorted
        } elseif (empty($sorting)) {
            $jsonData = $this->getResponseContent();
            if($jsonData && $jsonData['searchResult'] && isset($jsonData['searchResult']['simiFirstRecord']))
                $followSearch = $jsonData['searchResult']['simiFirstRecord'];
        //mark as not valid
        } else {
            $followSearch = 0;
        }
        return $followSearch;
    }

    /**
     * Returns true if the search response is valid or false if an error occurred.
     *
     * @return bool
     */
    protected function isValidResponse($jsonData)
    {
        return (!empty($jsonData) && !isset($jsonData['error']) && isset($jsonData['searchResult']));
    }
}


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

/**
 * Users should extend this class to implement customized logging
 * event filtering. Note that {@link LoggerCategory} and {@link LoggerAppender},
 * the parent class of all standard
 * appenders, have built-in filtering rules. It is suggested that you
 * first use and understand the built-in rules before rushing to write
 * your own custom filters.
 *
 *
This abstract class assumes and also imposes that filters be
 * organized in a linear chain. The {@link #decide
 * decide(LoggerLoggingEvent)} method of each filter is called sequentially,
 * in the order of their addition to the chain.
 *
 *
The {@link decide()} method must return one
 * of the integer constants {@link LoggerFilter::DENY},
 * {@link LoggerFilter::NEUTRAL} or {@link LoggerFilter::ACCEPT}.
 *
 *
If the value {@link LoggerFilter::DENY} is returned, then the log event is
 * dropped immediately without consulting with the remaining
 * filters.
 *
 *
If the value {@link LoggerFilter::NEUTRAL} is returned, then the next filter
 * in the chain is consulted. If there are no more filters in the
 * chain, then the log event is logged. Thus, in the presence of no
 * filters, the default behaviour is to log all logging events.
 *
 *
If the value {@link LoggerFilter::ACCEPT} is returned, then the log
 * event is logged without consulting the remaining filters.
 *
 *
The philosophy of log4php filters is largely inspired from the
 * Linux ipchains.
 *
 * @version $Revision: 1213283 $
 * @package log4php
 */
abstract class LoggerFilter extends LoggerConfigurable {

	/**
	 * The log event must be logged immediately without consulting with
	 * the remaining filters, if any, in the chain.
	 */
	const ACCEPT = 1;

	/**
	 * This filter is neutral with respect to the log event. The
	 * remaining filters, if any, should be consulted for a final decision.
	 */
	const NEUTRAL = 0;

	/**
	 * The log event must be dropped immediately without consulting
	 * with the remaining filters, if any, in the chain.
	 */
	const DENY = -1;

	/**
	 * @var LoggerFilter Points to the next {@link LoggerFilter} in the filter chain.
	 */
	protected $next;

	/**
	 * Usually filters options become active when set. We provide a
	 * default do-nothing implementation for convenience.
	*/
	public function activateOptions() {
	}

	/**
	 * Decide what to do.
	 *
If the decision is {@link LoggerFilter::DENY}, then the event will be
	 * dropped. If the decision is {@link LoggerFilter::NEUTRAL}, then the next
	 * filter, if any, will be invoked. If the decision is {@link LoggerFilter::ACCEPT} then
	 * the event will be logged without consulting with other filters in
	 * the chain.
	 *
	 * @param LoggerLoggingEvent $event The {@link LoggerLoggingEvent} to decide upon.
	 * @return integer {@link LoggerFilter::NEUTRAL} or {@link LoggerFilter::DENY}|{@link LoggerFilter::ACCEPT}
	 */
	public function decide(LoggerLoggingEvent $event) {
		return self::NEUTRAL;
	}

	/**
	 * Adds a new filter to the filter chain this filter is a part of.
	 * If this filter has already and follow up filter, the param filter
	 * is passed on until it is the last filter in chain.
	 *
	 * @param $filter - the filter to add to this chain
	 */
	public function addNext($filter) {
		if($this->next !== null) {
			$this->next->addNext($filter);
		} else {
			$this->next = $filter;
		}
	}

	/**
	 * Returns the next filter in this chain
	 * @return the next filter
	 */
	public function getNext() {
		return $this->next;
	}

}


<?php
namespace FACTFinder\Core\Server;

use FACTFinder\Loader as FF;

/**
 * Represents all data belonging to a single connection. It holds all data
 * necessary to configure a request and can also be filled with the
 * corresponding response.
 */
class ConnectionData
{
    /**
     * @var \FACTFinder\Util\Parameters
     */
    private $httpHeaderFields;

    /**
     * @var string
     */
    private $action;

    /**
     * @var \FACTFinder\Util\Parameters
     */
    private $parameters;

    /**
     * @var mixed[]
     */
    private $connectionOptions = array();

    /**
     * @var Response
     */
    private $response;

    /**
     * @var string
     */
    private $previousUrl;

    /**
     * Optionally takes a Parameters object to initialize the query parameters.
     * @param \FACTFinder\Util\Parameters $parameters
     * @return type
     */
    public function __construct($parameters = null)
    {
        if (FF::isInstanceOf($parameters, 'Util\Parameters'))
            $this->parameters = $parameters;
        else
            $this->parameters = FF::getInstance('Util\Parameters');

        $this->httpHeaderFields = FF::getInstance('Util\Parameters');
        $this->action = '';
        $this->setNullResponse();
    }

    /**
     * Returns the parameters object used for the connection, on which HTTP
     * header fields can be read and changed.
     *
     * @return \FACTFinder\Util\Parameters
     */
    public function getHttpHeaderFields()
    {
        return $this->httpHeaderFields;
    }

    /**
     * Set the action to be queried on the FACT-Finder server. e.g. "Search.ff".
     *
     * @param string $action
     */
    public function setAction($action)
    {
        $this->action = $action;
    }

    /**
     * Get the action to be queried on the FACT-Finder server. e.g. "Search.ff".
     *
     * @return string
     */
    public function getAction()
    {
        return $this->action;
    }

    /**
     * Returns the parameters object used for the connection, on which query
     * parameters can be read and changed.
     *
     * @return \FACTFinder\Util\Parameters
     */
    public function getParameters()
    {
        return $this->parameters;
    }

    /**
     * Set an option for the connection. This can be an arbitrary pair of key
     * and value. How this is used depends on the kind of connection that is set
     * up (e.g. cURL or socket). We simply provide an array to store options
     * specific to this connection.
     *
     * @param mixed $name The option's identifier. This can be any value that is
     *        a valid array index in PHP (hence, it SHOULD be a string or an
     *        integer; all other types will be interpreted as a string).
     * @param mixed $value The option's value.
     */
    public function setConnectionOption($name, $value)
    {
        $this->connectionOptions[$name] = $value;
    }

    /**
     * Sets all given connection options at once. If an option with the same
     * name already exists, the old value will be replaced. Options that are not
     * contained in the $options parameter will retain their value(s).
     * @param mixed[] $options An array of options. For more information on
     *        valid keys and values see setConnectionOption($name, $value).
     */
    public function setConnectionOptions($options)
    {
        // We cannot use array_merge() here, because that does not preserve
        // numeric keys. Implementing this with a loop also has the advantage
        // of not creating a new, third array.
        foreach ($options as $k => $v)
            $this->connectionOptions[$k] = $v;
    }

    /**
     * @param mixed $name The option's identifier.
     * @return bool
     */
    public function issetConnectionOption($name)
    {
        return isset($this->connectionOptions[$name]);
    }

    /**
     * @param mixed $name The option's identifier.
     * @return mixed The option's value.
     */
    public function getConnectionOption($name)
    {
        return $this->connectionOptions[$name];
    }

    /**
     * Get an array of all set connection options.
     * @return mixed[]
     */
    public function getConnectionOptions()
    {
        return $this->connectionOptions;
    }

    /**
     * Set a response for the current connection settings along with the URL
     * which was used to obtain the response.
     * @param Response $response
     * @param string $url The URL corresponding to $response.
     */
    public function setResponse(Response $response, $url)
    {
        $this->response = $response;
        $this->previousUrl = $url;
    }

    /**
     * Set a null response - e.g. if the connection could not even be attempted
     * at all.
     */
    public function setNullResponse()
    {
        $this->response = FF::getInstance('Core\Server\NullResponse');
        $this->previousUrl = null;
    }

    /**
     * Get the response that was most recently set.
     *
     * @return Response
     */
    public function getResponse()
    {
        return $this->response;
    }

    /**
     * Get the URL corresponding to the currently stored Response.
     *
     * @return string
     */
    public function getPreviousUrl()
    {
        return $this->previousUrl;
    }
}


<?php
namespace FACTFinder\Adapter;

use FACTFinder\Loader as FF;

/**
 * Base class for all adapters which support the personalisation.
 */
abstract class PersonalisedResponse extends ConfigurableResponse
{
    /**
     * @var string
     */
    protected $sid = false;

    /**
     * @param string $loggerClass Class name of logger to use. The class should
     *        implement FACTFinder\Util\LoggerInterface.
     * @param \FACTFinder\Core\ConfigurationInterface $configuration
     *        Configuration object to use.
     * @param \FACTFinder\Core\Server\Request $request The request object from
     *        which to obtain the server data.
     * @param \FACTFinder\Core\Client\UrlBuilder $urlBuilder
     *        Client URL builder object to use.
     * @param \FACTFinder\Core\encodingConverter $encodingConverter
     *        Encoding converter object to use
     */
    public function __construct(
        $loggerClass,
        \FACTFinder\Core\ConfigurationInterface $configuration,
        \FACTFinder\Core\Server\Request $request,
        \FACTFinder\Core\Client\UrlBuilder $urlBuilder,
        \FACTFinder\Core\AbstractEncodingConverter $encodingConverter = null
    ) {
        parent::__construct($loggerClass, $configuration, $request,
                            $urlBuilder, $encodingConverter);
    }

    /**
     * Set the session id for personalization.
     *
     * @param string $sessionId session id
     */
    public function setSid($sessionId)
    {
        if (strcmp($sessionId, $this->sid) !== 0) {
            $this->sid = $sessionId;
            $this->parameters['sid'] = $this->sid;
            $this->upToDate = false;
        }
    }
 }


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

/**
 * Defines the minimum set of levels recognized by the system, that is
 * OFF, FATAL, ERROR,
 * WARN, INFO, DEBUG and
 * ALL.
 *
 *
The LoggerLevel class may be subclassed to define a larger
 * level set.

 *
 * @version $Revision: 1379729 $
 * @package log4php
 * @since 0.5
 */
class LoggerLevel {

	const OFF = 2147483647;
	const FATAL = 50000;
	const ERROR = 40000;
	const WARN = 30000;
	const INFO = 20000;
	const DEBUG = 10000;
	const TRACE = 5000;
	const ALL = -2147483647;

	/** Integer level value. */
	private $level;

	/** Contains a list of instantiated levels. */
	private static $levelMap;

	/** String representation of the level. */
	private $levelStr;

	/**
	 * Equivalent syslog level.
	 * @var integer
	 */
	private $syslogEquivalent;

	/**
	 * Constructor
	 *
	 * @param integer $level
	 * @param string $levelStr
	 * @param integer $syslogEquivalent
	 */
	private function __construct($level, $levelStr, $syslogEquivalent) {
		$this->level = $level;
		$this->levelStr = $levelStr;
		$this->syslogEquivalent = $syslogEquivalent;
	}

	/**
	 * Compares two logger levels.
	 *
	 * @param LoggerLevels $other
	 * @return boolean
	 */
	public function equals($other) {
		if($other instanceof LoggerLevel) {
			if($this->level == $other->level) {
				return true;
			}
		} else {
			return false;
		}
	}

	/**
	 * Returns an Off Level
	 * @return LoggerLevel
	 */
	public static function getLevelOff() {
		if(!isset(self::$levelMap[LoggerLevel::OFF])) {
			self::$levelMap[LoggerLevel::OFF] = new LoggerLevel(LoggerLevel::OFF, 'OFF', LOG_ALERT);
		}
		return self::$levelMap[LoggerLevel::OFF];
	}

	/**
	 * Returns a Fatal Level
	 * @return LoggerLevel
	 */
	public static function getLevelFatal() {
		if(!isset(self::$levelMap[LoggerLevel::FATAL])) {
			self::$levelMap[LoggerLevel::FATAL] = new LoggerLevel(LoggerLevel::FATAL, 'FATAL', LOG_ALERT);
		}
		return self::$levelMap[LoggerLevel::FATAL];
	}

	/**
	 * Returns an Error Level
	 * @return LoggerLevel
	 */
	public static function getLevelError() {
		if(!isset(self::$levelMap[LoggerLevel::ERROR])) {
			self::$levelMap[LoggerLevel::ERROR] = new LoggerLevel(LoggerLevel::ERROR, 'ERROR', LOG_ERR);
		}
		return self::$levelMap[LoggerLevel::ERROR];
	}

	/**
	 * Returns a Warn Level
	 * @return LoggerLevel
	 */
	public static function getLevelWarn() {
		if(!isset(self::$levelMap[LoggerLevel::WARN])) {
			self::$levelMap[LoggerLevel::WARN] = new LoggerLevel(LoggerLevel::WARN, 'WARN', LOG_WARNING);
		}
		return self::$levelMap[LoggerLevel::WARN];
	}

	/**
	 * Returns an Info Level
	 * @return LoggerLevel
	 */
	public static function getLevelInfo() {
		if(!isset(self::$levelMap[LoggerLevel::INFO])) {
			self::$levelMap[LoggerLevel::INFO] = new LoggerLevel(LoggerLevel::INFO, 'INFO', LOG_INFO);
		}
		return self::$levelMap[LoggerLevel::INFO];
	}

	/**
	 * Returns a Debug Level
	 * @return LoggerLevel
	 */
	public static function getLevelDebug() {
		if(!isset(self::$levelMap[LoggerLevel::DEBUG])) {
			self::$levelMap[LoggerLevel::DEBUG] = new LoggerLevel(LoggerLevel::DEBUG, 'DEBUG', LOG_DEBUG);
		}
		return self::$levelMap[LoggerLevel::DEBUG];
	}

	/**
	 * Returns a Trace Level
	 * @return LoggerLevel
	 */
	public static function getLevelTrace() {
		if(!isset(self::$levelMap[LoggerLevel::TRACE])) {
			self::$levelMap[LoggerLevel::TRACE] = new LoggerLevel(LoggerLevel::TRACE, 'TRACE', LOG_DEBUG);
		}
		return self::$levelMap[LoggerLevel::TRACE];
	}

	/**
	 * Returns an All Level
	 * @return LoggerLevel
	 */
	public static function getLevelAll() {
		if(!isset(self::$levelMap[LoggerLevel::ALL])) {
			self::$levelMap[LoggerLevel::ALL] = new LoggerLevel(LoggerLevel::ALL, 'ALL', LOG_DEBUG);
		}
		return self::$levelMap[LoggerLevel::ALL];
	}

	/**
	 * Return the syslog equivalent of this level as an integer.
	 * @return integer
	 */
	public function getSyslogEquivalent() {
		return $this->syslogEquivalent;
	}

	/**
	 * Returns true if this level has a higher or equal
	 * level than the level passed as argument, false
	 * otherwise.
	 *
	 * @param LoggerLevel $other
	 * @return boolean
	 */
	public function isGreaterOrEqual($other) {
		return $this->level >= $other->level;
	}

	/**
	 * Returns the string representation of this level.
	 * @return string
	 */
	public function toString() {
		return $this->levelStr;
	}

	/**
	 * Returns the string representation of this level.
	 * @return string
	 */
	public function __toString() {
		return $this->toString();
	}

	/**
	 * Returns the integer representation of this level.
	 * @return integer
	 */
	public function toInt() {
		return $this->level;
	}

	/**
	 * Convert the input argument to a level. If the conversion fails, then
	 * this method returns the provided default level.
	 *
	 * @param mixed $arg The value to convert to level.
	 * @param LoggerLevel $default Value to return if conversion is not possible.
	 * @return LoggerLevel
	 */
	public static function toLevel($arg, $defaultLevel = null) {
		if(is_int($arg)) {
			switch($arg) {
				case self::ALL:	return self::getLevelAll();
				case self::TRACE: return self::getLevelTrace();
				case self::DEBUG: return self::getLevelDebug();
				case self::INFO: return self::getLevelInfo();
				case self::WARN: return self::getLevelWarn();
				case self::ERROR: return self::getLevelError();
				case self::FATAL: return self::getLevelFatal();
				case self::OFF:	return self::getLevelOff();
				default: return $defaultLevel;
			}
		} else {
			switch(strtoupper($arg)) {
				case 'ALL':	return self::getLevelAll();
				case 'TRACE': return self::getLevelTrace();
				case 'DEBUG': return self::getLevelDebug();
				case 'INFO': return self::getLevelInfo();
				case 'WARN': return self::getLevelWarn();
				case 'ERROR': return self::getLevelError();
				case 'FATAL': return self::getLevelFatal();
				case 'OFF':	return self::getLevelOff();
				default: return $defaultLevel;
			}
		}
	}
}


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

/**
 * A convenience class to convert property values to specific types.
 *
 * @version $Revision: 1374617 $
 * @package log4php
 * @subpackage helpers
 * @since 0.5
 */
class LoggerOptionConverter {

	/** String values which are converted to boolean TRUE. */
	private static $trueValues = array('1', 'true', 'yes', 'on');

	/**
	 * String values which are converted to boolean FALSE.
	 *
	 * Note that an empty string must convert to false, because
	 * parse_ini_file() which is used for parsing configuration
	 * converts the value _false_ to an empty string.
	 */
	private static $falseValues = array('0', 'false', 'no', 'off', '');

	/**
	 * Read a predefined var.
	 *
	 * It returns a value referenced by <var>$key</var> using this search criteria:
	 * - if <var>$key</var> is a constant then return it. Else
	 * - if <var>$key</var> is set in <var>$_ENV</var> then return it. Else
	 * - return <var>$def</var>.
	 *
	 * @param string $key The key to search for.
	 * @param string $def The default value to return.
	 * @return string	the string value of the system property, or the default
	 *					value if there is no property with that key.
	 */
	public static function getSystemProperty($key, $def) {
		if(defined($key)) {
			return (string)constant($key);
		} else if(isset($_SERVER[$key])) {
			return (string)$_SERVER[$key];
		} else if(isset($_ENV[$key])) {
			return (string)$_ENV[$key];
		} else {
			return $def;
		}
	}

	/** Converts $value to boolean, or throws an exception if not possible. */
	public static function toBooleanEx($value) {
		if (isset($value)) {
			if (is_bool($value)) {
				return $value;
			}
			$value = strtolower(trim($value));
			if (in_array($value, self::$trueValues)) {
				return true;
			}
			if (in_array($value, self::$falseValues)) {
				return false;
			}
		}

		throw new LoggerException("Given value [" . var_export($value, true) . "] cannot be converted to boolean.");
	}

	/**
	 * Converts $value to integer, or throws an exception if not possible.
	 * Floats cannot be converted to integer.
	 */
	public static function toIntegerEx($value) {
		if (is_integer($value)) {
			return $value;
		}
		if (is_numeric($value) && ($value == (integer) $value)) {
			return (integer) $value;
		}

		throw new LoggerException("Given value [" . var_export($value, true) . "] cannot be converted to integer.");
	}

	/**
	 * Converts $value to integer, or throws an exception if not possible.
	 * Floats cannot be converted to integer.
	 */
	public static function toPositiveIntegerEx($value) {
		if (is_integer($value) && $value > 0) {
			return $value;
		}
		if (is_numeric($value) && ($value == (integer) $value) && $value > 0) {
			return (integer) $value;
		}

		throw new LoggerException("Given value [" . var_export($value, true) . "] cannot be converted to a positive integer.");
	}

	/** Converts the value to a level. Throws an exception if not possible. */
	public static function toLevelEx($value) {
		if ($value instanceof LoggerLevel) {
			return $value;
		}
		$level = LoggerLevel::toLevel($value);
		if ($level === null) {
			throw new LoggerException("Given value [" . var_export($value, true) . "] cannot be converted to a logger level.");
		}
		return $level;
	}

	/**
	 * Converts a value to a valid file size (integer).
	 *
	 * Supports 'KB', 'MB' and 'GB' suffixes, where KB = 1024 B etc.
	 *
	 * The final value will be rounded to the nearest integer.
	 *
	 * Examples:
	 * - '100' => 100
	 * - '100.12' => 100
	 * - '100KB' => 102400
	 * - '1.5MB' => 1572864
	 *
	 * @param mixed $value File size (optionally with suffix).
	 * @return integer Parsed file size.
	 */
	public static function toFileSizeEx($value) {

		if (empty($value)) {
			throw new LoggerException("Empty value cannot be converted to a file size.");
		}

		if (is_numeric($value)) {
			return (integer) $value;
		}

		if (!is_string($value)) {
			throw new LoggerException("Given value [" . var_export($value, true) . "] cannot be converted to a file size.");
		}

		$str = strtoupper(trim($value));
		$count = preg_match('/^([0-9.]+)(KB|MB|GB)?$/', $str, $matches);

		if ($count > 0) {
			$size = $matches[1];
			$unit = $matches[2];

			switch($unit) {
				case 'KB': $size *= pow(1024, 1); break;
				case 'MB': $size *= pow(1024, 2); break;
				case 'GB': $size *= pow(1024, 3); break;
			}

			return (integer) $size;
		}

		throw new LoggerException("Given value [$value] cannot be converted to a file size.");
	}

	/**
	 * Converts a value to string, or throws an exception if not possible.
	 *
	 * Objects can be converted to string if they implement the magic
	 * __toString() method.
	 *
	 */
	public static function toStringEx($value) {
		if (is_string($value)) {
			return $value;
		}
		if (is_numeric($value)) {
			return (string) $value;
		}
		if (is_object($value) && method_exists($value, '__toString')) {
			return (string) $value;
		}

		throw new LoggerException("Given value [" . var_export($value, true) . "] cannot be converted to string.");
	}

	/**
	 * Performs value substitution for string options.
	 *
	 * An option can contain PHP constants delimited by '${' and '}'.
	 *
	 * E.g. for input string "some ${FOO} value", the method will attempt
	 * to substitute ${FOO} with the value of constant FOO if it exists.
	 *
	 * Therefore, if FOO is a constant, and it has value "bar", the resulting
	 * string will be "some bar value".
	 *
	 * If the constant is not defined, it will be replaced by an empty string,
	 * and the resulting string will be "some  value".
	 *
	 * @param string $string String on which to perform substitution.
	 * @return string
	 */
	public static function substConstants($string) {
		preg_match_all('/\${([^}]+)}/', $string, $matches);

		foreach($matches[1] as $key => $match) {
			$match = trim($match);
			$search = $matches[0][$key];
			$replacement = defined($match) ? constant($match) : '';
			$string = str_replace($search, $replacement, $string);
		}
		return $string;
	}
}


<?php
namespace FACTFinder\Core\Server;

use FACTFinder\Loader as FF;

/**
 * This implementation retrieves the FACT-Finder data by using the "easy cURL
 * interface" (I didn't even make that up; that's what cURL itself calls it:
 * 'easy' as opposed to 'multi').
 * Responses are queried sequentially and lazily and are cached as long as
 * parameters don't change.
 */
class EasyCurlDataProvider extends AbstractDataProvider
{
    /**
     * @var \FACTFinder\Util\LoggerInterface
     */
    private $log;

    /**
     * @var UrlBuilder
     */
    protected $urlBuilder;

    /**
     * @var \FACTFinder\Util\CurlInterface
     */
    protected $curl;

    protected $defaultCurlOptions;
    protected $necessaryCurlOptions;

    public function __construct(
        $loggerClass,
        \FACTFinder\Core\ConfigurationInterface $configuration,
        \FACTFinder\Util\CurlInterface $curl,
        UrlBuilder $urlBuilder
    ) {
        parent::__construct($loggerClass, $configuration);

        $this->log = $loggerClass::getLogger(__CLASS__);

        $this->urlBuilder = $urlBuilder;

        $this->curl = $curl;

        $this->defaultCurlOptions = array(
            CURLOPT_CONNECTTIMEOUT => $this->configuration->getDefaultConnectTimeout(),
            CURLOPT_TIMEOUT        => $this->configuration->getDefaultTimeout(),
            CURLOPT_SSL_VERIFYPEER => false,
            CURLOPT_SSL_VERIFYHOST => false,
            CURLOPT_ENCODING       => '',
        );

        $this->necessaryCurlOptions = array(
            CURLOPT_RETURNTRANSFER => true,
        );
    }

    public function setConnectTimeout($id, $timeout)
    {
        if (!isset($this->connectionData[$id]))
            throw new \InvalidArgumentException('Tried to set timeout for invalid ID $id.');

        $this->connectionData[$id]->setConnectionOption(
            CURLOPT_CONNECTTIMEOUT,
            $timeout
        );
    }

    public function setTimeout($id, $timeout)
    {
        if (!isset($this->connectionData[$id]))
            throw new \InvalidArgumentException('Tried to set timeout for invalid ID $id.');

        $this->connectionData[$id]->setConnectionOption(
            CURLOPT_TIMEOUT,
            $timeout
        );
    }

    // TODO: Could this be refactored some more?
    public function loadResponse($id)
    {
        if (!isset($this->connectionData[$id]))
            throw new \InvalidArgumentException('Tried to get response for invalid ID $id.');

        if (!$this->hasUrlChanged($id))
            return;

        $connectionData = $this->connectionData[$id];

        $action = $connectionData->getAction();
        if (empty($action))
        {
            $this->log->error('Request type missing.');
            $connectionData->setNullResponse();
            return;
        }

        $httpHeaderFields = $this->prepareHttpHeaders($connectionData);
        $parameters = $this->prepareParameters($connectionData);
        $url = $this->prepareConnectionOptions(
            $connectionData,
            $httpHeaderFields,
            $parameters
        );

        $response = $this->retrieveResponse($connectionData);

        $connectionData->setResponse($response, $url);

        $this->logResult($response);
    }

    private function prepareHttpHeaders($connectionData)
    {
        $httpHeaderFields = clone $connectionData->getHttpHeaderFields();

        $language = $this->configuration->getLanguage();
        if (!empty($language))
            $httpHeaderFields['Accept-Language'] = $language;

        return $httpHeaderFields;
    }

    private function prepareParameters($connectionData)
    {
        $parameters = clone $connectionData->getParameters();

        if ($this->configuration->isDebugEnabled())
            $parameters['verbose'] = 'true';

        return $parameters;
    }

    private function prepareConnectionOptions(
        $connectionData,
        $httpHeaderFields,
        $parameters
    ) {
        if ($this->configuration->isDebugEnabled()
            && isset($_SERVER['HTTP_REFERER'])
            && !$connectionData->issetConnectionOption(CURLOPT_REFERER)
        ) {
            $connectionData->setConnectionOption(
                CURLOPT_REFERER,
                $_SERVER['HTTP_REFERER']
            );
        }

        $connectionData->setConnectionOption(
            CURLOPT_HTTPHEADER,
            $httpHeaderFields->toHttpHeaderFields()
        );

        $url = $this->urlBuilder->getAuthenticationUrl(
            $connectionData->getAction(),
            $parameters
        );

        $connectionData->setConnectionOption(CURLOPT_URL, $url);

        return $url;
    }

    private function retrieveResponse($connectionData)
    {
        $curlHandle = $this->curl->init();
        if ($curlHandle === false)
        {
            $this->log->error("curl_init() did not return a handle for ID $id. "
                            . 'Setting an empty response...');
            return FF::getInstance('Core\Server\NullResponse');
        }

        // We cannot use array_merge() here, because that does not preserve
        // numeric keys. So we use array union instead. However, as opposed to
        // array_merge(), the left-hand operator's keys will be preserved.
        $curlOptions = $this->necessaryCurlOptions
                     + $connectionData->getConnectionOptions()
                     + $this->defaultCurlOptions;

        $this->curl->setopt_array(
            $curlHandle,
            $curlOptions
        );

        $responseText = $this->curl->exec($curlHandle);
        $httpCode = (int)$this->curl->getinfo($curlHandle, CURLINFO_HTTP_CODE);
        $curlErrorNumber = $this->curl->errno($curlHandle);
        $curlError = $this->curl->error($curlHandle);

        $this->curl->close($curlHandle);

        return FF::getInstance('Core\Server\Response',
            $responseText,
            $httpCode,
            $curlErrorNumber,
            $curlError
        );
    }

    private function logResult($response)
    {
        $httpCode = $response->getHttpCode();
        $curlError = $response->getConnectionError();
        if ($httpCode >= 400) {
            $this->log->error("Connection failed. HTTP code: $httpCode");
        } else if ($httpCode == 0) {
            $this->log->error("Connection refused. cURL error: $curlError");
        } else if (floor($httpCode / 100) == 2) { // all successful status codes (2**)
            $this->log->info("Request successful!");
        }
    }

    private function hasUrlChanged($id)
    {
        $connectionData = $this->connectionData[$id];

        if (FF::isInstanceOf($connectionData->getResponse(), 'Core\Server\NullResponse'))
            return true;

        $url = $this->urlBuilder->getNonAuthenticationUrl(
            $connectionData->getAction(),
            $this->prepareParameters($connectionData)
        );
        return $url != $connectionData->getPreviousUrl();
    }
}


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

/**
 * This class is specialized in retrieving loggers by name and also maintaining
 * the logger hierarchy. The logger hierarchy is dealing with the several Log-Levels
 * Logger can have. From log4j website:
 *
 * "A logger is said to be an ancestor of another logger if its name followed
 * by a dot is a prefix of the descendant logger name. A logger is said to be
 * a parent of a child logger if there are no ancestors between itself and the
 * descendant logger."
 *
 * Child Loggers do inherit their Log-Levels from their Ancestors. They can
 * increase their Log-Level compared to their Ancestors, but they cannot decrease it.
 *
 *
The casual user does not have to deal with this class directly.

 *
 *
The structure of the logger hierarchy is maintained by the
 * getLogger method. The hierarchy is such that children link
 * to their parent but parents do not have any pointers to their
 * children. Moreover, loggers can be instantiated in any order, in
 * particular descendant before ancestor.

 *
 *
In case a descendant is created before a particular ancestor,
 * then it creates a provision node for the ancestor and adds itself
 * to the provision node. Other descendants of the same ancestor add
 * themselves to the previously created provision node.

 *
 * @version $Revision: 1394956 $
 * @package log4php
 */
class LoggerHierarchy {

	/** Array holding all Logger instances. */
	protected $loggers = array();

	/**
	 * The root logger.
	 * @var RootLogger
	 */
	protected $root;

	/**
	 * The logger renderer map.
	 * @var LoggerRendererMap
	 */
	protected $rendererMap;

	/**
	 * Main level threshold. Events with lower level will not be logged by any
	 * logger, regardless of it's configuration.
	 * @var LoggerLevel
	 */
	protected $threshold;

	/**
	 * Creates a new logger hierarchy.
	 * @param LoggerRoot $root The root logger.
	 */
	public function __construct(LoggerRoot $root) {
		$this->root = $root;
		$this->setThreshold(LoggerLevel::getLevelAll());
		$this->rendererMap = new LoggerRendererMap();
	}

	/**
	 * Clears all loggers.
	 */
	public function clear() {
		$this->loggers = array();
	}

	/**
	 * Check if the named logger exists in the hierarchy.
	 * @param string $name
	 * @return boolean
	 */
	public function exists($name) {
		return isset($this->loggers[$name]);
	}

	/**
	 * Returns all the currently defined loggers in this hierarchy as an array.
	 * @return array
	 */
	public function getCurrentLoggers() {
		return array_values($this->loggers);
	}

	/**
	 * Returns a named logger instance logger. If it doesn't exist, one is created.
	 *
	 * @param string $name Logger name
	 * @return Logger Logger instance.
	 */
	public function getLogger($name) {
		if(!isset($this->loggers[$name])) {
			$logger = new Logger($name);

			$nodes = explode('.', $name);
			$firstNode = array_shift($nodes);

			// if name is not a first node but another first node is their
			if($firstNode != $name and isset($this->loggers[$firstNode])) {
				$logger->setParent($this->loggers[$firstNode]);
			} else {
				// if there is no father, set root logger as father
				$logger->setParent($this->root);
			}

			// if there are more nodes than one
			if(count($nodes) > 0) {
				// find parent node
				foreach($nodes as $node) {
					$parentNode = "$firstNode.$node";
					if(isset($this->loggers[$parentNode]) and $parentNode != $name) {
						$logger->setParent($this->loggers[$parentNode]);
					}
					$firstNode .= ".$node";
				}
			}

			$this->loggers[$name] = $logger;
		}

		return $this->loggers[$name];
	}

	/**
	 * Returns the logger renderer map.
	 * @return LoggerRendererMap
	 */
	public function getRendererMap() {
		return $this->rendererMap;
	}

	/**
	 * Returns the root logger.
	 * @return LoggerRoot
	 */
	public function getRootLogger() {
		return $this->root;
	}

	/**
	 * Returns the main threshold level.
	 * @return LoggerLevel
	 */
	public function getThreshold() {
		return $this->threshold;
	}

	/**
	 * Returns true if the hierarchy is disabled for given log level and false
	 * otherwise.
	 * @return boolean
	 */
	public function isDisabled(LoggerLevel $level) {
		return ($this->threshold->toInt() > $level->toInt());
	}

	/**
	 * Reset all values contained in this hierarchy instance to their
	 * default.
	 *
	 * This removes all appenders from all loggers, sets
	 * the level of all non-root loggers to null,
	 * sets their additivity flag to true and sets the level
	 * of the root logger to {@link LOGGER_LEVEL_DEBUG}.
	 *
	 *
Existing loggers are not removed. They are just reset.
	 *
	 *
This method should be used sparingly and with care as it will
	 * block all logging until it is completed.

	 */
	public function resetConfiguration() {
		$root = $this->getRootLogger();

		$root->setLevel(LoggerLevel::getLevelDebug());
		$this->setThreshold(LoggerLevel::getLevelAll());
		$this->shutDown();

		foreach($this->loggers as $logger) {
			$logger->setLevel(null);
			$logger->setAdditivity(true);
			$logger->removeAllAppenders();
		}

		$this->rendererMap->reset();
		LoggerAppenderPool::clear();
	}

	/**
	 * Sets the main threshold level.
	 * @param LoggerLevel $l
	 */
	public function setThreshold(LoggerLevel $threshold) {
		$this->threshold = $threshold;
	}

	/**
	 * Shutting down a hierarchy will safely close and remove
	 * all appenders in all loggers including the root logger.
	 *
	 * The shutdown method is careful to close nested
	 * appenders before closing regular appenders. This is allows
	 * configurations where a regular appender is attached to a logger
	 * and again to a nested appender.
	 *
	 * @todo Check if the last paragraph is correct.
	 */
	public function shutdown() {
		$this->root->removeAllAppenders();

		foreach($this->loggers as $logger) {
			$logger->removeAllAppenders();
		}
	}

	/**
	 * Prints the current Logger hierarchy tree. Useful for debugging.
	 */
	public function printHierarchy() {
		$this->printHierarchyInner($this->getRootLogger(), 0);
	}

	private function printHierarchyInner(Logger $current, $level) {
		for ($i = 0; $i < $level; $i++) {
			echo ($i == $level - 1) ? "|--" : "|  ";
		}
		echo $current->getName() . "\n";

		foreach($this->loggers as $logger) {
			if ($logger->getParent() == $current) {
				$this->printHierarchyInner($logger, $level + 1);
			}
		}
	}
}