<?php

require_once 'xml/XmlReader.php';
require_once 'filesystem/FileSystemUtils.php';

/**
 * Map reader class.
 * 
 * This class creates and populates a <em>OrmMap</em>. The map is readed from 
 * an xml source, being a string or a filename. The data will be accesed only 
 * once, and all info will be stored in the <em>OrmMap</em>, so it is released 
 * after processing it, and also can be released this class.
 * 
 * @author  $Author: pruna $
 * @package orm
 * @version $Revision: 1.3 $
 * @created 02-Apr-2005
 * @modified $Date: 2005/12/06 02:56:45 $
 */
class OrmMapReader extends XmlReader {
    
    /**
     * @const 
     */
    const ATTRIB_NAME = 'name';
    
    /**
     * @const 
     */
    const ATTRIB_COLUMN = 'column';
    
    /**
     * @const 
     */
    const ATTRIB_UPDATE = 'update';
    
    /**
     * @const 
     */
    const ATTRIB_INSERT = 'insert';
    
    /**
     * @const 
     */
    const ATTRIB_FORMULA = 'formula';
    
    /**
     * @const 
     */
    const ATTRIB_PROPERTY_REF = 'property_ref';
    
    /**
     * @const 
     */
    const ATTRIB_LAZY = 'lazy';
    
    /**
     * @const 
     */
    const ATTRIB_EXTENDS = 'extends';
    
    /**
     * @const 
     */
    const ATTRIB_ALIAS = 'alias';
    
    /**
     * @const 
     */
    const ATTRIB_MAP = 'map';
    
    /**
     * @const 
     */
    const ATTRIB_MAP_EXTENDS = 'map_extends';
    
    /**
     * @const 
     */
    const ATTRIB_FETCH = 'fetch';
    
    /**
     * @const 
     */
    const ATTRIB_JOIN = 'join';
    
    /**
     * @const 
     */
    const ATTRIB_NULL = 'nullable';
    
    /**
     * @const 
     */
    const ATTRIB_CLASS = 'class';
    
    /**
     * @const 
     */
    const ATTRIB_TABLE = 'table';
    
    /**
     * @const 
     */
    const ATTRIB_CASCADE = 'cascade';
    
    /**
     * @const 
     */
    const ATTRIB_CASCADE_PIVOT = 'cascade_pivot';
    
    /**
     * @const 
     */
    const ATTRIB_PIVOT_TABLE = 'pivot_table';
    
    /**
     * @const 
     */
    const ATTRIB_PIVOT_COLUMN_PK = 'pivot_column_pk';
    
    /**
     * @const 
     */
    const ATTRIB_PIVOT_TO_LEFT = 'pivot_to_left';
    
    /**
     * @const 
     */
    const ATTRIB_PIVOT_TO_RIGHT = 'pivot_to_right';
    
    /**
     * @const 
     */
    const VALUE_ID = 'id';
    
    /**
     * @const 
     */
    const VALUE_SELECT = 'select';
    
    /**
     * @const 
     */
    const VALUE_JOIN = 'join';
    
    /**
     * Internal collection holder for possible paths where must look for map
     * definitions. Could be string (only for one path), or a collection.
     *
     * @var      mixed
     * @access   private
     */
    private $mapsPaths = null;

    /**
     * Returns possible paths where must look for map definitions.
     * 
     * @access  public
     * @return  mixed
     */
    public function getMapsPaths() {
        return $this->mapsPaths;
    }

    /**
     * Main method. Loads a xml map from source definition.
     * 
     * Recursively loads a full <em>OrmMap</em> from its XML definition.
     * It receives a Dsn object or string, pointing to xml source to read from.
     * There should be a <em>OrmReader</em> defined that be able to read from 
     * specified source. Basic ones are provided, and you can add any other you 
     * could need.
     * If you are using a file <em>Dsn</em>, you can set previously several 
     * other paths where look for this file, with <em>setMapsPaths</em>.
     * 
     * @param   mixed    $source  Source to read from, as Dsn object or string
     * @access  public
     * @return  OrmMap
     */
    public function loadMap($source, $map = null) {
        require_once 'dsn/Dsn.php';
        require_once 'dsn/DsnHandler.php';
        require_once 'orm/reader/OrmReaderFile.php';
        
        if (is_string($source)) {
            // 'file' is the default scheme here, if dsn passed as string.
            $dsn = new Dsn(array('scheme' => 'file'));
            $dsn->explode($source);
        } else {
            $dsn = $source;
        }
        
        $dsnHandler = new DsnHandler();
        $dsnHandler->addToHandlers(new OrmReaderFile());
        
        $handler = $dsnHandler->getHandlerFor($dsn);
        $xml = $handler->handle($dsn, $this);
        
        return $this->readMap($xml, $map);
    }
    
