<?php

/*
 *  $Id: XmlToAppData.php 536 2007-01-10 14:30:38Z heltem $
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information please see
 * <http://propel.phpdb.org>.
 */

include_once 'propel/engine/database/model/AppData.php';

// Phing dependencies
require_once 'phing/parser/AbstractHandler.php';
include_once 'phing/system/io/FileReader.php';

/**
 * A class that is used to parse an input xml schema file and creates an AppData
 * PHP object.
 *
 * @author     Hans Lellelid <hans@xmpl.org> (Propel)
 * @author     Leon Messerschmidt <leon@opticode.co.za> (Torque)
 * @author     Jason van Zyl <jvanzyl@apache.org> (Torque)
 * @author     Martin Poeschl <mpoeschl@marmot.at> (Torque)
 * @author     Daniel Rall <dlr@collab.net> (Torque)
 * @version    $Revision: 536 $
 * @package    propel.engine.database.transform
 */
class XmlToAppData extends AbstractHandler {

	/** enables debug output */
	const DEBUG = false;

	private $app;
	private $platform;
	private $currDB;
	private $currTable;
	private $currColumn;
	private $currFK;
	private $currIndex;
	private $currUnique;
	private $currValidator;
	private $currVendorObject;

	private $isForReferenceOnly;
	private $currentPackage;
	private $currentXmlFile;
	private $defaultPackage;

	private $encoding;

	/** two-dimensional array,
		first dimension is for schemas(key is the path to the schema file),
		second is for tags within the schema */
	private $schemasTagsStack = array();

	public $parser;

	/**
	 * Creates a new instance for the specified database type.
	 *
	 * @param      Platform $platform The type of database for the application.
	 * @param      string $defaultPackage the default PHP package used for the om
	 * @param      string $encoding The database encoding.
	 */
	public function __construct(Platform $platform, $defaultPackage, $encoding = 'iso-8859-1')
	{
		$this->app = new AppData($platform);
		$this->platform = $platform;
		$this->defaultPackage = $defaultPackage;
		$this->firstPass = true;
		$this->encoding = $encoding;
	}

	/**
	 * Parses a XML input file and returns a newly created and
	 * populated AppData structure.
	 *
	 * @param      string $xmlFile The input file to parse.
	 * @return     AppData populated by <code>xmlFile</code>.
	 */
	public function parseFile($xmlFile)
	{
		// we don't want infinite recursion
		if($this->isAlreadyParsed($xmlFile)) {
			return;
		}

		$domDocument = new DomDocument('1.0', 'UTF-8');
		$domDocument->load($xmlFile);

		// store current schema file path
		$this->schemasTagsStack[$xmlFile] = array();

		$this->currentXmlFile = $xmlFile;

		try {
			$fr = new FileReader($xmlFile);
		} catch (Exception $e) {
			$f = new PhingFile($xmlFile);
			throw new Exception("XML File not found: " . $f->getAbsolutePath());
		}

		$br = new BufferedReader($fr);

		$this->parser = new ExpatParser($br);
		$this->parser->parserSetOption(XML_OPTION_CASE_FOLDING, 0);
		$this->parser->setHandler($this);

		try {
			$this->parser->parse();
		} catch (Exception $e) {
			$br->close();
			throw $e;
		}
		$br->close();

		array_pop($this->schemasTagsStack);

		return $this->app;
	}

