1Pietsje LLeeuwarden1983
    2Klaasje LLeeuwarden1982
    3Groningen1983
    4Thomas AAnderlecht1982
  
*/
/* (example.php)
$sql = kjwsql_from_url('xmlsql://localhost/mydb');
print_r($sql->selectAll("* FROM mytable order by destination, id desc limit 1,2"));
*/
if (!function_exists('xml_parser_create'))
	trigger_error('xml_* functions not defined.', E_USER_ERROR);
require_once(dirname(__FILE__) . '/KjwFakeSql.php');
require_once(dirname(__FILE__) . '/KjwArrayResultSet.php');
/**
 * KjwXmlSql is a tiny xml implementation of the KjwFakeSql abstract class. It does not
 * support much SQL at all.
 * It's supposed to be a temporary solution for tiny database needs.
 * Don't use this for anything larger than a tiny guestbook or some other
 * small thing.
 */
class KjwXmlSql extends KjwFakeSql {
	var $_dbpath = null;
	var $_last_insert_id = null;
	/**
	 * Construct a KjwXmlSql object.
     *
	 * @param $server Must be 'localhost' as we'll be using local files.
	 * @param $unused_port Unused, must be a zero.
	 * @param $unused_user Username, must be an empty string.
	 * @param $unused_pass Password, must be an empty string.
	 * @param $path Path to the xml/lock files, should be writable.
	 */
	function KjwXmlSql($server, $unused_port, $unused_user, $unused_pass, $path) {
		assert('$server == "localhost" && $unused_port === 0 && $unused_user == "" && $unused_pass == ""');
		if (substr($path, -1) != '/')
			$path .= '/';
		parent::KjwFakeSql('xmlsql', $server, $unused_port, $unused_user, $unused_pass, $path . 'lock');
		$this->_dbpath = $path;
	}
/*
	function updateArray($table, $args, $where = null) {
		$this->croak('Database Data Transfer Failure', 'implement me');
	}
*/
	function insertId() {
		return $this->_last_insert_id;
	}
	function _simpleSelect($table, $columns, $order, $skip = 0, $max = -1) {
		$obj = new KjwXmlSqlTable($table, $this->_dbpath);
		$obj->sort($order);
		$resultSet = $obj->getResultSet($skip, $max);
		$obj->destroy();
		return $resultSet;
	}
	function _simpleInsert($table, $args) {
		$this->_last_insert_id = 0;
		$obj = new KjwXmlSqlTable($table, $this->_dbpath);
		// Args without keys => columnless insert
		if (array_key_exists(0, $args)) {
			
			if (sizeof($args) != sizeof($obj->_description))
				$this->croak('Database Data Transfer Failure', 'Value count mismatches column count');
			$values = $args;
		// Args with keys => rewrite to array
		} else {
			$values = array();
			foreach ($args as $key => $value) {
				for ($i = 0; $i < sizeof($obj->_description); ++$i) {
					if ($obj->_description[$i]['name'] == $key) {
						$values[$i] = $value;
						break;
					}
				}
			}
			if (sizeof($values) != sizeof($args))
				$this->croak('Database Data Transfer Failure', 'Trying to write to non-existent columns?');
			for ($i = 0; $i < sizeof($obj->_description); ++$i) {
				if (!isset($values[$i]))
					$values[$i] = null;
				if ($obj->_description[$i]['type'] == 'serial' && $values[$i] === null)
					$this->_last_insert_id = $values[$i] = $obj->_description[$i]['next']++;
			}
			ksort($values); // fix our unordered array insert
		}
		// Convert nulls in serial columns to 'next'
		for ($i = 0; $i < sizeof($obj->_description); ++$i) {
			if ($obj->_description[$i]['type'] == 'serial' && $values[$i] === null)
				$this->_last_insert_id = $values[$i] = $obj->_description[$i]['next']++;
		}
		// Add the values to the end of the data
		array_push($obj->_data, $values);
		$obj->store($table);
		$obj->destroy();
	}
}
class KjwXmlSqlTable extends KjwObject {
	function KjwXmlSqlTable($tableName, $dbPath) {
		parent::KjwObject();
		$this->_tableName = $tableName;
		$this->_tableFile = $dbPath . $tableName . '.xml';
		$this->_state = 'root';
		$parser = xml_parser_create();
		xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, false);
		xml_set_object($parser, &$this);
		xml_set_element_handler($parser, 'onBeginElement', 'onEndElement');
		xml_set_character_data_handler($parser, 'onData');
		$fp = fopen($this->_tableFile, "rb"); // XXX mooie croaks als het misgaat
		while (($data = fread($fp, 8192)) !== '') {
			if (!xml_parse($parser, $data, feof($fp)))
				$this->croak('Database Data Transfer Failure', 'Xml parsing failed!');
		}
		fclose($fp);
		xml_parser_free($parser);
	}
	function sort($columnOrder) {
		$this->_setColumnOrder($columnOrder);
		$ret = usort($this->_data, array($this, 'cmp'));
		assert('$ret == true');
		unset($this->_columnOrder);
	}
	function onBeginElement($parser, $name, $attributes) {
		if ($this->_state == 'root' && $name == 'table') {
			if ($attributes['name'] != $this->_tableName)
				$this->croak('Database Data Transfer Failure', 'Table name missing or invalid');
			$this->_state = 'table';
		} elseif ($this->_state == 'table' && $name == 'description') {
			if (isset($this->_description))
				$this->croak('Database Data Transfer Failure', 'Got description already');
			$this->_state = 'description';
			$this->_description = array();
		} elseif ($this->_state == 'description' && $name == 'column') {
			if (!isset($attributes['type']) || !isset($attributes['name']))
				$this->croak('Database Data Transfer Failure', 'Description lacks type or name');
			array_push($this->_description, $attributes);
		} elseif ($this->_state == 'table' && $name == 'data') {
			if (!isset($this->_description))
				$this->croak('Database Data Transfer Failure', 'Data found before description.');
			$this->_state = 'data';
			$this->_data = array();
		} elseif ($this->_state == 'data' && $name == 'row') {
			$this->_state = 'data_row';
			$this->_tmp_row = array();
		} elseif ($this->_state == 'data_row' && $name == 'col') {
			if (@$attributes['null']) {
				$this->_state = 'data_column_null';
				array_push($this->_tmp_row, null);
			} else {
				$this->_state = 'data_column';
				array_push($this->_tmp_row, '');
			}
		} else {
			$this->croak('Database Data Transfer Failure', 'Unexpected element: ' . $this->_state . '/' . $name);
		}
	}
	function onEndElement($parser, $name) {
		if ($this->_state == 'table' && $name == 'table') {
			$this->_state = 'root';
		} elseif ($this->_state == 'description' && $name == 'description') {
			$this->_state = 'table';
		} elseif ($this->_state == 'description' && $name == 'column') {
			;
		} elseif ($this->_state == 'data' && $name == 'data') {
			$this->_state = 'table';
		} elseif (($this->_state == 'data_column' || $this->_state == 'data_column_null')
				&& $name == 'col') {
			$this->_state = 'data_row';
		} elseif ($this->_state == 'data_row' && $name == 'row') {
			$this->_state = 'data';
			array_push($this->_data, array_map(array($this, 'cast'), $this->_tmp_row, array_keys($this->_tmp_row)));
			unset($this->_tmp_row);
		} else {
			$this->croak('Database Data Transfer Failure', 'Unexpected element: ' . $this->_state . '/' . $name);
		}
	}
	function onData($parser, $data) {
		if ($this->_state == 'data_column') {
			$this->_tmp_row[sizeof($this->_tmp_row)-1] .= $data;
		} elseif ($this->_state == 'data_column_null') {
			; // Nothing, is already null
		} elseif (trim($data) != '') {
			$this->croak('Database Data Transfer Failure', $this->_state . ' - ' . $data);
		}
	}
	function cast($data, $column) {
		$type = $this->_description[$column]['type'];
		switch ($type) {
		case 'serial': return (int)$data;
		case 'string': return (string)$data;
		case 'number': return (float)$data;
		default: $this->croak('Database Data Transfer Failure', "Unknown data type $type");
		}
	}
	function getResultSet($skip = 0, $max = -1) {
		$arr = array();
		foreach ($this->_data as $k => $v) {
			if ($skip-- > 0)
				continue;
			if ($max-- == 0)
				break;
			$dict = array();
			for ($i = 0; $i < sizeof($this->_description); ++$i)
				$dict[$this->_description[$i]['name']] = $v[$i];
			array_push($arr, $dict);
		}
		return new KjwArrayResultSet($arr);
	}
	function store() {
		$fp = fopen($this->_tableFile, 'wb');
		fwrite($fp, "\n");
		fwrite($fp, "_tableName, ENT_COMPAT, 'UTF-8') . "\">\n  \n");
		foreach ($this->_description as $column) {
			fwrite($fp, "     $value)
				fwrite($fp, " $key=\"" . htmlentities($value, ENT_COMPAT, 'UTF-8') . "\"");
			fwrite($fp, "/>\n");
		}
		fwrite($fp, "  \n  \n");
		foreach ($this->_data as $row) {
			fwrite($fp, "    ");
			foreach ($row as $column) {
				if ($column === null)
					fwrite($fp, "");
				else
					fwrite($fp, "" . htmlentities($column, ENT_COMPAT, 'UTF-8') . "");
			}
			fwrite($fp, "
\n");
		}
		fwrite($fp, "  \n
\n");
	}
    function cmp($a, $b) {
        foreach ($this->_sortOrder as $column => $asc) {
            if (($ret = $this->_primitive_compare($a[$column], $b[$column])) != 0)
                return $asc * $ret;
        }
        return 0;
    }
	function _setColumnOrder($order) {
		$column_order = array();
		foreach ($order as $column) {
			$asc = 1;
			if ($column[0] == '+') {
				$column = substr($column, 1);
			} elseif ($column[0] == '-') {
				$column = substr($column, 1);
				$asc = -1;
			}
			$column_pos = -1;
			for ($i = 0; $i < sizeof($this->_description); ++$i) {
				if ($this->_description[$i]['name'] == $column) {
					$column_pos = $i;
					break;
				}
			}
			if ($column_pos == -1 || isset($column_order[$column_pos]))
				continue;
			$column_order[$column_pos] = $asc;
		}
		$this->_sortOrder = $column_order;
	}
    function _primitive_compare($a, $b) {
		if ($a === $b)
			return 0;
        if (is_null($a) && !is_null($b))
            return 1; // null sorts later.. this is implementation dependent in sql
        if (is_null($b))
            return -1;
        if (is_int($a) or is_float($a))
            return $a < $b ? -1 : ($a == $b ? 0 : 1);
        if (is_string($a))
            return strcasecmp($a, $b);
		assert('false');
    }
}
?>