    /**
     * Main method. Loads a Map from a SimpleXml definition.
     * 
     * Recursively loads a full <em>OrmMap</em> from a XML definition.
     * It accepts a xml file name, or a SimpleXMLElement to read from.
     * If reading from a child SimpleXMLElement, can take a second 
     * parameter to get Map's name for external files.
     * @todo it might be useful to create special methods when you know the 
     * type of parameteres, to call them directly, and then avoid the logic 
     * in this method.
     * 
     * @param   mixed    $dsn         Source to read from, as Dsn object or string
     * @param   object   $mapElement  SimpleXMLElement object
     * @access  public
     * @return  OrmMap
     */
    protected function loadSubMap($cfg = null, $mapElement = null) {
        $beanName = null;
        if ((ObjectInspector::isA($cfg, 'SimpleXMLElement'))
                && (!is_null($mapElement))) {
            if (!isset($cfg->id)) {
                $cfg = $mapElement->getMap();
            }
            $beanName = $mapElement->getClass();
        }
        
        if (is_string($cfg)) {
            $file = simplexml_load_file(
                    FileSystemUtils::getFilePath($cfg, 
                    $this->getMapsPaths()));
            $cfg = $file->map;
        }
        
        return $this->readMap($cfg, null, $beanName);
    }
    
    /**
     * Internal main method. Loads a Map from a SimpleXml definition.
     * 
     * This method loads a full <em>OrmMap</em> from an XML definition.
     * If you pass a <em>map</em> parameter, it will add - and overwrite - 
     * what reads here to it.
     * 
     * @param   SimpleXMLElement    $xml        Xml to read from
     * @param   OrmMap              $map        OrmMap where will add data
     * @param   string              $beanName   Bean name
     * @access  protected
     * @return  OrmMap
     */
    function readMap($xml, $map = null, $beanName = null) {
        // if use 'exenteds', read it first
        if (!is_null($extends = $this->readAttr($xml, self::ATTRIB_MAP_EXTENDS, 
                self::TYPE_STRING, null))) {
            $this->loadMap($extends, $map);
        }
        
        if (is_null($map)) {
            // If creating a new map, sets default values to read
            // 'table', 'tableAlias' and 'id' are read only first time
            $map = new OrmMap();
            $defaultBeanName = (string)$xml[self::ATTRIB_CLASS];
            $map->setTable((string)$xml[self::ATTRIB_TABLE]);
            $map->setTableAlias(
                    $this->readAttr($xml, self::ATTRIB_ALIAS, 
                    self::TYPE_STRING, $map->getTable()));
            
            $map->setId($this->readId($xml->id, $map->getTableAlias()));
        } else {
            // If has a previous map, gets its values as defaults
            $defaultBeanName = (isset($xml[self::ATTRIB_CLASS]) ? 
                    (string)$xml[self::ATTRIB_CLASS] : $map->getBeanName());
        }
        
        $map->setBeanName(
                ($beanName ? $beanName : $defaultBeanName));
        
        foreach ($xml->property as $attr) {
            $map->addToProps($this->readProp($attr, $map->getTableAlias()));
        }
        
        foreach ($xml->one_to_one as $attr) {
            $map->addToO2os($this->readO2o($attr, $map->getTableAlias()));
        }
        
        foreach ($xml->one_to_many as $attr) {
            $map->addToO2ms($this->readO2m($attr, $map->getTableAlias(), 
                    $map->getId()->getName()));
        }
        
        foreach ($xml->many_to_one as $attr) {
            $map->addToM2os($this->readM2o($attr, $map->getTableAlias()));
        }
        
        foreach ($xml->many_to_many as $attr) {
            $map->addToM2ms($this->readM2m($attr, $map->getTableAlias(), 
                    $map->getId()->getName()));
        }
        
        foreach ($xml->group as $attr) {
            $this->readMap($attr, $map);
        }
        
        return $map;
    }
    