	/**
	 * Handles opening elements of the xml file.
	 *
	 * @param      string $uri
	 * @param      string $localName The local name (without prefix), or the empty string if
	 *		 Namespace processing is not being performed.
	 * @param      string $rawName The qualified name (with prefix), or the empty string if
	 *		 qualified names are not available.
	 * @param      string $attributes The specified or defaulted attributes
	 */
	public function startElement($name, $attributes) {

		try {

	  $parentTag = $this->peekCurrentSchemaTag();

	  if ($parentTag === false) {

				switch($name) {
					case "database":
						if ($this->isExternalSchema()) {
							$this->currentPackage = @$attributes["package"];
							if ($this->currentPackage === null) {
								$this->currentPackage = $this->defaultPackage;
							}
						} else {
							$this->currDB = $this->app->addDatabase($attributes);
						}
					break;

					default:
						$this->_throwInvalidTagException($name);
				}

			} elseif  ($parentTag == "database") {

				switch($name) {

					case "external-schema":
						$xmlFile = @$attributes["filename"];

						//"referenceOnly" attribute is valid in the main schema XML file only,
						//and it's ingnored in the nested external-schemas
						if(!$this->isExternalSchema()) {
							$isForRefOnly = @$attributes["referenceOnly"];
							$this->isForReferenceOnly = ($isForRefOnly !== null ? (strtolower($isForRefOnly) === "true") : true); // defaults to TRUE
						}

						if ($xmlFile{0} != '/') {
							$f = new PhingFile($this->currentXmlFile);
							$xf = new PhingFile($f->getParent(), $xmlFile);
							$xmlFile = $xf->getPath();
						}

						$this->parseFile($xmlFile);
					break;

		  case "domain":
					  $this->currDB->addDomain($attributes);
				  break;

					case "table":
						$this->currTable = $this->currDB->addTable($attributes);
						if ($this->isExternalSchema()) {
							$this->currTable->setForReferenceOnly($this->isForReferenceOnly);
							$this->currTable->setPackage($this->currentPackage);
						}
					break;

					case "vendor":
						$this->currVendorObject = new ObjectWithVendorSpecificData($this->currDB, $attributes['type']);
					break;

					default:
						$this->_throwInvalidTagException($name);
				}

			} elseif  ($parentTag == "table") {

				switch($name) {
					case "column":
						$this->currColumn = $this->currTable->addColumn($attributes);
					break;

					case "foreign-key":
						$this->currFK = $this->currTable->addForeignKey($attributes);
					break;

					case "index":
						$this->currIndex = $this->currTable->addIndex($attributes);
					break;

					case "unique":
						$this->currUnique = $this->currTable->addUnique($attributes);
					break;

					case "vendor":
						$this->currVendorObject = new ObjectWithVendorSpecificData($this->currTable, $attributes['type']);
					break;

		  case "validator":
					  $this->currValidator = $this->currTable->addValidator($attributes);
		  break;

		  case "id-method-parameter":
			$this->currTable->addIdMethodParameter($attributes);
					break;

					default:
						$this->_throwInvalidTagException($name);
				}

			} elseif  ($parentTag == "column") {

				switch($name) {
					case "inheritance":
						$this->currColumn->addInheritance($attributes);
					break;

					case "vendor":
						$this->currVendorObject = new ObjectWithVendorSpecificData($this->currColumn, $attributes['type']);
					break;

					default:
						$this->_throwInvalidTagException($name);
				}

			} elseif ($parentTag == "foreign-key") {

				switch($name) {
					case "reference":
						$this->currFK->addReference($attributes);
					break;

					case "vendor":
						$this->currVendorObject = new ObjectWithVendorSpecificData($this->currFK, $attributes['type']);
					break;

					default:
						$this->_throwInvalidTagException($name);
				}

			} elseif  ($parentTag == "index") {

				switch($name) {
					case "index-column":
						$this->currIndex->addColumn($attributes);
					break;

					case "vendor":
						$this->currVendorObject = new ObjectWithVendorSpecificData($this->currIndex, $attributes['type']);
					break;

					default:
						$this->_throwInvalidTagException($name);
				}

			} elseif ($parentTag == "unique") {

				switch($name) {
					case "unique-column":
						$this->currUnique->addColumn($attributes);
					break;

					case "vendor":
						$this->currVendorObject = new ObjectWithVendorSpecificData($this->currUnique, $attributes['type']);
					break;

					default:
						$this->_throwInvalidTagException($name);
				}
	  } elseif ($parentTag == "validator") {
		switch($name) {
		  case "rule":
					  $this->currValidator->addRule($attributes);
		  break;
		  default:
			$this->_throwInvalidTagException($name);
		}
			} elseif ($parentTag == "vendor") {

				switch($name) {
					case "parameter":
						if($this->currVendorObject->isCompatible($this->platform->getDatabaseType())) {
							$this->currVendorObject->setVendorParameter($attributes['name'], iconv('utf-8',$this->encoding, $attributes['value']));
						}
					break;

					default:
						$this->_throwInvalidTagException($name);
				}

			} else {
				// it must be an invalid tag
		$this->_throwInvalidTagException($name);
	  }

			$this->pushCurrentSchemaTag($name);

		} catch (BuildException $e) {
			throw $e;
		} catch (Exception $e) {
			echo $e;
			echo "\n";
			throw $e;
		}
	}

	function _throwInvalidTagException($tag_name)
	{
		throw new BuildException("Unexpected tag <" . $tag_name . ">", $this->parser->getLocation());
	}

	/**
	 * Handles closing elements of the xml file.
	 *
	 * @param      uri
	 * @param      localName The local name (without prefix), or the empty string if
	 *		 Namespace processing is not being performed.
	 * @param      rawName The qualified name (with prefix), or the empty string if
	 *		 qualified names are not available.
	 */
	public function endElement($name)
	{
		if (self::DEBUG) {
			print("endElement(" . $name . ") called\n");
		}

		$this->popCurrentSchemaTag();
	}

	protected function peekCurrentSchemaTag()
	{
				$keys = array_keys($this->schemasTagsStack);
		return end($this->schemasTagsStack[end($keys)]);
	}

	protected function popCurrentSchemaTag()
	{
				$keys = array_keys($this->schemasTagsStack);
		array_pop($this->schemasTagsStack[end($keys)]);
	}

	protected function pushCurrentSchemaTag($tag)
	{
				$keys = array_keys($this->schemasTagsStack);
		$this->schemasTagsStack[end($keys)][] = $tag;
	}

	protected function isExternalSchema()
	{
		return (sizeof($this->schemasTagsStack) > 1);
	}

	protected function isAlreadyParsed($filePath)
	{
		return isset($this->schemasTagsStack[$filePath]);
	}
}

/**
 * Utility class used for objects with vendor data.
 *
 * @package    propel.engine.database.transform
 */
class ObjectWithVendorSpecificData
{
	protected $object;
	protected $vendorType;

	public function __construct($object, $vendorType)
	{
		$this->object = $object;
		$this->vendorType = $vendorType;
	}

	public function isCompatible($type)
	{
		return ($this->vendorType == $type);
	}

	public function setVendorParameter($name, $value)
	{
		$this->object->setVendorParameter($name, $value);
	}
}
