Current File : //opt/RZphp73/includes/XML/Query2XML.php
<?php
/**
 * This file contains the class XML_Query2XML and all exception classes.
 *
 * PHP version 5
 *
 * @category  XML
 * @package   XML_Query2XML
 * @author    Lukas Feiler <lukas.feiler@lukasfeiler.com>
 * @copyright 2006 Lukas Feiler
 * @license   http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
 * @version   CVS: $Id: Query2XML.php 309897 2011-04-02 17:36:42Z lukasfeiler $
 * @link      http://pear.php.net/package/XML_Query2XML
 */

/**
 * PEAR_Exception is used as the parent for XML_Query2XML_Exception.
 */
require_once 'PEAR/Exception.php';

/**
 * Create XML data from SQL queries.
 *
 * XML_Query2XML heavily uses exceptions and therefore requires PHP5.
 * Additionally one of the following database abstraction layers is
 * required: PDO (compiled-in by default since PHP 5.1), PEAR DB,
 * PEAR MDB2, ADOdb.
 *
 * The two most important public methods this class provides are:
 *
 * <b>{@link XML_Query2XML::getFlatXML()}</b>
 * Transforms your SQL query into flat XML data.
 *
 * <b>{@link XML_Query2XML::getXML()}</b>
 * Very powerful and flexible method that can produce whatever XML data you want. It
 * was specifically written to also handle LEFT JOINS.
 *
 * They both return an instance of the class DOMDocument provided by PHP5's
 * built-in DOM API.
 *
 * A typical usage of XML_Query2XML looks like this:
 * <code>
 * <?php
 * require_once 'XML/Query2XML.php';
 * $query2xml = XML_Query2XML::factory(MDB2::factory($dsn));
 * $dom = $query2xml->getXML($sql, $options);
 * header('Content-Type: application/xml');
 * print $dom->saveXML();
 * ?>
 * </code>
 *
 * Please read the <b>{@tutorial XML_Query2XML.pkg tutorial}</b> for
 * detailed usage examples and more documentation.
 * 
 * @category  XML
 * @package   XML_Query2XML
 * @author    Lukas Feiler <lukas.feiler@lukasfeiler.com>
 * @copyright 2006 Lukas Feiler
 * @license   http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
 * @version   Release: 1.7.2
 * @link      http://pear.php.net/package/XML_Query2XML
 */
class XML_Query2XML
{
    /**
     * Primary driver.
     * @var XML_Query2XML_Driver A subclass of XML_Query2XML_Driver.
     */
    private $_driver;
    
    /**
     * An instance of PEAR Log
     * @var mixed An object that has a method with the signature log(String $msg);
     *            preferably PEAR Log.
     * @see enableDebugLog
     * @see disableDebugLog
     */
    private $_debugLogger;
    
    /**
     * Whether debug logging is to be performed
     * @var boolean
     * @see enableDebugLog
     * @see disableDebugLog
     */
    private $_debug = false;
    
    /**
     * Whether profiling is to be performed
     * @var boolean
     * @see startProfiling()
     * @see stopProfiling()
     */
    private $_profiling = false;
    
    /**
     * The profiling data.
     * @var array A multi dimensional associative array
     * @see startProfiling()
     * @see stopProfiling()
     * @see _debugStartQuery()
     * @see _debugStopQuery()
     * @see _stopDBProfiling()
     */
    private $_profile = array();
    
    /**
     * An associative array of global options.
     * @var array An associative array
     * @see setGlobalOption()
     */
    private $_globalOptions = array(
        'hidden_container_prefix' => '__'
    );
    
    /**
     * An associative array that will contain an element for each prefix.
     * The prefix is used as the element key. Each array element consists
     * of an indexed array containing a file path and a class name.
     * @var array An associative multidimensional array.
     * @see registerPrefix()
     * @see unregisterPrefix()
     * @see unregisterAllPrefixes()
     * @see _buildCommandChain()
     */
    private $_prefixes = array(
        '?' => array(
            'XML/Query2XML/Data/Condition/NonEmpty.php',
            'XML_Query2XML_Data_Condition_NonEmpty'
        ),
        '&' => array(
            'XML/Query2XML/Data/Processor/Unserialize.php',
            'XML_Query2XML_Data_Processor_Unserialize'
        ),
        '=' => array(
            'XML/Query2XML/Data/Processor/CDATA.php',
            'XML_Query2XML_Data_Processor_CDATA'
        ),
        '^' => array(
            'XML/Query2XML/Data/Processor/Base64.php',
            'XML_Query2XML_Data_Processor_Base64'
        ),
        ':' => array(
            'XML/Query2XML/Data/Source/Static.php',
            'XML_Query2XML_Data_Source_Static'
        ),
        '#' => array(
            'XML/Query2XML/Data/Source/PHPCallback.php',
            'XML_Query2XML_Data_Source_PHPCallback'
        ),
        '~' => array(
            'XML/Query2XML/Data/Source/XPath.php',
            'XML_Query2XML_Data_Source_XPath'
        )
    );
    
    /**
     * Constructor
     *
     * @param mixed $backend A subclass of XML_Query2XML_Driver or
     *                       an instance of PEAR DB, PEAR MDB2, ADOdb,
     *                       PDO, Net_LDAP2 or Net_LDAP.
     */
    private function __construct($backend)
    {
        if ($backend instanceof XML_Query2XML_Driver) {
            $this->_driver = $backend;
        } else {
            $this->_driver = XML_Query2XML_Driver::factory($backend);
        }
    }
    
    /**
     * Factory method.
     * As first argument pass an instance of PDO, PEAR DB, PEAR MDB2, ADOdb,
     * Net_LDAP or an instance of any class that extends XML_Query2XML_Driver:
     * <code>
     * <?php
     * require_once 'XML/Query2XML.php';
     * $query2xml = XML_Query2XML::factory(
     *   new PDO('mysql://root@localhost/Query2XML_Tests')
     * );
     * ?>
     * </code>
     *
     * <code>
     * <?php
     * require_once 'XML/Query2XML.php';
     * require_once 'DB.php';
     * $query2xml = XML_Query2XML::factory(
     *   DB::connect('mysql://root@localhost/Query2XML_Tests')
     * );
     * ?>
     * </code>
     * 
     * <code>
     * <?php
     * require_once 'XML/Query2XML.php';
     * require_once 'MDB2.php';
     * $query2xml = XML_Query2XML::factory(
     *   MDB2::factory('mysql://root@localhost/Query2XML_Tests')
     * );
     * ?>
     * </code>
     * 
     * <code>
     * <?php
     * require_once 'XML/Query2XML.php';
     * require_once 'adodb/adodb.inc.php';
     * $adodb = ADONewConnection('mysql');
     * $adodb->Connect('localhost', 'root', '', 'Query2XML_Tests');
     * $query2xml = XML_Query2XML::factory($adodb);
     * ?>
     * </code>
     *
     * @param mixed $backend An instance of PEAR DB, PEAR MDB2, ADOdb, PDO,
     *                       Net_LDAP or a subclass of XML_Query2XML_Driver.
     *
     * @return XML_Query2XML A new instance of XML_Query2XML
     * @throws XML_Query2XML_DriverException If $backend already is a PEAR_Error.
     * @throws XML_Query2XML_ConfigException If $backend is not an instance of a
     *                  child class of DB_common, MDB2_Driver_Common, ADOConnection
     *                  PDO, Net_LDAP or XML_Query2XML_Driver.
     */
    public static function factory($backend)
    {
        return new XML_Query2XML($backend);
    }
    
    /**
     * Register a prefix that can be used in all value specifications.
     *
     * @param string $prefix    The prefix name. This must be a single chracter.
     * @param string $className The name of the Data Class. This class has
     *                          to extend XML_Query2XML_Data.
     * @param string $filePath  The path to the file that contains the Command
     *                          class. This argument is optional.
     *
     * @return void
     * @throws XML_Query2XML_ConfigException Thrown if $prefix is not a string
     *                                       or has a length other than 1.
     */
    public function registerPrefix($prefix, $className, $filePath = '')
    {
        if (!is_string($prefix) || strlen($prefix) != 1) {
            throw new XML_Query2XML_ConfigException(
                'Prefix name has to be a single character'
            );
        }
        $this->_prefixes[$prefix] = array(
            $filePath,
            $className
        );
    }
    
    /**
     * Unregister a prefix.
     *
     * @param string $prefix The prefix name.
     *
     * @return void
     */
    public function unregisterPrefix($prefix)
    {
        unset($this->_prefixes[$prefix]);
    }
    
    /**
     * Unregister all prefixes.
     *
     * @return void
     */
    public function unregisterAllPrefixes()
    {
        $this->_prefixes = array();
    }
    
    /**
     * Set a global option.
     * Currently the following global options are available:
     *
     * hidden_container_prefix: The prefix to use for container elements that are
     *   to be removed before the DOMDocument before it is returned by
     *   {@link XML_Query2XML::getXML()}. This has to be a non-empty string.
     *   The default value is '__'.
     *
     * @param string $option The name of the option
     * @param mixed  $value  The option value
     *
     * @return void
     * @throws XML_Query2XML_ConfigException If the configuration option
     *         does not exist or if the value is invalid for that option
     */
    public function setGlobalOption($option, $value)
    {
        switch ($option) {
        case 'hidden_container_prefix':
            if (is_string($value) && strlen($value) > 0) {
                // unit test: setGlobalOption/hidden_container_prefix.phpt
                $this->_globalOptions[$option] = $value;
            } else {
                /*
                 * unit test: setGlobalOption/
                 * configException_hidden_container_prefix_wrongTypeObject.phpt
                 * configException_hidden_container_prefix_wrongTypeEmptyStr.phpt
                 */
                throw new XML_Query2XML_ConfigException(
                    'The value for the hidden_container_prefix option '
                    . 'has to be a non-empty string'
                );
            }
            break;

        default:
            // unit tests: setGlobalOption/configException_noSuchOption.phpt
            throw new XML_Query2XML_ConfigException(
                'No such global option: ' . $option
            );
        }
    }
    
    /**
     * Returns the current value for a global option.
     * See {@link XML_Query2XML::setGlobalOption()} for a list
     * of available options.
     *
     * @param string $option The name of the option
     *
     * @return mixed The option's value
     * @throws XML_Query2XML_ConfigException If the option does not exist
     */
    public function getGlobalOption($option)
    {
        if (!isset($this->_globalOptions[$option])) {
            // unit test: getGlobalOption/configException_noSuchOption.phpt
            throw new XML_Query2XML_ConfigException(
                'No such global option: ' . $option
            );
        }
        // unit test: getGlobalOption/hidden_container_prefix.phpt
        return $this->_globalOptions[$option];
    }
    