    /**
     * Reads an id element
     *
     * @param  SimpleXml $attr           Id element to read
     * @param  string    $qNamePrefix    qName prefix
     * @return OrmMapElementId
     */
    protected function readId($attr, $qNamePrefix) {
        $f = new OrmMapElementId();
        $f->setName(
                $this->readAttr($attr, self::ATTRIB_NAME, self::TYPE_STRING, 
                self::VALUE_ID));
        $f->setQName($qNamePrefix . '_' . $f->getName());
        $f->setColumn(
                $this->readAttr($attr, self::ATTRIB_COLUMN, self::TYPE_STRING, 
                $f->getName()));
        return $f;
    }
    
    /**
     * Reads a property element
     *
     * @param  SimpleXml $attr           Property element to read
     * @param  string    $qNamePrefix    qName prefix
     * @return OrmMapElementProp
     */
    protected function readProp($attr, $qNamePrefix) {
        $f = new OrmMapElementProp();
        $f->setName((string)$attr[self::ATTRIB_NAME]);
        $f->setQName($qNamePrefix . '_' . $f->getName());
        $f->setColumn(
                $this->readAttr($attr, self::ATTRIB_COLUMN, 
                self::TYPE_STRING, $f->getName()));
        $f->setUpdate(
                $this->readAttr($attr, self::ATTRIB_UPDATE, 
                self::TYPE_BOOLEAN, $f->isUpdate()));
        $f->setInsert(
                $this->readAttr($attr, self::ATTRIB_INSERT, 
                self::TYPE_BOOLEAN, $f->isInsert()));
        $f->setFormula(
                $this->readAttr($attr, self::ATTRIB_FORMULA, 
                self::TYPE_STRING));
        return $f;
    }
    
    /**
     * Reads a one_to_one element
     *
     * @param  SimpleXml $attr           O2o element to read
     * @param  string    $qNamePrefix    qName prefix
     * @return OrmMapElementO2o
     */
    protected function readO2o($attr, $qNamePrefix) {
        $f = new OrmMapElementO2o();
        $f->setName((string)$attr[self::ATTRIB_NAME]);
        $f->setQName($qNamePrefix . '_' . $f->getName());
        $f->setClass((string)$attr[self::ATTRIB_CLASS]);
        $f->setPropertyRef(
                $this->readAttr($attr, self::ATTRIB_PROPERTY_REF, 
                self::TYPE_STRING, self::VALUE_ID));
        $f->setFetch(
                $this->readAttrString($attr, self::ATTRIB_FETCH, 
                self::VALUE_SELECT, $this->getCleanupFunctions()));
       $f->setJoin(
                $this->readAttrString($attr, self::ATTRIB_JOIN, 
                self::VALUE_JOIN, $this->getCleanupFunctions()));
        $f->setColumn(
                $this->readAttr($attr, self::ATTRIB_COLUMN, 
                self::TYPE_STRING, $f->getName()));
        $f->setUpdate(
                $this->readAttr($attr, self::ATTRIB_UPDATE, 
                self::TYPE_BOOLEAN, $f->isUpdate()));
        $f->setInsert(
                $this->readAttr($attr, self::ATTRIB_INSERT, 
                self::TYPE_BOOLEAN, $f->isInsert()));
        $f->setNullable(
                $this->readAttr($attr, self::ATTRIB_NULL, 
                self::TYPE_BOOLEAN, $f->isNullable()));
        $f->setLazy(
                $this->readAttr($attr, self::ATTRIB_LAZY, 
                self::TYPE_BOOLEAN, $f->isLazy()));
        $f->setExtends(
                $this->readAttr($attr, self::ATTRIB_EXTENDS, 
                self::TYPE_BOOLEAN, $f->isExtends()));
        $f->setCascade($this->readAttrList($attr, self::ATTRIB_CASCADE));
        $f->setTable((string)$attr[self::ATTRIB_TABLE]);
        $f->setTableAlias(
                $this->readAttr($attr, self::ATTRIB_ALIAS, 
                self::TYPE_STRING, $f->getTable()));
        $f->setMap(
                $this->readAttr($attr, self::ATTRIB_MAP, 
                self::TYPE_STRING, $f->getMap()));
        $f->setFields($this->loadSubMap($attr, $f));
        return $f;
    }
    