    /**
     * Enable the logging of debug messages.
     * This will include all queries sent to the database.
     * Example:
     * <code>
     * <?php
     * require_once 'Log.php';
     * require_once 'XML/Query2XML.php';
     * $query2xml = XML_Query2XML::factory(MDB2::connect($dsn));
     * $debugLogger = Log::factory('file', 'out.log', 'XML_Query2XML');
     * $query2xml->enableDebugLog($debugLogger);
     * ?>
     * </code>
     * Please see {@link http://pear.php.net/package/Log} for details on PEAR Log.
     *
     * @param mixed $log Most likely an instance of PEAR Log but any object
     *                   that provides a method named 'log' is accepted.
     *
     * @return void
     */
    public function enableDebugLog($log)
    {
        // unit test: enableDebugLog/enableDebugLog.phpt
        $this->_debugLogger = $log;
        $this->_debug       = true;
    }
    
    /**
     * Disable the logging of debug messages
     *
     * @return void
     */
    public function disableDebugLog()
    {
        // unit test: disableDebugLog/disableDebugLog.phpt
        $this->_debug = false;
    }
    
    /**
     * Start profiling.
     *
     * @return void
     */
    public function startProfiling()
    {
        // unit tests: startProfile/startProfile.phpt
        $this->_profiling = true;
        $this->_profile   = array(
            'queries'    => array(),
            'start'      => microtime(1),
            'stop'       => 0,
            'duration'   => 0,
            'dbStop'     => 0,
            'dbDuration' => 0
        );
    }
    
    /**
     * Stop profiling.
     *
     * @return void
     */
    public function stopProfiling()
    {
        // unit test: stopProfile/stopProfile.phpt
        $this->_profiling = false;
        if (isset($this->_profile['start']) && $this->_profile['stop'] == 0) {
            $this->_profile['stop']     = microtime(1);
            $this->_profile['duration'] =
                $this->_profile['stop'] - $this->_profile['start'];
        }
    }
    
    /**
     * Returns all raw profiling data.
     * In 99.9% of all cases you will want to use getProfile().
     *
     * @see getProfile()
     * @return array
     */
    public function getRawProfile()
    {
        // unit test: getRawProfile/getRawProfile.phpt
        $this->stopProfiling();
        return $this->_profile;
    }
    
    /**
     * Returns the profile as a single multi line string.
     *
     * @return string The profiling data.
     */
    public function getProfile()
    {
        // unit test: getProfile/getProfile.phpt
        $this->stopProfiling();
        if (count($this->_profile) === 0) {
            return '';
        }
        $ret = 'COUNT AVG_DURATION DURATION_SUM SQL' . "\n";
        foreach ($this->_profile['queries'] as $sql => $value) {
            $durationSum   = 0.0;
            $durationCount = 0;
            $runTimes      =& $this->_profile['queries'][$sql]['runTimes'];
            foreach ($runTimes as $runTime) {
                $durationSum += ($runTime['stop'] - $runTime['start']);
                ++$durationCount;
            }
            if ($durationCount == 0) {
                // so that division does not fail
                $durationCount = 1;
            }
            $durationAverage = $durationSum / $durationCount;
            
            $ret .= str_pad($this->_profile['queries'][$sql]['count'], 5)
                  . ' '
                  . substr($durationAverage, 0, 12). ' '
                  . substr($durationSum, 0, 12). ' '
                  . $sql . "\n";
        }
        $ret .= "\n";
        $ret .= 'TOTAL_DURATION: ' . $this->_profile['duration'] . "\n";
        $ret .= 'DB_DURATION:    ' . $this->_profile['dbDuration'] . "\n";
        return $ret;
    }
    
    /**
     * Calls {@link XML_Query2XML::stopProfiling()} and then clears the profiling
     * data by resetting a private property.
     *
     * @return void
     */
    public function clearProfile()
    {
        // unit test: clearProfile/clearProfile.phpt
        $this->stopProfiling();
        $this->_profile = array();
    }
    
    /**
     * Transforms the data retrieved by a single SQL query into flat XML data.
     *
     * This method will return a new instance of DOMDocument. The column names
     * will be used as element names.
     *
     * Example:
     * <code>
     * <?php
     * require_once 'XML/Query2XML.php';
     * $query2xml = XML_Query2XML::factory(MDB2::connect($dsn));
     * $dom = $query2xml->getFlatXML(
     *   'SELECT * FROM artist',
     *   'music_library',
     *   'artist'
     * );
     * ?>
     * </code>
     *
     * @param string $sql         The query string.
     * @param string $rootTagName The name of the root tag; this argument is optional
     *                            (default: 'root').
     * @param string $rowTagName  The name of the tag used for each row; this
     *                            argument is optional (default: 'row').
     *
     * @return DOMDocument        A new instance of DOMDocument.
     * @throws XML_Query2XML_Exception This is the base class for the exception
     *                            types XML_Query2XML_DBException and
     *                            XML_Query2XML_XMLException. By catching
     *                            XML_Query2XML_Exception you can catch all
     *                            exceptions this method will ever throw.
     * @throws XML_Query2XML_DBException If a database error occurrs.
     * @throws XML_Query2XML_XMLException If an XML error occurrs - most likely
     *                            $rootTagName or $rowTagName is not a valid
     *                            element name.
     */
    public function getFlatXML($sql, $rootTagName = 'root', $rowTagName = 'row')
    {
        /*
         * unit tests: getFlatXML/*.phpt
         */
        $dom     = self::_createDOMDocument();
        $rootTag = self::_addNewDOMChild($dom, $rootTagName, 'getFlatXML');
        $records = $this->_getAllRecords(array('query' => $sql), 'getFlatXML', $sql);
        foreach ($records as $record) {
            $rowTag = self::_addNewDOMChild($rootTag, $rowTagName, 'getFlatXML');
            foreach ($record as $field => $value) {
                self::_addNewDOMChild(
                    $rowTag,
                    $field,
                    'getFlatXML',
                    self::_utf8encode($value)
                );
            }
        }
        return $dom;
    }
    
    /**
     * Transforms your SQL data retrieved by one or more queries into complex and
     * highly configurable XML data.
     *
     * This method will return a new instance of DOMDocument.
     * Please see the <b>{@tutorial XML_Query2XML.pkg tutorial}</b> for details.
     * 
     * @param mixed $sql     A string an array or the boolean value false.
     * @param array $options Options for the creation of the XML data stored in an
     *                       associative, potentially mutli-dimensional array
     *                       (please see the tutorial).
     *
     * @return DOMDocument   The XML data as a new instance of DOMDocument.
     * @throws XML_Query2XML_Exception This is the base class for the exception types
     *                       XML_Query2XML_DBException, XML_Query2XML_XMLException
     *                       and XML_Query2XML_ConfigException. By catching
     *                       XML_Query2XML_Exception you can catch all exceptions
     *                       this method will ever throw.
     * @throws XML_Query2XML_DBException If a database error occurrs.
     * @throws XML_Query2XML_XMLException If an XML error occurrs - most likely
     *                       an invalid XML element name.
     * @throws XML_Query2XML_ConfigException If some configuration options passed
     *                       as second argument are invalid or missing.
     */
    public function getXML($sql, $options)
    {
        /*
        * unit tests: getXML/*.phpt
        */
        
        // the default root tag name is 'root'
        if (isset($options['rootTag'])) {
            $rootTagName = $options['rootTag'];
        } else {
            $rootTagName = 'root';
        }
        
        $dom     = self::_createDOMDocument();
        $rootTag = self::_addNewDOMChild($dom, $rootTagName, '[rootTag]');
        
        $options['sql'] = $sql;
        
        if ($options['sql'] === false) {
            $options['sql'] = '';
        }
        $this->_preprocessOptions($options);
        
        /* Used to store the information which element has been created
        *  for which ID column value.
        */
        $tree = array();
        
        if ($sql === false) {
            $records = array(array()); // one empty record
        } else {
            $records = $this->_applySqlOptionsToRecord(
                $options,
                $emptyRecord = array()
            );
        }
        
        foreach ($records as $key => $record) {
            $tag = $this->_getNestedXMLRecord($records[$key], $options, $dom, $tree);
            
            /* _getNestedXMLRecord() returns false if an element already existed for
            *  the current ID column value.
            */
            if ($tag !== false) {
                $rootTag->appendChild($tag);
            }
        }
        
        $this->_stopDBProfiling();
        
        self::_removeContainers(
            $dom,
            $this->getGlobalOption('hidden_container_prefix')
        );
        return $dom;
    }
    
    /**
     * Perform pre-processing on $options.
     * This is a recursive method; it will call itself for every complex element
     * specification and every complex attribute specification found.
     *
     * @param array  &$options An associative array
     * @param string $context  Indecates whether an element or an attribute is
     *                         to be processed.
     *
     * @return void
     * @throws XML_Query2XML_ConfigException If a mandatory option is missing
     *                       or any option is defined incorrectly.
     */
    private function _preprocessOptions(&$options, $context = 'elements')
    {
        if (!isset($options['--q2x--path'])) {
            // things to do only at the root level
            $options['--q2x--path'] = '';
            
            if (!isset($options['rowTag'])) {
                $options['rowTag'] = 'row';
            }
            
            if (!isset($options['idColumn'])) {
                /*
                * unit test: _preprocessOptions/
                *  throwConfigException_idcolumnOptionMissing.phpt
                */
                throw new XML_Query2XML_ConfigException(
                    'The configuration option "idColumn" '
                    . 'is missing on the root level.'
                );
            }
        }
        
        foreach (array('encoder', 'mapper') as $option) {
            if (isset($options[$option])) {
                if (
                    is_string($options[$option]) &&
                    strpos($options[$option], '::') !== false
                ) {
                    $options[$option] = explode('::', $options[$option]);
                }
                if (
                    $options[$option] !== false
                    &&
                    !($option == 'encoder' && $options[$option] === null)
                    &&
                    !($option == 'mapper' && $options[$option] == false)
                    &&
                    !is_callable($options[$option], false, $callableName)
                ) {
                    /*
                    * Only check whether $options['encoder'] is callable if it's not
                    * set to:
                    * - false (don't use an encoder)
                    * - null (use self::_utf8encode()).
                    *
                    * unit test: _preprocessOptions/
                    *  throwConfigException_encoderNotCallableStaticMethod1.phpt
                    *  throwConfigException_encoderNotCallableStaticMethod2.phpt
                    *  throwConfigException_encoderNotCallableNonstaticMethod.phpt
                    *  throwConfigException_encoderNotCallableFunction.phpt
                    *
                    *
                    * Only check whether $options['mapper'] is callable if
                    * - $options['mapper'] != false
                    *
                    * unit tests: _preprocessOptions/
                    *  throwConfigException_mapperNotCallableStaticMethod1.phpt
                    *  throwConfigException_mapperNotCallableStaticMethod2.phpt
                    *  throwConfigException_mapperNotCallableNonstaticMethod.phpt
                    *  throwConfigException_mapperNotCallableFunction.phpt
                    */
                    throw new XML_Query2XML_ConfigException(
                        $options['--q2x--path'] . '[' . $option . ']: The '
                        . 'method/function "' . $callableName . '" is not callable.'
                    );
                }
            } else {
                $options[$option] = null;
            }
        }
        
        if ($context == 'elements') {
            foreach (array('elements', 'attributes') as $option) {
                if (isset($options[$option])) {
                    if (!is_array($options[$option])) {
                        /*
                        * unit test: _preprocessOptions/
                        *  throwConfigException_attributesOptionWrongType.phpt
                        *  throwConfigException_elementsOptionWrongType.phpt
                        */
                        throw new XML_Query2XML_ConfigException(
                            $options['--q2x--path'] . '[' . $option . ']: '
                            . 'array expected, ' . gettype($options[$option])
                            . ' given.'
                        );
                    }
                    foreach ($options[$option] as $key => $columnStr) {
                        $configPath = $options['--q2x--path'] . '[' . $option
                                      . '][' . $key . ']';
                        if (is_string($columnStr)) {
                            $options[$option][$key] =
                                $this->_buildCommandChain($columnStr, $configPath);
                            if (
                                is_numeric($key) &&
                                is_object($options[$option][$key])
                            ) {
                                /*
                                 * unit test: _preprocessOptions/
                                 *  throwConfigException_prefix_noArrayKey.phpt
                                 */
                                throw new XML_Query2XML_ConfigException(
                                    $configPath . ': the element name has to be '
                                    . 'specified as the array key when prefixes '
                                    . 'are used within the value specification'
                                );
                            }
                        } elseif (is_array($columnStr)) {
                            $options[$option][$key]['--q2x--path'] = $configPath;
                            
                            // encoder option used by elements as well as attributes
                            if (
                                !array_key_exists(
                                    'encoder',
                                    $options[$option][$key]
                                 )
                            ) {
                                $options[$option][$key]['encoder'] =
                                    $options['encoder'];
                            }
                            if ($option == 'elements') {
                                // these options are only used by elements
                                if (
                                    !isset($options[$option][$key]['rootTag']) ||
                                    $options[$option][$key]['rootTag'] == ''
                                ) {
                                    /*
                                     * If rootTag is not set or an empty string:
                                     * create a hidden root tag
                                     */
                                    $options[$option][$key]['rootTag'] = 
                                        $this->getGlobalOption(
                                            'hidden_container_prefix'
                                        ) . $key;
                                }
                                if (!isset($options[$option][$key]['rowTag'])) {
                                    $options[$option][$key]['rowTag'] = $key;
                                }
        
                                foreach (array('mapper', 'idColumn') as $option2) {
                                    if (
                                        !array_key_exists(
                                            $option2,
                                            $options[$option][$key]
                                        )
                                    ) {
                                        $options[$option][$key][$option2] =
                                            $options[$option2];
                                    }
                                }
                            }
                            $this->_preprocessOptions(
                                $options[$option][$key],
                                $option
                            );
                        } elseif (self::_isCallback($columnStr)) {
                            if (is_numeric($key)) {
                                /*
                                 * unit test: _preprocessOptions/
                                 *  throwConfigException_callbackInterface_
                                 *  noArrayKey.phpt
                                 */
                                throw new XML_Query2XML_ConfigException(
                                    $configPath . ': the element name has to be '
                                    . 'specified as the array key when the value '
                                    . 'is specified using an instance of '
                                    . 'XML_Query2XML_Callback.'
                                );
                            }
                        } else {
                            /*
                             * $columnStr is neither a string, an array or an
                             * instance of XML_Query2XML_Callback.
                             *
                             * unit tests:
                             *  _getNestedXMLRecord/
                             *   throwConfigException_attributeSpecWrongType.phpt
                             *  _preprocessOptions/
                             *   throwConfigException_callbackInterface_
                             *    complexAttributeSpec.phpt
                             *    simpleAttributeSpec.phpt
                             *    simpleElementSpec.phpt
                             */
                            throw new XML_Query2XML_ConfigException(
                                $configPath . ': array, string or instance of'
                                . ' XML_Query2XML_Callback expected, '
                                . gettype($columnStr)
                                . ' given.'
                            );
                        }
                    }
                }
            } // end of foreach (array('elements', 'attributes'))
        } else {
            // $context == 'attributes'
            if (!isset($options['value'])) {
                /*
                * the option "value" is mandatory
                * unit test: _preprocessOptions/
                *  throwConfigException_valueOptionMissing.phpt
                */
                throw new XML_Query2XML_ConfigException(
                    $options['--q2x--path'] . '[value]: Mandatory option "value" '
                    . 'missing from the complex attribute specification.'
                );
            }
        }
        
        $opt = array('value', 'condition', 'dynamicRowTag', 'idColumn');
        foreach ($opt as $option) {
            if (isset($options[$option])) {
                if (is_string($options[$option])) {
                    $options[$option] = $this->_buildCommandChain(
                        $options[$option],
                        $options['--q2x--path'] . '[value]'
                    );
                } elseif (
                    !self::_isCallback($options[$option]) &&
                    !($option == 'idColumn' && $options[$option] === false)
                ) {
                    /*
                    * unit tests:
                    *  _preprocessOptions/
                    *   throwConfigException_callbackInterface_
                    *    complexElementSpec.phpt
                    *    condition.phpt
                    *    idColumn.phpt
                    */
                    throw new XML_Query2XML_ConfigException(
                        $options['--q2x--path'] . '[' . $option . ']: string or'
                        . ' instance of XML_Query2XML_Callback expected, '
                        . gettype($options[$option])
                        . ' given.'
                    );
                }
            }
        }
        
        if (isset($options['query'])) {
            $options['sql'] = $options['query'];
        }
        if (isset($options['sql'])) {
            
            // we will pre-process $options['sql_options'] first
            if (isset($options['query_options'])) {
                $options['sql_options'] = $options['query_options'];
            }
            if (!isset($options['sql_options'])) {
                $options['sql_options'] = array();
            }
            $sql_options = array(
                'cached', 'single_record', 'merge', 'merge_master', 'merge_selective'
            );
            foreach ($sql_options as $option) {
                if (!isset($options['sql_options'][$option])) {
                    $options['sql_options'][$option] = false;
                }
            }
            if (isset($options['sql_options']['uncached'])) {
                $options['sql_options']['cached'] =
                    !$options['sql_options']['uncached'];
            }
            
            if ($options['sql_options']['cached']) {
                if (!is_array($options['sql'])) {
                    $options['sql'] = array('query' => $options['sql']);
                }
                if (isset($options['sql']['driver'])) {
                    $driver = $options['sql']['driver'];
                } else {
                    $driver = $this->_driver;
                }
                if (
                    !class_exists('XML_Query2XML_Driver_Caching') ||
                    !($driver instanceof XML_Query2XML_Driver_Caching)
                ) {
                    include_once 'XML/Query2XML/Driver/Caching.php';
                    $options['sql']['driver'] = new XML_Query2XML_Driver_Caching(
                        $driver
                    );
                }
            }
            
            if (
                $options['sql_options']['merge_selective'] !== false &&
                !is_array($options['sql_options']['merge_selective'])
            ) {
                /*
                * unit test: _preprocessOptions/
                *  throwConfigException_mergeselectiveOptionWrongType.phpt
                */
                throw new XML_Query2XML_ConfigException(
                    $options['--q2x--path'] . '[sql_options][merge_selective]: '
                    . 'array expected, '
                    . gettype($options['sql_options']['merge_selective']) . ' given.'
                );
            }
            // end of pre-processing of $options['sql_options']
            
            if (
                is_array($options['sql']) && 
                isset($options['sql']['driver']) &&
                $options['sql']['driver'] instanceof XML_Query2XML_Driver
            ) {
                $query = $options['sql']['driver']->preprocessQuery(
                    $options['sql'],
                    $options['--q2x--path'] . '[sql]'
                );
            } else {
                $query = $this->_driver->preprocessQuery(
                    $options['sql'],
                    $options['--q2x--path'] . '[sql]'
                );
            }
            $options['--q2x--query_statement'] = $query;
            if (
                is_array($options['sql']) && 
                isset($options['sql']['driver']) &&
                !($options['sql']['driver'] instanceof XML_Query2XML_Driver)
            ) {
                /*
                 * unit test: _preprocessOptions
                 *  throwConfigException_sqlOptionWrongType.phpt
                 */
                throw new XML_Query2XML_ConfigException(
                    $options['--q2x--path'] . '[sql][driver]: '
                    . 'instance of XML_Query2XML_Driver expected, '
                    . gettype($options['sql']['driver']) . ' given.'
                );
            }
            
            if (is_array($options['sql'])) {
                if (isset($options['sql']['data'])) {
                    if (is_array($options['sql']['data'])) {
                        foreach ($options['sql']['data'] as $key => $data) {
                            if (is_string($data)) {
                                $options['sql']['data'][$key] =
                                    $this->_buildCommandChain(
                                        $options['sql']['data'][$key],
                                        $options['--q2x--path']
                                            . '[sql][data][' . $key . ']'
                                    );
                            } elseif (!self::_isCallback($data)) {
                                /*
                                * unit tests: _preprocessOptions/
                                *   throwConfigException_callbackInterface_data.phpt
                                */
                                throw new XML_Query2XML_ConfigException(
                                    $options['--q2x--path'] . '[sql][data][' . $key
                                    . ']: string or'
                                    . ' instance of XML_Query2XML_Callback expected,'
                                    . ' ' . gettype($options['sql']['data'][$key])
                                    . ' given.'
                                );
                            }
                        }
                    } else {
                        /*
                        * unit test: _preprocessOptions/
                        *  throwConfigException_dataOptionWrongType.phpt
                        */
                        throw new XML_Query2XML_ConfigException(
                            $options['--q2x--path'] . '[sql][data]: array expected, '
                            . gettype($options['sql']['data']) . ' given.'
                        );
                    }
                }
            }
        } // end of if (isset($options['sql'])
    }
    