    /**
     * Reads a one_to_many element
     *
     * @param  SimpleXml $attr           O2m element to read
     * @param  string    $qNamePrefix    qName prefix
     * @param  string    $defaultColumn  Default column name
     * @return OrmMapElementO2m
     */
    protected function readO2m($attr, $qNamePrefix, $defaultColumn) {
        $f = new OrmMapElementO2m();
        $f->setName((string)$attr[self::ATTRIB_NAME]);
        $f->setQName($qNamePrefix . '_' . $f->getName());
        $f->setClass((string)$attr[self::ATTRIB_CLASS]);
        $f->setTable((string)$attr[self::ATTRIB_TABLE]);
        $f->setTableAlias(
                $this->readAttr($attr, self::ATTRIB_ALIAS, 
                self::TYPE_STRING, $f->getTable()));
        $f->setColumn(
                $this->readAttr($attr, self::ATTRIB_COLUMN, 
                self::TYPE_STRING, $defaultColumn));
        $f->setPropertyRef((string)$attr[self::ATTRIB_PROPERTY_REF]);
        $f->setLazy(
                $this->readAttr($attr, self::ATTRIB_LAZY, 
                self::TYPE_BOOLEAN, $f->isLazy()));
        $f->setMap(
                $this->readAttr($attr, self::ATTRIB_MAP, 
                self::TYPE_STRING, $f->getMap()));
        $f->setFields($this->loadSubMap($attr, $f));
        $f->setCascade($this->readAttrList($attr, self::ATTRIB_CASCADE));
        
        // fix missing back-reference, if necesary
        if (($g = $this->getMissingBackReference($f, $qNamePrefix)) !== null) {
            $f->getFields()->addToProps($g);
        }
        return $f;
    }
    
    /**
     * This method checks that a back-reference was found in o2m relations.
     * 
     * If not found as explicit attribute, add it from <em>property_ref</em>.
     * Returns a new <em>OrmMapElementProp</em> or null.
     *
     * @param  OrmMapElementO2m  $f              OrmMapElementO2m in use
     * @param  string            $qNamePrefix    qName prefix
     * @return OrmMapElementProp
     */
    private function getMissingBackReference($f, $qNamePrefix) {
        $foundBackReference = FALSE;
        foreach ($f->getFields()->getProps() as $prop) {
            if ($prop->getColumn() == $f->getPropertyRef()) {
                $foundBackReference = TRUE;
                break;
            }
        }
        // if not found, add property manually
        if (!$foundBackReference) {
            $g = new OrmMapElementProp();
            $g->setName($f->getPropertyRef());
            $g->setQName($qNamePrefix . '_' . $g->getName());
            $g->setColumn($f->getPropertyRef());
            return $g;
        }
        return null;
    }
    