    /**
     * Private recursive method that creates the nested XML elements from a record.
     *
     * getXML calls this method for every row in the initial result set.
     * The $tree argument deserves some more explanation. All DOMNodes are stored
     * in $tree the way they appear in the XML document. The same hirachy needs to be
     * built so that we can know if a DOMNode that corresponds to a column ID of 2 is
     * already a child node of a certain XML element. Let's have a look at an example
     * to clarify this:
     * <code>
     * <music_library>
     *   <artist>
     *     <artistid>1</artistid>
     *     <albums>
     *       <album>
     *         <albumid>1</albumid>
     *       </album>
     *       <album>
     *         <albumid>2</albumid>
     *       </album>
     *     </albums>
     *   </artist>
     *   <artist>
     *     <artistid>3</artistid>
     *     <albums />
     *   </artist>
     * </music_library>
     * </code>
     * would be represended in the $tree array as something like this:
     * <code>
     * array (
     *   [1] => array (
     *     [tag] => DOMElement Object
     *     [elements] => array (
     *       [albums] => array (
     *         [1] => array (
     *           [tag] => DOMElement Object
     *         )
     *         [2] => array (
     *           [tag] => DOMElement Object
     *         )
     *       )
     *     )
     *   )
     *   [2] => array (
     *     [tag] => DOMElement Object
     *     [elements] => array
     *     (
     *       [albums] => array ()
     *     )
     *   )
     * )
     * </code>
     * The numbers in the square brackets are column ID values.
     *
     * @param array       $record   An associative array representing a record;
     *                              column names must be used as keys.
     * @param array       &$options An array containing the options for this nested 
     *                              element; this will be a subset of the array
     *                              originally passed to getXML().
     * @param DOMDocument $dom      An instance of DOMDocument.
     * @param array       &$tree    An associative multi-dimensional array, that is
     *                              used to store the information which tag has
     *                              already been created for a certain ID column
     *                              value. It's format is:
     *                              Array(
     *                                "$id1" => Array(
     *                                  'tag' => DOMElement,
     *                                  'elements' => Array(
     *                                    "$id2" => Array(
     *                                      'tag' => DOMElement,
     *                                      'elements' => Array( ... )
     *                                    ),
     *                                    "$id3" => ...
     *                                  )
     *                                )
     *                              )
     *
     * @return mixed           The XML element's representation as a new instance of
     *                         DOMNode or the boolean value false (meaning no
     *                         new tag was created).
     * @throws XML_Query2XML_DBException  Bubbles up through this method if thrown by
     *                         - _processComplexElementSpecification()
     * @throws XML_Query2XML_XMLException Bubbles up through this method if thrown by
     *                         - _createDOMElement()
     *                         - _setDOMAttribute
     *                         - _appendTextChildNode()
     *                         - _addNewDOMChild()
     *                         - _addDOMChildren()
     *                         - _processComplexElementSpecification()
     *                         - _expandShortcuts()
     *                         - _executeEncoder()
     * @throws XML_Query2XML_ConfigException  Thrown if
     *                         - $options['idColumn'] is not set
     *                         - $options['elements'] is set but not an array
     *                         - $options['attributes'] is set but not an array
     *                         Bubbles up through this method if thrown by
     *                         - _applyColumnStringToRecord()
     *                         - _processComplexElementSpecification()
     *                         - _expandShortcuts()
     * @throws XML_Query2XML_Exception  Bubbles up through this method if thrown by
     *                         - _expandShortcuts()
     *                         - _applyColumnStringToRecord()
     */
    private function _getNestedXMLRecord($record, &$options, $dom, &$tree)
    {
        // the default row tag name is 'row'
        if (isset($options['dynamicRowTag'])) {
            $rowTagName = $this->_applyColumnStringToRecord(
                $options['dynamicRowTag'],
                $record,
                $options['--q2x--path'] . '[dynamicRowTag]'
            );
        } else {
            $rowTagName = $options['rowTag'];
        }
        
        if ($options['idColumn'] === false) {
            static $uniqueIdCounter = 0;
            $id = ++$uniqueIdCounter;
        } else {
            $id = $this->_applyColumnStringToRecord(
                $options['idColumn'],
                $record,
                $options['--q2x--path'] . '[idColumn]'
            );
        
            if ($id === null) {
                // the ID column is NULL
                return false;
            } elseif (is_object($id) || is_array($id)) {
                /*
                * unit test: _getNestedXMLRecord/
                *   throwConfigException_idcolumnOptionWrongTypeArray.phpt
                *   throwConfigException_idcolumnOptionWrongTypeObject.phpt
                */
                throw new XML_Query2XML_ConfigException(
                    $options['--q2x--path'] . '[idColumn]: Must evaluate to a '
                    . 'value that is not an object or an array.'
                );
            }
        }
        
        /* Is there already an identical tag (identity being determined by the
        *  value of the ID-column)?
        */
        if (isset($tree[$id])) {
            if (isset($options['elements'])) {
                foreach ($options['elements'] as $tagName => $column) {
                    if (is_array($column)) {
                        $this->_processComplexElementSpecification(
                            $record,
                            $options['elements'][$tagName],
                            $tree[$id],
                            $tagName
                        );
                    }
                }
            }
            /*
            * We return false because $tree[$id]['tag'] is already
            * a child of the parent element.
            */
            return false;
        } else {
            $tree[$id] = array();
            
            if (isset($options['value'])) {
                $parsedValue = $this->_applyColumnStringToRecord(
                    $options['value'],
                    $record,
                    $options['--q2x--path'] . '[value]'
                );
                if (!$this->_evaluateCondition($parsedValue, $options['value'])) {
                    // this element is to be skipped
                    return false;
                }
            }
            if (isset($options['condition'])) {
                $continue = $this->_applyColumnStringToRecord(
                    $options['condition'],
                    $record,
                    $options['--q2x--path'] . '[condition]'
                );
                if (!$continue) {
                    // this element is to be skipped
                    return false;
                }
            }
            $tree[$id]['tag'] = self::_createDOMElement(
                $dom,
                $rowTagName,
                $options['--q2x--path'] . '[rowTag/dynamicRowTag]'
            );

            $tag = $tree[$id]['tag'];
            
            // add attributes
            if (isset($options['attributes'])) {
                if (!isset($options['processed'])) {
                    $options['attributes'] = self::_expandShortcuts(
                        $options['attributes'],
                        $record,
                        $options['mapper'],
                        $options['--q2x--path'] . '[attributes]'
                    );
                }
                foreach ($options['attributes'] as $attributeName => $column) {
                    if (is_array($column)) {
                        // complex attribute specification
                        $this->_processComplexAttributeSpecification(
                            $attributeName, $record, $column, $tree[$id]['tag']
                        );
                    } else {
                        // simple attribute specifications
                        $attributeValue = $this->_applyColumnStringToRecord(
                            $column,
                            $record,
                            $options['--q2x--path']
                            . '[attributes][' . $attributeName . ']'
                        );
                        if ($this->_evaluateCondition($attributeValue, $column)) {
                            self::_setDOMAttribute(
                                $tree[$id]['tag'],
                                $attributeName,
                                self::_executeEncoder(
                                    $attributeValue,
                                    $options
                                ),
                                $options['--q2x--path']
                                . '[attributes][' . $attributeName . ']'
                            );
                        }
                    }
                }
            }
            if (isset($options['value'])) {
                if ($parsedValue instanceof DOMNode || is_array($parsedValue)) {
                    /*
                    * The value returned from _applyColumnStringToRecord() and
                    * stored in $parsedValue is an instance of DOMNode or an
                    * array of DOMNode instances. _addDOMChildren() will handle
                    * both.
                    */
                    self::_addDOMChildren(
                        $tree[$id]['tag'],
                        $parsedValue,
                        $options['--q2x--path'] . '[value]',
                        true
                    );
                } else {
                    if ($parsedValue !== false && !is_null($parsedValue)) {
                        self::_appendTextChildNode(
                            $tree[$id]['tag'],
                            self::_executeEncoder(
                                $parsedValue,
                                $options
                            ),
                            $options['--q2x--path'] . '[value]'
                        );
                    }
                }
            }
            
            // add child elements
            if (isset($options['elements'])) {
                if (!isset($options['processed'])) {
                    $options['elements'] = self::_expandShortcuts(
                        $options['elements'],
                        $record,
                        $options['mapper'],
                        $options['--q2x--path'] . '[elements]'
                    );
                }
                foreach ($options['elements'] as $tagName => $column) {
                    if (is_array($column)) {
                        // complex element specification
                        $this->_processComplexElementSpecification(
                            $record,
                            $options['elements'][$tagName],
                            $tree[$id],
                            $tagName
                        );
                    } else {
                        // simple element specification
                        $tagValue = $this->_applyColumnStringToRecord(
                            $column,
                            $record,
                            $options['--q2x--path'] . '[elements][' . $tagName . ']'
                        );
                        if ($this->_evaluateCondition($tagValue, $column)) {
                            if (
                                $tagValue instanceof DOMNode ||
                                is_array($tagValue)
                            ) {
                                /*
                                * The value returned from
                                * _applyColumnStringToRecord() and stored in
                                * $tagValue is an instance of DOMNode or an array
                                * of DOMNode instances. self::_addDOMChildren()
                                * will handle both.
                                */
                                self::_addDOMChildren(
                                    self::_addNewDOMChild(
                                        $tree[$id]['tag'],
                                        $tagName,
                                        $options['--q2x--path']
                                        . '[elements][' . $tagName . ']'
                                    ),
                                    $tagValue,
                                    $options['--q2x--path']
                                    . '[elements][' . $tagName . ']',
                                    true
                                );
                            } else {
                                self::_addNewDOMChild(
                                    $tree[$id]['tag'],
                                    $tagName,
                                    $options['--q2x--path']
                                    . '[elements][' . $tagName . ']',
                                    self::_executeEncoder(
                                        $tagValue,
                                        $options
                                    )
                                );
                            }
                        }
                    }
                }
            }
            
            // some things only need to be done once
            $options['processed'] = true;
            
            /*
            *  We return $tree[$id]['tag'] because it needs to be added to it's
            *  parent; this is to be handled by the method that called
            *  _getNestedXMLRecord().
            */
            return $tree[$id]['tag'];
        }
    }
    
    /**
     * Private method that will expand asterisk characters in an array
     * of simple element specifications.
     *
     * This method gets called to handle arrays specified using the 'elements'
     * or the 'attributes' option. An element specification that contains an
     * asterisk will be duplicated for each column present in $record.
     * Please see the {@tutorial XML_Query2XML.pkg tutorial} for details.
     *
     * @param Array  &$elements  An array of simple element specifications.
     * @param Array  &$record    An associative array that represents a single
     *                           record.
     * @param mixed  $mapper     A valid argument for call_user_func(), a full method
     *                           method name (e.g. "MyMapperClass::map") or a value
     *                           that == false for no special mapping at all.
     * @param string $configPath The config path; used for exception messages.
     *
     * @return Array The extended array.
     * @throws XML_Query2XML_ConfigException If only the column part but not the
     *                        explicitly defined tagName part contains an asterisk.
     * @throws XML_Query2XML_Exception Will bubble up if it is thrown by
     *                        _mapSQLIdentifierToXMLName(). This should never
     *                        happen as _getNestedXMLRecord() already checks if
     *                        $mapper is callable.
     * @throws XML_Query2XML_XMLException Will bubble up if it is thrown by
     *                        _mapSQLIdentifierToXMLName() which will happen if the
     *                        $mapper function called, throws any exception.
     */
    private function _expandShortcuts(&$elements, &$record, $mapper, $configPath)
    {
        $newElements = array();
        foreach ($elements as $tagName => $column) {
            if (is_numeric($tagName)) {
                $tagName = $column;
            }
            if (!is_array($column) && strpos($tagName, '*') !== false) {
                // expand all occurences of '*' to all column names
                foreach ($record as $columnName => $value) {
                    $newTagName = str_replace('*', $columnName, $tagName);
                    if (is_string($column)) {
                        $newColumn = str_replace('*', $columnName, $column);
                    } elseif (
                        class_exists('XML_Query2XML_Data') &&
                        $column instanceof XML_Query2XML_Data
                    ) {
                        $newColumn = clone $column;
                        $callback  = $newColumn->getFirstPreProcessor();
                        if (
                            class_exists('XML_Query2XML_Data_Source') &&
                            $callback instanceof XML_Query2XML_Data_Source
                        ) {
                            $callback->replaceAsterisks($columnName);
                        }
                    } else {
                        $newColumn =& $column;
                    }
                    // do the mapping
                    $newTagName = self::_mapSQLIdentifierToXMLName(
                        $newTagName,
                        $mapper,
                        $configPath . '[' . $tagName . ']'
                    );
                    if (!isset($newElements[$newTagName])) {
                        // only if the tagName hasn't already been used
                        $newElements[$newTagName] = $newColumn;
                    }
                }
            } else {
                /*
                * Complex element specifications will always be dealt with here.
                * We don't want any mapping or handling of the asterisk shortcut
                * to be done for complex element specifications.
                */
            
                if (!is_array($column)) {
                    // do the mapping but not for complex element specifications
                    $tagName = self::_mapSQLIdentifierToXMLName(
                        $tagName,
                        $mapper,
                        $configPath . '[' . $tagName . ']'
                    );
                }
                    
                /*
                 * explicit specification without an asterisk;
                 * this always overrules an expanded asterisk
                 */
                unset($newElements[$tagName]);
                $newElements[$tagName] = $column;
            }
        }
        return $newElements;
    }
    
    /**
     * Maps an SQL identifier to an XML name using the supplied $mapper.
     *
     * @param string $sqlIdentifier The SQL identifier as a string.
     * @param mixed  $mapper        A valid argument for call_user_func(), a full
     *                              method method name (e.g. "MyMapperClass::map")
     *                              or a value that == false for no special mapping
     *                              at all.
     * @param string $configPath    The config path; used for exception messages.
     *
     * @return string The mapped XML name.
     * @throws XML_Query2XML_Exception If $mapper is not callable. This should never
     *                              happen as _getNestedXMLRecord() already checks
     *                              if $mapper is callable.
     * @throws XML_Query2XML_XMLException If the $mapper function called, throws any
     *                              exception.
     */
    private function _mapSQLIdentifierToXMLName($sqlIdentifier, $mapper, $configPath)
    {
        if (!$mapper) {
            // no mapper was defined
            $xmlName = $sqlIdentifier;
        } else {
            if (is_callable($mapper, false, $callableName)) {
                try {
                    $xmlName = call_user_func($mapper, $sqlIdentifier);
                } catch (Exception $e) {
                    /*
                    * This will also catch XML_Query2XML_ISO9075Mapper_Exception
                    * if $mapper was "XML_Query2XML_ISO9075Mapper::map".
                    * unit test:
                    *  _mapSQLIdentifierToXMLName/throwXMLException.phpt
                    */
                    throw new XML_Query2XML_XMLException(
                        $configPath . ': Could not map "' . $sqlIdentifier
                        . '" to an XML name using the mapper '
                        . $callableName . ': ' . $e->getMessage()
                    );
                }
            } else {
                /*
                * This should never happen as _preprocessOptions() already
                * checks if $mapper is callable. Therefore no unit tests
                * can be provided for this exception.
                */
                throw new XML_Query2XML_ConfigException(
                    $configPath . ': The mapper "' . $callableName
                    . '" is not callable.'
                );
            }
        }
        return $xmlName;
    }
    
    /**
     * Private method that processes a complex element specification
     * for {@link XML_Query2XML::_getNestedXMLRecord()}.
     *
     * @param array  &$record  The current record.
     * @param array  &$options The current options.
     * @param array  &$tree    Associative multi-dimensional array, that is used to
     *                         store which tags have already been created
     * @param string $tagName  The element's name.
     *
     * @return void
     * @throws XML_Query2XML_XMLException This exception will bubble up
     *                        if it is thrown by _getNestedXMLRecord(),
     *                        _applySqlOptionsToRecord() or _addDOMChildren().
     * @throws XML_Query2XML_DBException  This exception will bubble up
     *                        if it is thrown by _applySqlOptionsToRecord()
     *                        or _getNestedXMLRecord().
     * @throws XML_Query2XML_ConfigException This exception will bubble up
     *                        if it is thrown by _applySqlOptionsToRecord()
     *                        or _getNestedXMLRecord().
     * @throws XML_Query2XML_Exception  This exception will bubble up if it
     *                        is thrown by _getNestedXMLRecord().
     */
    private function _processComplexElementSpecification(&$record, &$options, &$tree,
        $tagName)
    {
        $tag = $tree['tag'];
        if (!isset($tree['elements'])) {
            $tree['elements'] = array();
        }
        if (!isset($tree['elements'][$tagName])) {
            $tree['elements'][$tagName]            = array();
            $tree['elements'][$tagName]['rootTag'] = self::_addNewDOMChild(
                $tag,
                $options['rootTag'],
                $options['--q2x--path'] . '[rootTag]'
            );
        }
        
        $records =& $this->_applySqlOptionsToRecord($options, $record);
        
        for ($i = 0; $i < count($records); $i++) {
            self::_addDOMChildren(
                $tree['elements'][$tagName]['rootTag'],
                $this->_getNestedXMLRecord(
                    $records[$i],
                    $options,
                    $tag->ownerDocument,
                    $tree['elements'][$tagName]
                ),
                $options['--q2x--path']
            );
        }
    }
    
    /**
     * Private method that processes a complex attribute specification
     * for {@link XML_Query2XML::_getNestedXMLRecord()}.
     *
     * A complex attribute specification consists of an associative array
     * with the keys 'value' (mandatory), 'condition', 'sql' and 'sql_options'.
     *
     * @param string  $attributeName The name of the attribute as it was specified
     *                               using the array key of the complex attribute
     *                               specification.
     * @param array   &$record       The current record.
     * @param array   &$options      The complex attribute specification itself.
     * @param DOMNode $tag           The DOMNode to which the attribute is to be
     *                               added.
     *
     * @return void
     * @throws XML_Query2XML_XMLException This exception will bubble up
     *                          if it is thrown by _setDOMAttribute(),
     *                          _applyColumnStringToRecord(),
     *                          _applySqlOptionsToRecord() or _executeEncoder().
     * @throws XML_Query2XML_DBException  This exception will bubble up
     *                          if it is thrown by _applySqlOptionsToRecord().
     * @throws XML_Query2XML_ConfigException This exception will bubble up
     *                          if it is thrown by _applySqlOptionsToRecord() or
     *                          _applyColumnStringToRecord(). It will also be thrown 
     *                          by this method if $options['value'] is not set.
     */
    private function _processComplexAttributeSpecification($attributeName, &$record,
        &$options, $tag)
    {
        if (isset($options['condition'])) {
            $continue = $this->_applyColumnStringToRecord(
                $options['condition'],
                $record,
                $options['--q2x--path'] . '[condition]'
            );
            if (!$continue) {
                // this element is to be skipped
                return;
            }
        }
        
        // only fetching a single record makes sense for a single attribute
        $options['sql_options']['single_record'] = true;
        
        $records = $this->_applySqlOptionsToRecord($options, $record);
        if (count($records) == 0) {
            /*
            * $options['sql'] was set but the query did not return any records.
            * Therefore this attribute is to be skipped.
            */
            return;
        }
        $attributeRecord = $records[0];
        
        $attributeValue = $this->_applyColumnStringToRecord(
            $options['value'],
            $attributeRecord,
            $options['--q2x--path'] . '[value]'
        );
        if ($this->_evaluateCondition($attributeValue, $options['value'])) {
            self::_setDOMAttribute(
                $tag,
                $attributeName,
                self::_executeEncoder($attributeValue, $options),
                $options['--q2x--path'] . '[value]'
            );
        }
    }
                    