    /**
     * Reads a many_to_one element
     *
     * @param  SimpleXml $attr           M2o element to read
     * @param  string    $qNamePrefix    qName prefix
     * @return OrmMapElementM2o
     */
    protected function readM2o($attr, $qNamePrefix) {
        $f = new OrmMapElementM2o();
        $f->setName((string)$attr[self::ATTRIB_NAME]);
        $f->setQName($qNamePrefix . '_' . $f->getName());
        $f->setClass((string)$attr[self::ATTRIB_CLASS]);
        $f->setPropertyRef(
                $this->readAttr($attr, self::ATTRIB_PROPERTY_REF, 
                self::TYPE_STRING, self::VALUE_ID));
        $f->setFetch(
                $this->readAttrString($attr, self::ATTRIB_FETCH, 
                self::VALUE_SELECT, $this->getCleanupFunctions()));
        $f->setJoin(
                $this->readAttrString($attr, self::ATTRIB_JOIN, 
                self::VALUE_JOIN, $this->getCleanupFunctions()));
        $f->setColumn(
                $this->readAttr($attr, self::ATTRIB_COLUMN, 
                self::TYPE_STRING, $f->getName()));
        $f->setUpdate(
                $this->readAttr($attr, self::ATTRIB_UPDATE, 
                self::TYPE_BOOLEAN, $f->isUpdate()));
        $f->setInsert(
                $this->readAttr($attr, self::ATTRIB_INSERT, 
                self::TYPE_BOOLEAN, $f->isInsert()));
        $f->setNullable(
                $this->readAttr($attr, self::ATTRIB_NULL, 
                self::TYPE_BOOLEAN, $f->isNullable()));
        $f->setLazy(
                $this->readAttr($attr, self::ATTRIB_LAZY, 
                self::TYPE_BOOLEAN, $f->isLazy()));
        $f->setCascade($this->readAttrList($attr, self::ATTRIB_CASCADE));
        $f->setTable((string)$attr[self::ATTRIB_TABLE]);
        $f->setTableAlias(
                $this->readAttr($attr, self::ATTRIB_ALIAS, 
                self::TYPE_STRING, $f->getTable()));
        $f->setMap(
                $this->readAttr($attr, self::ATTRIB_MAP, 
                self::TYPE_STRING, $f->getMap()));
        $f->setFields($this->loadSubMap($attr, $f));
        return $f;
    }
    
    /**
     * Reads a many_to_many element
     *
     * @param  SimpleXml $attr           M2m element to read
     * @param  string    $qNamePrefix    qName prefix
     * @param  string    $defaultColumn  Default column name
     * @return OrmMapElementM2m
     */
    protected function readM2m($attr, $qNamePrefix, $defaultColumn) {
        $f = new OrmMapElementM2m();
        $f->setName((string)$attr[self::ATTRIB_NAME]);
        $f->setQName($qNamePrefix . '_' . $f->getName());
        $f->setTable((string)$attr[self::ATTRIB_TABLE]);
        $f->setTableAlias(
                $this->readAttr($attr, self::ATTRIB_ALIAS, 
                self::TYPE_STRING, $f->getTable()));
        $f->setClass((string)$attr[self::ATTRIB_CLASS]);
        $f->setPropertyRef(
                $this->readAttr($attr, self::ATTRIB_PROPERTY_REF, 
                self::TYPE_STRING, self::VALUE_ID));
        $f->setColumn(
                $this->readAttr($attr, self::ATTRIB_COLUMN, 
                self::TYPE_STRING, $defaultColumn));
        $f->setUpdate(
                $this->readAttr($attr, self::ATTRIB_UPDATE, 
                self::TYPE_BOOLEAN, $f->isUpdate()));
        $f->setInsert(
                $this->readAttr($attr, self::ATTRIB_INSERT, 
                self::TYPE_BOOLEAN, $f->isInsert()));
        $f->setLazy(
                $this->readAttr($attr, self::ATTRIB_LAZY, 
                self::TYPE_BOOLEAN, $f->isLazy()));
        $f->setCascade($this->readAttrList($attr, self::ATTRIB_CASCADE));
        $f->setCascadePivot(
                $this->readAttrList($attr, self::ATTRIB_CASCADE_PIVOT));
        $f->setPivotTable((string)$attr[self::ATTRIB_PIVOT_TABLE]);
        $f->setPivotColumnPk(
                $this->readAttr($attr, self::ATTRIB_PIVOT_COLUMN_PK, 
                self::TYPE_STRING, $f->getPivotColumnPk()));
        $f->setPivotToLeft((string)$attr[self::ATTRIB_PIVOT_TO_LEFT]);
        $f->setPivotToRight((string)$attr[self::ATTRIB_PIVOT_TO_RIGHT]);
        $f->setMap(
                $this->readAttr($attr, self::ATTRIB_MAP, 
                self::TYPE_STRING, $f->getMap()));
        $f->setFields($this->loadSubMap($attr, $f));
        return $f;
    }
    
    /**
     * Sets possible paths where must look for map definitions.
     *
     * @param   mixed    $paths         Paths, as string or collection
     * @access  public
     */
    public function setMapsPaths($paths) {
        $this->mapsPaths = $paths;
    }
    
}

?>