    /**
     * Private method to apply the givenen sql option to a record.
     *
     * This method handles the sql options 'single_record',
     * 'merge', 'merge_master' and 'merge_selective'. Please see the
     * {@tutorial XML_Query2XML.pkg tutorial} for details.
     * 
     * @param array &$options An associative multidimensional array of options.
     * @param array &$record  The current record as an associative array.
     *
     * @return array          An indexed array of records that are themselves
     *                        represented as associative arrays.
     * @throws XML_Query2XML_ConfigException This exception is thrown if
     *                        - a column specified in merge_selective does not exist
     *                          in the result set
     *                        - it bubbles up from _applyColumnStringToRecord()
     * @throws XML_Query2XML_DBException This exception will bubble up
     *                        if it is thrown by _getAllRecords().
     * @throws XML_Query2XML_XMLException It will bubble up if it is thrown
     *                        by _applyColumnStringToRecord().
     */
    private function _applySqlOptionsToRecord(&$options, &$record)
    {
        if (!isset($options['sql'])) {
            return array($record);
        }
        
        $single_record   = $options['sql_options']['single_record'];
        $merge           = $options['sql_options']['merge'];
        $merge_master    = $options['sql_options']['merge_master'];
        $merge_selective = $options['sql_options']['merge_selective'];

        $sql = $options['sql'];
        if (is_array($sql)) {
            if (isset($sql['data'])) {
                foreach ($sql['data'] as $key => $columnStr) {
                    $sql['data'][$key] = $this->_applyColumnStringToRecord(
                        $columnStr,
                        $record,
                        $options['--q2x--path'] . '[sql][data][' . $key . ']'
                    );
                }
            }
        }
        $sqlConfigPath = $options['--q2x--path'] . '[sql]';
        
        $records =& $this->_getAllRecords(
            $sql,
            $sqlConfigPath,
            $options['--q2x--query_statement']
        );
        if ($single_record && isset($records[0])) {
            $records = array($records[0]);
        }
        
        if (is_array($merge_selective)) {
            // selective merge
            if ($merge_master) {
                // current records are master
                for ($ii = 0; $ii < count($merge_selective); $ii++) {
                    for ($i = 0; $i < count($records); $i++) {
                        if (!array_key_exists($merge_selective[$ii], $record)) {
                            /* Selected field does not exist in the parent record
                            * (passed as argumnet $record)
                            * unit test: _applySqlOptionsToRecord/
                            *  throwConfigException_mergeMasterTrue.phpt
                            */
                            throw new XML_Query2XML_ConfigException(
                                $options['--q2x--path'] . '[sql_options]'
                                . '[merge_selective]['. $ii . ']: The column "'
                                . $merge_selective[$ii] . '" '
                                . 'was not found in the result set.'
                            );
                        }
                        if (!array_key_exists($merge_selective[$ii], $records[$i])) {
                            // we are the master, so only if it does not yet exist
                            $records[$i][$merge_selective[$ii]] =
                                $record[$merge_selective[$ii]];
                        }
                    }
                }
            } else {
                // parent record is master
                for ($ii = 0; $ii < count($merge_selective); $ii++) {
                    for ($i = 0; $i < count($records); $i++) {
                        if (!array_key_exists($merge_selective[$ii], $record)) {
                            /* Selected field does not exist in the parent record
                            *  (passed as argumnet $record)
                            *  unit test: _applySqlOptionsToRecord/
                            *   throwConfigException_mergeMasterFalse.phpt
                            */
                            throw new XML_Query2XML_ConfigException(
                                $options['--q2x--path'] . '[sql_options]'
                                . '[merge_selective]['. $ii . ']: The column "'
                                . $merge_selective[$ii] . '" '
                                . 'was not found in the result set.'
                            );
                        }
                        // parent is master!
                        $records[$i][$merge_selective[$ii]] =
                            $record[$merge_selective[$ii]];
                    }
                }
            }
        } elseif ($merge) {
            // regular merge
            if ($merge_master) {
                for ($i = 0; $i < count($records); $i++) {
                    $records[$i] = array_merge($record, $records[$i]);
                } 
            } else {
                for ($i = 0; $i < count($records); $i++) {
                    $records[$i] = array_merge($records[$i], $record);
                }
            }
        }
        return $records;
    }
    
    /**
     * Private method to apply a column string to a record.
     * Please see the tutorial for details on the different column strings.
     *
     * @param string $columnStr  A valid column name or an instance of a class
     *                           implementing XML_Query2XML_Callback.
     * @param array  &$record    The record as an associative array.
     * @param string $configPath The config path; used for exception messages.
     *
     * @return mixed A value that can be cast to a string or an instance of DOMNode.
     * @throws XML_Query2XML_ConfigException  Thrown if $columnStr is not
     *               a string or an instance of XML_Query2XML_Callback or if
     *               $record[$columnStr] does not exist (and $columnStr has
     *               no special prefix).
     * @throws XML_Query2XML_XMLException     Thrown if the '&' prefix was used
     *               but the data was not unserializeable, i.e. not valid XML data.
     */
    private function _applyColumnStringToRecord($columnStr, &$record, $configPath)
    {
        if (self::_isCallback($columnStr)) {
            $value = $columnStr->execute($record);
        } elseif (is_string($columnStr)) {
            if (array_key_exists($columnStr, $record)) {
                $value = $record[$columnStr];
            } else {
                /*
                * unit test:
                *  _applyColumnStringToRecord/throwConfigException_element1.phpt
                *  _applyColumnStringToRecord/throwConfigException_element2.phpt
                *  _applyColumnStringToRecord/throwConfigException_idcolumn.phpt
                */
                throw new XML_Query2XML_ConfigException(
                    $configPath . ': The column "' . $columnStr
                    . '" was not found in the result set.'
                );
                
            }
        } else {
            // should never be reached
            throw new XML_Query2XML_ConfigException(
                $configPath . ': string or instance of XML_Query2XML_Callback'
                . ' expected, ' . gettype($columnStr) . ' given.'
            );
        }
        return $value;
    }
    
    /**
     * Returns whether $value is to be included in the output.
     * If $spec is a string an is prefixed by a question mark this method will
     * return false if $value is null or is a string with a length of zero. In
     * any other case, this method will return the true.
     *
     * @param string $value The value.
     * @param mixed  $spec  The value specification. This can be a string
     *                      or an instance of XML_Query2XML_Callback.
     *
     * @return boolean Whether $value is to be included in the output.
     */
    private function _evaluateCondition($value, $spec)
    {
        return !class_exists('XML_Query2XML_Data_Condition') ||
               !$spec instanceof XML_Query2XML_Data_Condition ||
               $spec->evaluateCondition($value);
    }
            
    /**
     * Private method to fetch all records from a result set.
     *
     * @param mixed  $sql            The SQL query as a string or an array.
     * @param string $configPath     The config path; used for exception messages.
     * @param string $queryStatement The query as a string; it will be used for
     *                               logging and profiling.
     *
     * @return array An array of records. Each record itself will be an
     *                   associative array.
     */
    private function &_getAllRecords($sql, $configPath, $queryStatement)
    {
        // $queryStatement will be used for profiling
        if ($this->_profiling || $this->_debug) {
            $loggingQuery = $queryStatement;
            if (is_array($sql) && isset($sql['data']) && is_array($sql['data'])) {
                $loggingQuery .= '; DATA:' . implode(',', $sql['data']);
            }
            $this->_debugStartQuery($loggingQuery, $queryStatement);
        }
        
        if (is_array($sql) && isset($sql['driver'])) {
            $driver = $sql['driver'];
        } else {
            $driver = $this->_driver;
        }
        $records = $driver->getAllRecords($sql, $configPath);
        
        $this->_debugStopQuery($queryStatement);
        return $records;
    }
    
    /**
     * Initializes a query's profile (only used if profiling is turned on).
     *
     * @param mixed &$sql The SQL query as a string or an array.
     *
     * @return void
     * @see startProfiling()
     */
    private function _initQueryProfile(&$sql)
    {
        if (!isset($this->_profile['queries'][$sql])) {
            $this->_profile['queries'][$sql] = array(
                'count' => 0,
                'runTimes' => array()
            );
        }
    }
    
    /**
     * Starts the debugging and profiling of the query passed as argument.
     *
     * @param string $loggingQuery   The query statement as it will be logged.
     * @param string $profilingQuery The query statement as it will be used for
     *                               profiling.
     *
     * @return void
     */
    private function _debugStartQuery($loggingQuery, $profilingQuery)
    {
        $this->_debug('QUERY: ' . $loggingQuery);
        if ($this->_profiling) {
            $this->_initQueryProfile($profilingQuery);
            ++$this->_profile['queries'][$profilingQuery]['count'];
            $this->_profile['queries'][$profilingQuery]['runTimes'][] = array(
                'start' => microtime(true),
                'stop' => 0
            );
        }
    }
    
    /**
     * Ends the debugging and profiling of the query passed as argument.
     *
     * @param string $profilingQuery The query statement as it will be used for
     *                               profiling.
     *
     * @return void
     */
    private function _debugStopQuery($profilingQuery)
    {
        $this->_debug('DONE');
        if ($this->_profiling) {
            $this->_initQueryProfile($profilingQuery);
            $lastIndex =
                count(
                    $this->_profile['queries'][$profilingQuery]['runTimes']
                ) - 1;
            
            $this->_profile['queries'][$profilingQuery]['runTimes'][$lastIndex]['stop'] =
                microtime(true);
        }
    }
    
    /**
     * Stops the DB profiling.
     * This will set $this->_profile['dbDuration'].
     *
     * @return void
     */
    private function _stopDBProfiling()
    {
        if ($this->_profiling && isset($this->_profile['start'])) {
            $this->_profile['dbStop']     = microtime(1);
            $this->_profile['dbDuration'] =
                $this->_profile['dbStop'] - $this->_profile['start'];
        }
    }
    
    /**
     * Private method used to log debug messages.
     * This method will do no logging if $this->_debug is set to false.
     *
     * @param string $msg The message to log.
     *
     * @return void
     * @see _debugLogger
     * @see _debug
     */
    private function _debug($msg)
    {
        if ($this->_debug) {
            $this->_debugLogger->log($msg);
        }
    }
    
    /**
     * Returns whether $object is an instance of XML_Query2XML_Callback.
     *
     * @param mixed $object The variable to check.
     *
     * @return boolean
     */
    private static function _isCallback($object)
    {
        return is_object($object) &&
               interface_exists('XML_Query2XML_Callback') &&
               $object instanceof XML_Query2XML_Callback;
    }
    
    /**
     * Parse specifications that use the prifixes ?, &, =, ^, :,  or #.
     *
     * This method will produce a number of chained Data Class objects all of
     * which be an instance of the abstract class XML_Query2XML_Data.
     *
     * @param string $columnStr  The original specification.
     * @param string $configPath The config path; used for exception messages.
     *
     * @return mixed An instance of XML_Query2XML_Callback or a column
     *               name as a string.
     * @throws XML_Query2XML_ConfigException Bubbles up through this method if
     *                                       thrown by any of the command class
     *                                       constructors.
     */
    private function _buildCommandChain($columnStr, $configPath)
    {
        $prefixList = implode('', array_keys($this->_prefixes));
        if (ltrim($columnStr, $prefixList) == $columnStr) {
            return $columnStr;
        }
        
        $firstCallback = null;
        for ($i = 0; $i < strlen($columnStr); $i++) {
            $prefix = substr($columnStr, $i, 1);
            if (isset($this->_prefixes[$prefix])) {
                $columnSubStr = substr($columnStr, $i + 1);
                $filePath     = $this->_prefixes[$prefix][0];
                $className    = $this->_prefixes[$prefix][1];
                if ($columnSubStr === false) {
                    $columnSubStr = '';
                }
                
                if ($filePath) {
                    include_once $filePath;
                }
                
                if (!in_array(
                        'XML_Query2XML_Data',
                        class_parents($className)
                    )
                ) {
                    throw new XML_Query2XML_ConfigException(
                        $configPath . ': Prefix class ' . $className . ' does ' .
                        'not extend XML_Query2XML_Data.'
                    );
                }
                
                if (in_array(
                        'XML_Query2XML_Data_Source',
                        class_parents($className)
                    )
                ) {
                    // data source prefix
                    $callback = call_user_func_array(
                        array($className, 'create'),
                        array($columnSubStr, $configPath)
                    );
                } else {
                    // data processing prefix
                    $callback = call_user_func_array(
                        array($className, 'create'),
                        array(null, $configPath)
                    );
                    
                    if (ltrim($columnSubStr, $prefixList) == $columnSubStr) {
                        // no more prefixes: ColumnValue is the default data source
                        include_once 'XML/Query2XML/Data/Source/ColumnValue.php';
                        $callback->setPreProcessor(
                            new XML_Query2XML_Data_Source_ColumnValue(
                                $columnSubStr,
                                $configPath
                            )
                        );
                    }
                }
                
                if (is_null($firstCallback)) {
                    $firstCallback = $callback;
                } else {
                    if (
                        $callback instanceof XML_Query2XML_Data_Condition &&
                        !($firstCallback instanceof XML_Query2XML_Data_Condition)
                    ) {
                        throw new XML_Query2XML_ConfigException(
                            $configPath . ': conditional prefixes always have to '
                            . 'go first.'
                        );
                    }
                    $firstCallback->getFirstPreProcessor()->setPreProcessor(
                        $callback
                    );
                }
                if (
                    $firstCallback->getFirstPreProcessor()
                    instanceof XML_Query2XML_Data_Source
                ) {
                    // there can only be one data source
                    break;
                }
            } else {
                break;
            }
        }
        if (is_null($firstCallback)) {
            return $columnStr;
        } else {
            return $firstCallback;
        }
    }
    
    /**
     * Creates a new instance of DOMDocument.
     * '1.0' is passed as first argument and 'UTF-8' as second to the
     * DOMDocument constructor.
     *
     * @return DOMDocument The new instance.
     */
    private static function _createDOMDocument()
    {
        return new DOMDocument('1.0', 'UTF-8');
    }
    
    /**
     * Create and then add a new child element.
     *
     * @param DOMNode $element    The parent DOMNode the new DOM element should be
     *                            appended to.
     * @param string  $name       The tag name of the new element.
     * @param string  $configPath The config path; used for exception messages.
     * @param string  $value      The value of a child text node. This argument is
     *                            optional. The default is the boolean value false,
     *                            which means that no child text node will be
     *                            appended.
     *
     * @return DOMNode The newly created DOMNode instance that was appended
     *                 to $element.
     * @throws XML_Query2XML_XMLException This exception will bubble up if it is
     *                 thrown by _createDOMElement().
     */
    private static function _addNewDOMChild(DOMNode $element, $name, $configPath,
        $value = false)
    {
        if ($element instanceof DOMDocument) {
            $dom = $element;
        } else {
            $dom = $element->ownerDocument;
        }
        $child = self::_createDOMElement($dom, $name, $configPath, $value);
        $element->appendChild($child);
        return $child;
    }
    
    /**
     * Helper method to create a new instance of DOMNode
     *
     * @param DOMDocument $dom        An instance of DOMDocument. It's
     *                                createElement() method is used to create the
     *                                new DOMNode instance.
     * @param string      $name       The tag name of the new element.
     * @param string      $configPath The config path; used for exception messages.
     * @param string      $value      The value of a child text node. This argument
     *                                is optional. The default is the boolean value
     *                                false, which means that no child text node will
     *                                be appended.
     *
     * @return DOMNode An instance of DOMNode.
     * @throws XML_Query2XML_XMLException If $name is an invalid XML identifier.
     *                                    Also it will bubble up if it is thrown by
     *                                    _appendTextChildNode().
     */
    private static function _createDOMElement(DOMDocument $dom, $name, $configPath,
        $value = false)
    {
        try {
            $element = $dom->createElement($name);
        } catch(DOMException $e) {
            /*
            * unit tests:
            *  _createDOMElement/throwXMLException_elementInvalid1.phpt
            *  _createDOMElement/throwXMLException_elementInvalid2.phpt
            *  _createDOMElement/throwXMLException_roottagOptionInvalid1.phpt
            *  _createDOMElement/throwXMLException_roottagOptionInvalid2.phpt
            *  _createDOMElement/throwXMLException_rowtagOptionInvalid.phpt
            */
            throw new XML_Query2XML_XMLException(
                $configPath . ': "' . $name . '" is an invalid XML element name: '
                . $e->getMessage(),
                $e
            );
        }
        self::_appendTextChildNode($element, $value, $configPath);
        return $element;
    }
    
    /**
     * Append a new child text node to $element.
     * $value must already be UTF8-encoded; this is to be handled
     * by self::_executeEncoder() and $options['encoder'].
     *
     * This method will not create and append a child text node
     * if $value === false || is_null($value).
     *
     * @param DOMNode $element    An instance of DOMNode
     * @param string  $value      The value of the text node.
     * @param string  $configPath The config path; used for exception messages.
     *
     * @return void
     * @throws XML_Query2XML_XMLException Any lower-level DOMException will 
     *                 wrapped and re-thrown as a XML_Query2XML_XMLException. This 
     *                 will happen if $value cannot be UTF8-encoded for some reason.
     *                 It will also be thrown if $value is an object or an array
     *                 (and can therefore not be converted into a string).
     */
    private static function _appendTextChildNode(DOMNode $element,
                                                 $value,
                                                 $configPath)
    {
        if ($value === false || is_null($value)) {
            return;
        } elseif (is_object($value) || is_array($value)) {
            /*
            * Objects and arrays cannot be cast
            * to a string without an error.
            *
            * unit test:
            * _appendTextChildNode/throwXMLException.phpt
            */
            throw new XML_Query2XML_XMLException(
                $configPath . ': A value of the type ' . gettype($value)
                . ' cannot be used for a text node.'
            );
        }
        $dom = $element->ownerDocument;
        try {
            $element->appendChild($dom->createTextNode($value));
        } catch(DOMException $e) {
            // this should never happen as $value is UTF-8 encoded
            throw new XML_Query2XML_XMLException(
                $configPath . ': "' . $value . '" is not a vaild text node: '
                . $e->getMessage(),
                $e
            );
        }
    }
    
    /**
     * Set the attribute $name with a value of $value for $element.
     * $value must already be UTF8-encoded; this is to be handled
     * by self::_executeEncoder() and $options['encoder'].
     *
     * @param DOMNode $element    An instance of DOMNode
     * @param string  $name       The name of the attribute to set.
     * @param string  $value      The value of the attribute to set.
     * @param string  $configPath The config path; used for exception messages.
     *
     * @return void
     * @throws XML_Query2XML_XMLException Any lower-level DOMException will be
     *                 wrapped and re-thrown as a XML_Query2XML_XMLException. This
     *                 will happen if $name is not a valid attribute name. It will
     *                 also be thrown if $value is an object or an array (and can
     *                 therefore not be converted into a string).
     */
    private static function _setDOMAttribute(DOMNode $element,
                                             $name,
                                             $value,
                                             $configPath)
    {
        if (is_object($value) || is_array($value)) {
            /*
            * Objects and arrays cannot be cast
            * to a string without an error.
            *
            * unit test:
            * _setDOMAttribute/throwXMLException.phpt
            */
            throw new XML_Query2XML_XMLException(
                $configPath . ': A value of the type ' . gettype($value)
                . ' cannot be used for an attribute value.'
            );
        }
        
        try {
            $element->setAttribute($name, $value);
        } catch(DOMException $e) {
            // no unit test available for this one
            throw new XML_Query2XML_XMLException(
                $configPath . ': "' . $name . '" is an invalid XML attribute name: '
                . $e->getMessage(),
                $e
            );
        }
    }
    
    /**
     * Adds one or more child nodes to an existing DOMNode instance.
     *
     * @param DOMNode $base       An instance of DOMNode.
     * @param mixed   $children   An array of DOMNode instances or
     *                            just a single DOMNode instance.
     *                            Boolean values of false are always ignored.
     * @param string  $configPath The config path; used for exception messages.
     * @param boolean $import     Whether DOMDocument::importNode() should be called
     *                            for $children. This is necessary if the instance(s)
     *                            passed as $children was/were created using a
     *                            different DOMDocument instance. This argument is
     *                            optional. The default is false.
     *
     * @return void
     * @throws XML_Query2XML_XMLException If one of the specified children
     *                         is not one of the following: an instance of DOMNode,
     *                         the boolean value false, or an array containing
     *                         these two.
     */
    private static function _addDOMChildren(DOMNode $base,
                                            $children,
                                            $configPath,
                                            $import = false)
    {
        if ($children === false) {
            // don't do anything
            return;
        } elseif ($children instanceof DOMNode) {
            // $children is a single complex child
            if ($import) {
                $children = $base->ownerDocument->importNode($children, true);
            }
            $base->appendChild($children);
        } elseif (is_array($children)) {
            for ($i = 0; $i < count($children); $i++) {
                if ($children[$i] === false) {
                    // don't do anything
                } elseif ($children[$i] instanceof DOMNode) {
                    if ($import) {
                        $children[$i] = $base->ownerDocument->importNode(
                            $children[$i],
                            true
                        );
                    }
                    $base->appendChild($children[$i]);
                } else {
                    /*
                    * unit tests:
                    * _addDOMChildren/throwXMLException_arrayWithObject.phpt
                    * _addDOMChildren/throwXMLException_arrayWithString.phpt
                    * _addDOMChildren/throwXMLException_arrayWithInt.phpt
                    * _addDOMChildren/throwXMLException_arrayWithBool.phpt
                    * _addDOMChildren/throwXMLException_arrayWithDouble.phpt
                    */
                    throw new XML_Query2XML_XMLException(
                        $configPath . ': DOMNode, false or an array of the two '
                        . 'expected, but ' . gettype($children[$i]) . ' given '
                        . '(hint: check your callback).'
                    );
                }
            }
        } else {
            /*
             * This should never happen because _addDOMChildren() is only called
             * for arrays and instances of DOMNode.
             */
            throw new XML_Query2XML_XMLException(
                $configPath . ': DOMNode, false or an array of the two '
                . 'expected, but ' . gettype($children) . ' given '
                . '(hint: check your callback).'
            );
        }
    }
    
    /**
     * Remove all container elements created by XML_Query2XML to ensure that all
     * elements are correctly ordered.
     *
     * This is a recursive method. This method calls
     * {@link XML_Query2XML::_replaceParentWithChildren()}. For the concept of
     * container elements please see the {@tutorial XML_Query2XML.pkg tutorial}.
     *
     * @param DOMNode $element               An instance of DOMNode.
     * @param string  $hiddenContainerPrefix The containers that will be removed
     *                                       all start with this string.
     *
     * @return void
     */
    private static function _removeContainers($element, $hiddenContainerPrefix)
    {
        $xpath      = new DOMXPath($element);
        $containers = $xpath->query(
            '//*[starts-with(name(),\'' . $hiddenContainerPrefix . '\')]'
        );
        foreach ($containers as $container) {
            if (!is_null($container->parentNode)) {
                self::_replaceParentWithChildren($container);
            }
        }
    }
    
    /**
     * Replace a certain node with its child nodes.
     *
     * @param DOMNode $parent An instance of DOMNode.
     *
     * @return void
     */
    private static function _replaceParentWithChildren(DOMNode $parent)
    {
        
        $child = $parent->firstChild;
        while ($child) {
            $nextChild = $child->nextSibling;
            $parent->removeChild($child);
            $parent->parentNode->insertBefore($child, $parent);
            $child = $nextChild;
        }
        $parent->parentNode->removeChild($parent);
    }
    
    /**
     * Calls an encoder for XML node and attribute values
     * $options['encoder'] can be one of the following:
     * - null: self::_utf8encode() will be used
     * - false: no encoding will be performed
     * - callback: a string or an array as defined by the
     *   callback pseudo-type; please see
     *   http://www.php.net/manual/en/
     *   language.pseudo-types.php#language.types.callback
     *
     * @param string $str     The string to encode
     * @param array  $options An associative array with $options['encoder'] set.
     *
     * @return void
     * @throws XML_Query2XML_XMLException If the $options['encoder'] is a callback
     *                                    function that threw an exception.
     */
    private static function _executeEncoder($str, $options)
    {
        if (!is_string($str) || $options['encoder'] === false) {
            return $str;
        }
        
        if ($options['encoder'] === null) {
            return self::_utf8encode($str);
        }
        
        try {
            return call_user_func($options['encoder'], $str);
        } catch (Exception $e) {
            /*
            * unit test:
            *  _executeEncoder/throwXMLException.phpt
            */
            throw new XML_Query2XML_XMLException(
                $options['--q2x--path'] . '[encoder]: Could not encode '
                . '"' . $str . '": ' . $e->getMessage()
            );
        }
    }
    
    /**
     * UTF-8 encode $str using mb_conver_encoding or if that is not
     * present, utf8_encode.
     *
     * @param string $str The string to encode
     *
     * @return String The UTF-8 encoded version of $str
     */
    private static function _utf8encode($str)
    {
        if (function_exists('mb_convert_encoding')) {
            $str = mb_convert_encoding($str, 'UTF-8');
        } else {
            $str = utf8_encode($str);
        }
        return $str;
    }
}

/**
 * Parent class for ALL exceptions thrown by this package.
 * By catching XML_Query2XML_Exception you will catch all exceptions
 * thrown by XML_Query2XML.
 *
 * @category XML
 * @package  XML_Query2XML
 * @author   Lukas Feiler <lukas.feiler@lukasfeiler.com>
 * @license  http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
 * @link     http://pear.php.net/package/XML_Query2XML
 */
class XML_Query2XML_Exception extends PEAR_Exception
{
    
    /**
     * Constructor method
     *
     * @param string    $message   The error message.
     * @param Exception $exception The Exception that caused this exception 
     *                             to be thrown. This argument is optional.
     */
    public function __construct($message, $exception = null)
    {
        parent::__construct($message, $exception);
    }
}

/**
 * Exception for driver errors
 *
 * @category XML
 * @package  XML_Query2XML
 * @author   Lukas Feiler <lukas.feiler@lukasfeiler.com>
 * @license  http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
 * @link     http://pear.php.net/package/XML_Query2XML
 * @since    Release 1.6.0RC1
 */
class XML_Query2XML_DriverException extends XML_Query2XML_Exception
{
    /**
     * Constructor
     *
     * @param string $message The error message.
     */
    public function __construct($message)
    {
        parent::__construct($message);
    }
}

/**
 * Exception for database errors
 *
 * @category XML
 * @package  XML_Query2XML
 * @author   Lukas Feiler <lukas.feiler@lukasfeiler.com>
 * @license  http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
 * @link     http://pear.php.net/package/XML_Query2XML
 */
class XML_Query2XML_DBException extends XML_Query2XML_DriverException
{
    /**
     * Constructor
     *
     * @param string $message The error message.
     */
    public function __construct($message)
    {
        parent::__construct($message);
    }
}

/**
 * Exception for XML errors
 * In most cases this exception will be thrown if a DOMException occurs.
 *
 * @category XML
 * @package  XML_Query2XML
 * @author   Lukas Feiler <lukas.feiler@lukasfeiler.com>
 * @license  http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
 * @link     http://pear.php.net/package/XML_Query2XML
 */
class XML_Query2XML_XMLException extends XML_Query2XML_Exception
{
    /**
     * Constructor
     *
     * @param string       $message   The error message.
     * @param DOMException $exception The DOMException that caused this exception 
     *                                to be thrown. This argument is optional.
     */
    public function __construct($message, DOMException $exception = null)
    {
        parent::__construct($message, $exception);
    }
}

/**
 * Exception that handles configuration errors.
 *
 * This exception handels errors in the $options array passed to
 * XML_Query2XML::getXML() and wrong arguments passed to the constructor via
 * XML_Query2XML::factory().
 *
 * @category XML
 * @package  XML_Query2XML
 * @author   Lukas Feiler <lukas.feiler@lukasfeiler.com>
 * @license  http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
 * @link     http://pear.php.net/package/XML_Query2XML
 * @see      XML_Query2XML::getXML()
 */
class XML_Query2XML_ConfigException extends XML_Query2XML_Exception
{
    /**
     * Constructor method
     *
     * @param string $message A detailed error message.
     */
    public function __construct($message)
    {
        parent::__construct($message);
    }
}

/**
 * Abstract driver class.
 *
 * usage:
 * <code>
 * $driver = XML_Query2XML_Driver::factory($backend);
 * </code>
 *
 * @category XML
 * @package  XML_Query2XML
 * @author   Lukas Feiler <lukas.feiler@lukasfeiler.com>
 * @license  http://www.gnu.org/copyleft/lesser.html  LGPL Version 2.1
 * @version  Release: 1.7.2
 * @link     http://pear.php.net/package/XML_Query2XML
 * @since    Release 1.5.0RC1
 */
abstract class XML_Query2XML_Driver
{
    /**
     * This method, when implemented executes the query passed as the
     * first argument and returns all records from the result set.
     *
     * The format of the first argument depends on the driver being used.
     *
     * @param mixed  $sql        The SQL query as a string or an array.
     * @param string $configPath The config path; used for exception messages.
     *
     * @return array An array of records. Each record itself will be an
     *               associative array.
     * @throws XML_Query2XML_DriverException If some driver related error occures.
     */
    abstract public function getAllRecords($sql, $configPath);
    
    /**
     * Pre-processes a query specification and returns a string representation
     * of the query.
     *
     * The returned string will be used for logging purposes. It
     * does not need to be valid SQL.
     *
     * If $query is a string, it will be changed to array('query' => $query).
     *
     * @param mixed  &$query     A string or an array containing the element 'query'.
     * @param string $configPath The config path; used for exception messages.
     *
     * @return string The query statement as a string.
     * @throws XML_Query2XML_ConfigException If $query is an array but does not
     *                                       contain the element 'query'.
     */
    public function preprocessQuery(&$query, $configPath)
    {
        if (is_string($query)) {
            $query = array('query' => $query);
        } elseif (is_array($query)) {
            if (!isset($query['query'])) {
                /*
                * unit test: _preprocessOptions/
                *  throwConfigException_queryOptionMissing.phpt
                */
                throw new XML_Query2XML_ConfigException(
                    $configPath . ': The configuration option'
                    . ' "query" is missing.'
                );
            }
        } else { //neither a string nor an array
            /*
            * unit test: _preprocessOptions/
            *  throwConfigException_sqlOptionWrongType.phpt
            */
            throw new XML_Query2XML_ConfigException(
                $configPath . ': array or string expected, '
                . gettype($query) . ' given.'
            );
        }
        return $query['query'];
    }
    
    /**
     * Factory method.
     *
     * @param mixed $backend An instance of MDB2_Driver_Common, PDO, DB_common,
     *                  ADOConnection, Net_LDAP2 or Net_LDAP.
     *
     * @return XML_Query2XML_Driver An instance of a driver class that
     *                  extends XML_Query2XML_Driver.
     * @throws XML_Query2XML_DriverException If $backend already is a PEAR_Error.
     * @throws XML_Query2XML_ConfigException If $backend is not an instance of a
     *                  child class of MDB2_Driver_Common, PDO, DB_common,
     *                  ADOConnection, Net_LDAP2 or Net_LDAP.
     */
    public static function factory($backend)
    {
        if (
            class_exists('MDB2_Driver_Common') &&
            $backend instanceof MDB2_Driver_Common
        ) {
            include_once 'XML/Query2XML/Driver/MDB2.php';
            return new XML_Query2XML_Driver_MDB2($backend);
        } elseif (class_exists('PDO') && $backend instanceof PDO) {
            include_once 'XML/Query2XML/Driver/PDO.php';
            return new XML_Query2XML_Driver_PDO($backend);
        } elseif (class_exists('DB_common') && $backend instanceof DB_common) {
            include_once 'XML/Query2XML/Driver/DB.php';
            return new XML_Query2XML_Driver_DB($backend);
        } elseif (
            class_exists('ADOConnection') &&
            $backend instanceof ADOConnection
        ) {
            include_once 'XML/Query2XML/Driver/ADOdb.php';
            return new XML_Query2XML_Driver_ADOdb($backend);
        } elseif (class_exists('Net_LDAP') && $backend instanceof Net_LDAP) {
            include_once 'XML/Query2XML/Driver/LDAP.php';
            return new XML_Query2XML_Driver_LDAP($backend);
        } elseif (class_exists('Net_LDAP2') && $backend instanceof Net_LDAP2) {
            include_once 'XML/Query2XML/Driver/LDAP2.php';
            return new XML_Query2XML_Driver_LDAP2($backend);
        } elseif (class_exists('PEAR_Error') && $backend instanceof PEAR_Error) {
            //unit tests: NoDBLayer/factory/throwDBException.phpt
            throw new XML_Query2XML_DriverException(
                'Driver error: ' . $backend->toString()
            );
        } else {
            //unit test: NoDBLayer/factory/throwConfigException.phpt
            throw new XML_Query2XML_ConfigException(
                'Argument passed to the XML_Query2XML constructor is not an '
                . 'instance of DB_common, MDB2_Driver_Common, ADOConnection'
                . ', PDO, Net_LDAP or Net_LDAP2.'
            );
        }
    }
}
?>