<?php
define('idDiGIR_config','$Id: DiGIR_config.php,v 1.7 2004/05/14 23:57:34 rdg Exp $');
if (defined('idDIGIR_VERSION_REQUEST'))
    return;

/*
$Id: DiGIR_config.php,v 1.7 2004/05/14 23:57:34 rdg Exp $
*/
require_once('DiGIR_globals.php');
require_once('DiGIR_utils.php');
require_once('DiGIR_flt.php');

/**
* class ConfigBuilder
*
*  Reads a configuration file and returns a configuration object.
*/
class ConfigBuilder
{
    var $Config;
    var $inTags;
    var $getXml;
    var $xmlStr;
    
    function ConfigBuilder($path)
    {
        $this->Config = NULL;
        $this->inTags = array();
        $this->loadConfig($path);
    }

    function startElement($parser, $name, $attrs)
    {
        $errlevel = error_reporting(E_ERROR | E_PARSE);

        array_push($this->inTags, strtolower($name));

        if (strcasecmp($name,'DATASOURCE') == 0)
        {
            if (strcasecmp($attrs['type'],'Z3950') == 0)
            {
                $this->Config = new Z3950Config();
                $this->Config->m_target = $attrs['target'];
                $this->Config->m_port = $attrs['port'];
                $this->Config->m_dbName = $attrs['database'];
            }
            elseif (strcasecmp($attrs['type'],'SQL') ==0)
            {
                $this->Config = new SQLConfig();
                $this->Config->setParams($attrs);
            }
            else
            {
                $this->Config = NULL;
            }
        }
        if (in_array('defaultformats', $this->inTags) and strcasecmp($name,'format') == 0)
        {
            if (isset($attrs['type']) && isset($attrs['location']))
            {
                $this->Config->defaultFormats[$attrs['type']] = $attrs['location'];
            }
        }
        if (strcasecmp($name,'CONCEPTS') == 0)
        {
            foreach ($attrs as $k=>$v)
            {
                if (strpos($k,'xmlns') == 0)
                {
                    $this->Config->addNameSpace($k,$v);
                }
            }
        }
        if (strcasecmp($name,'CONCEPT') == 0)
        {
            //split to element name and namespace
            $tmp = split(':',$attrs['name']);
            $ename = "";
            $namespace = "";
            if (count($tmp) > 1)
            {
                $ename = $tmp[1];
                $namespace = $this->Config->m_nameSpaces[$tmp[0]];
            }
            else
            {
                $ename = $attrs['name'];
            }
            $this->Config->addConcept($ename,
                            $attrs['type'],
                            $attrs['zid'],
                            strtolower($attrs['table']),
                            strtolower($attrs['field']),
                            $namespace,
                            $attrs['searchable'],
                            $attrs['returnable']);
        }
        elseif (strcasecmp($name,'TABLE')==0)
        {
            $aname = isset($attrs['name'])?strtolower($attrs['name']):'';
            $akey = isset($attrs['key'])?strtolower($attrs['key']):'';
            $ajoin = isset($attrs['join'])?strtolower($attrs['join']):'';
            $this->Config->StartTable($aname,$akey,$ajoin);
        }
        elseif (strcasecmp($name,'FILTER') == 0)
        {
            //since the filter element is parsed later in a different way 
            //(using Xpath), we need to "rebuild" its xml representation...
            $this->getXml = true;
        }
        
        if (strcasecmp($name,'MINQUERYTERMLENGTH') == 0)
        {
            $this->Config->minQueryTermLength =
                isset($attrs['v'])?$attrs['v']:g_MinTextLength;
        }
        if (strcasecmp($name,'MAXSEARCHRESPONSERECORDS') == 0)
        {
            $this->Config->maxResponseRecords =
                isset($attrs['v'])?$attrs['v']:DIGIR_ABS_RECORD_LIMIT;
        }
        if (strcasecmp($name,'MAXINVENTORYRESPONSERECORDS') == 0)
        {
            $this->Config->maxInventoryRecords =
                isset($attrs['v'])?$attrs['v']:DIGIR_ABS_RECORD_LIMIT;
        }
        if (strcasecmp($name,'DEFAULTRECORDFORMAT') == 0)
        {
            if (isset($attrs['v']))
            {
                $this->Config->defaultFormats['search'] = $attrs['v'];
            }
        }
        if (strcasecmp($name,'DEFAULTINVENTORYCONCEPT') == 0)
        {
            if (isset($attrs['v']))
            {
                $this->Config->defaultFormats['scan'] = $attrs['v'];
            }
        }

        if ($this->getXml)
        {
            $this->xmlStr .= '<'.$name;
            foreach ($attrs as $attName => $attVal)
            {
                $this->xmlStr .= ' '.$attName.'="'.$attVal.'"';
            }
            $this->xmlStr .= '>';
        }

        error_reporting($errlevel);
    }

    function endElement($parser, $name)
    {
         if ($this->getXml)
         {
            $this->xmlStr .= '</'.$name.'>';
         }

        if (strcasecmp($name,'TABLE') == 0)
        {
            $this->Config->EndTable();
        }
        elseif (strcasecmp($name,'FILTER') == 0)
        {
            $this->Config->SetFilter($this->xmlStr);

            $this->getXml = false;
            $this->xmlStr = '';
        }

        array_pop($this->inTags);
    }

    function characterData($parser, $data)
    {
      if (strlen(trim($data)))
      {
        $inTag = strtoupper($this->inTags[count($this->inTags)-1]);

        if ($inTag == 'MINQUERYTERMLENGTH')
        {
	  $this->Config->minQueryTermLength = trim($data);
        }
        if ($inTag == 'MAXSEARCHRESPONSERECORDS')
        {
	  $this->Config->maxResponseRecords = trim($data);
        }
        if ($inTag == 'MAXINVENTORYRESPONSERECORDS')
        {
	  $this->Config->maxInventoryRecords = trim($data);
        }
        if ($inTag == 'DEFAULTRECORDFORMAT')
        {
	  $this->Config->defaultFormats['search'] = trim($data);
        }
        if ($inTag == 'DEFAULTINVENTORYCONCEPT')
        {
	  $this->Config->defaultFormats['scan'] = trim($data);
        }
      }

      if ($this->getXml)
      {
         $this->xmlStr .= $data;
      }
    }

    function loadConfig($configFileName)
    {
        $this->parser = xml_parser_create();
        xml_parser_set_option($this->parser,XML_OPTION_CASE_FOLDING,0);
        xml_set_object($this->parser, $this);
        xml_set_element_handler($this->parser, 'startElement', 'endElement');
        xml_set_character_data_handler($this->parser, 'characterData');

        if (!($fp = fopen($configFileName, 'r')))
        {
            appendError(DC_NORESOURCESFILE,
            "Configuration data for resource could not be loaded.",
            DIAG_FATAL);
            $this->parser = NULL;
            $this->Config = NULL;
            return;
        }
        while ($data = fread($fp, 4096))
        {
            if (!xml_parse($this->parser, $data, feof($fp)))
            {
                die(sprintf("XML error: %s at line %d",
                xml_error_string(xml_get_error_code($xml_parser)),
                xml_get_current_line_number($xml_parser)));
            }
        }
        xml_parser_free($this->parser);
        fclose($fp);
    }
}

/**
* class DBConfig
*
* Implements a database connector.  The database connector defines the mapping
* between a federation schema and a database resource.
*/
class DBConfig
{
    //Concepts is an array of arrays.
    //Each array element = [0]type, [1]dbtable, [2]dbField, [3]name, [4]id
    var $m_concepts;
    var $m_tables;
    var $m_cTable;
    var $m_nameSpaces;
    var $defaultFormats;
    var $minQueryTermLength;
    var $maxResponseRecords;
    var $maxInventoryRecords;

    function DBConfig()
    {
        $this->m_concepts = array();
        $this->m_tables = array();
        $this->m_cTable = NULL;
        $this->m_nameSpaces = array();
        $this->defaultFormats = array('search'=>'','scan'=>'');
        $this->minQueryTermLength = g_MinTextLength;
        $this->maxResponseRecords = DIGIR_ABS_RECORD_LIMIT;
        $this->maxInventoryRecords = DIGIR_ABS_RECORD_LIMIT;
    }

    function Dump($bHTML=FALSE)
    {
        if ($bHTML)
        {
            echo "<h4>Tables</h4>\n";
            echo "<table border='1'><tr><th>Name</th><th>Key Field</th><th>Parent</th><th>Parent Key</th></tr>\n";
            foreach ($this->m_tables as $nm => $tb)
            {
                echo "<tr><td>$nm</td>";
                echo '<td>'.$tb->m_key.'</td>';
                echo '<td>'.$tb->m_parentName.'</td>';
                echo '<td>'.$tb->m_parentKey.'</td>';
                echo "</tr>\n";
            }
            echo "</table>\n";

            echo "<h4>Name Spaces</h4>\n";
            echo "<table border='1'><tr><th>Name</th><th>Space</th></tr>\n";
            foreach ($this->m_nameSpaces as $nm=>$sp)
            {
                echo "<tr><td>$nm</td><td>$sp</td></tr>\n";
            }
            echo "</table>\n";
    //[0] = type
    //[1] = table
    //[2] = field
    //[3] = name
    //[4] = zid
    //[5] = namespace
            echo "<h4>Concepts</h4>\n";
            echo "<table border='1'>\n";
            echo "<tr><th>Name</th><th>NameSpace</th>";
            echo "<th>Source Table</th><th>Source Field</th>";
            echo "<th>Meta type</th><th>ZID</th></tr>\n";
            foreach ($this->m_concepts as $key=>$c)
            {
                echo "<tr>";
                echo "<td>".$c[3]."</td>";
                echo "<td>".$c[5]."</td>";
                echo "<td>".$c[1]."</td>";
                echo "<td>".$c[2]."</td>";
                echo "<td>".typeToTypeName($c[0])."</td>";
                echo "<td>".$c[4]."</td>";
                echo "</tr>";
            }
            echo "</table>\n";
        }
        else
        {
            echo "<p>Name Spaces</p><pre><![CDATA[";
            for ($i=0; $i<count($this->m_nameSpaces); $i++)
            {
                echo $this->m_nameSpaces[$i][0]." = ".$this->m_nameSpaces[$i][1]."\n";
            }
            echo "]]></pre>";
            echo "<p>Concepts:</p><pre><![CDATA[";
            for ($i=0; $i<count($this->m_concepts); $i++)
            {
                  $v = $this->m_concepts[$i];
                  echo "[$i] ".$v[0].":".$v[1]." ".$v[2]." ".$v[3]." ".$v[4]."\n";
             }
             echo "]]></pre>";
            echo "<p>Tables (".count($this->m_tables)."):</p><pre><![CDATA[";
            $keys = array_keys($this->m_tables);
            for ($i=0; $i<count($keys); $i++)
            {
                  $v = $this->m_tables[$keys[$i]];
                  if ($v != NULL)
                      echo $v->Dump();
            }
            echo "]]></pre>";
        }
    }

    //name is the name of the concept in the schema
    //type is the type of data represented by the concept
    //id is the z39.50 profile access point id
    //table is the name of the table containing the data
    //field is the name of the field in the table containing the data
    //[0] = type
    //[1] = table
    //[2] = field
    //[3] = name
    //[4] = zid
    //[5] = namespace
    //[6] = boolean searchable?
    //[7] = boolean returnable?
    function addConcept($name,$type,$id,$table,$field,$namespace,$search,$ret)
    {
        $searchable = (boolean) $search;
        $returnable = (boolean) $ret;
        $t = typeNameToType($type);
        $a = array($t,$table,$field,$name,(int)$id,$namespace,$searchable,$returnable);
        array_push($this->m_concepts,$a);
    }

    function addNameSpace($name,$space)
    {
        if (strcasecmp($name,"xmlns") == 0)
            $this->m_nameSpaces[0] = $space;
        else
        {
            $tmp = split(":",$name);
            $this->m_nameSpaces[$tmp[1]] = $space;
        }
    }

    //returns the index of the concept that matches the specified z39.50 ID
    //or -1 if no match
    function getConceptByZid($zid)
    {
        $n = count($this->m_concepts);
        for ($i=0; $i < $n; $i++)
        {
            if ($this->m_concepts[$i][4] == $zid)
            {
                return $i;
            }
        }
        return -1;
    }

    /**
    * Returns the index of the concept that matches the specified concept name
    * or -1 if no match.  If a namespace is provided, then the comparison is
    * on the full name (name and namespace concatenated).  Note that all string
    * comparisons are case insensitive.
    *
    * @param string $name The concept name to locate
    * @param string $namespace The namespace of the $name parameter
    */
    function getConceptByName($name,$namespace="")
    {
        $cmp = $name.':'.$namespace;
        foreach ($this->m_concepts as $key => $concept)
        {
            $strcmp = $concept[3].':'.$concept[5];
            if (strcasecmp($cmp,$strcmp) == 0)
            {
                return $key;
            }
        }
        return -1;
    }

    /**
    * returns a reference to the concept identified by the index id.
    */
    function & getConcept($id)
    {
        return $this->m_concepts[$id];
    }

    // These two methods are just declared here, you will find the 
    // implementation in the SQLConfig derived class
    function StartTable($name, $keyField, $parentJoinField)
    {
    }

    function EndTable()
    {
    }
}

//=========================================================

class Z3950Config extends DBConfig
{
    var $m_target;
    var $m_port;
    var $m_database;

    function Z3950Config()
    {
        DBConfig::DBConfig();
        $m_target = "127.0.0.1";
        $m_port = "210";
        $m_database = "default";
    }
}

//=========================================================

class SQLTable
{
    var $m_name;
    var $m_key;
    var $m_parentName;
    var $m_parentKey;

    function SQLTable()
    {
        $this->m_name = '';
        $this->m_key = '';
        $this->m_parentName = '';
        $this->m_parentKey = '';
    }

    function Dump()
    {
        $res = "name:".$this->m_name."\n";
        $res .= "  key:".$this->m_key."\n";
        $res .= "  parentName:".$this->m_parentName."\n";
        $res .= "  parentKey:".$this->m_parentKey."\n";
        return $res;
    }
}

//=========================================================


class SQLConfig extends DBConfig
{
    var $connectionString;
    var $connectionType;
    var $connectionUID;
    var $connectionPWD;
    var $connectionDB;
    var $connectionEncoding;
    var $m_filter;

    function SQLConfig()
    {
        DBConfig::DBConfig();
        $this->connectionString = "";
        $this->connectionType="";
        $this->connectionUID="";
        $this->connectionPWD="";
        $this->connectionDB="";
        $this->connectionEncoding="";
    }

    /**
    * Returns the key field for the specified table.
    * If $tbl is null, then the key field of the root table is returned.
    */
    function tableKeyFieldName($tbl=NULL)
    {
        if ($tbl == NULL)
        {
            //find the root table, the one with no parent.
            foreach ($this->m_tables as $ctbl)
            {
                if ($ctbl->m_parentName == NULL)
                    return $ctbl->m_key;
            }
            return NULL;
        }
        return $this->m_tables[$tbl]->m_key;
    }

    /**
    * Returns the name of the root table
    */
    function RootTableName()
    {
        $k = array_keys($this->m_tables);
        return $this->m_tables[$k[0]]->m_name;
    }

    function setParams($attrs)
    {
        DebugMsg($attrs);
        $this->connectionString = $attrs["constr"];
        $this->connectionType = $attrs["dbtype"];
        $this->connectionUID = $attrs["uid"];
        $this->connectionPWD = $attrs["pwd"];
        $this->connectionDB = $attrs["database"];
        $this->connectionEncoding = $attrs["encoding"];
    }

    function Dump($bHTML=FALSE)
    {
        DBConfig::Dump($bHTML);
        
        if (!$bHTML)
        {
            echo "<SQLConfig>";
	   	    echo "<concepts><![CDATA[";
	   	    print_r($this->m_concepts);
            echo "]]></concepts>";
            echo "<tables><![CDATA[";
            print_r($this->m_tables);
            echo "]]></tables>";
            echo "<namespaces><![CDATA[";
            print_r($this->m_nameSpaces);
            echo "]]></namespaces>";
            echo "<cs>".$this->connectionString."</cs>";
            echo "<uid>".$this->connectionUID."</uid>";
            echo "<pwd>".$this->connectionPWD."</pwd>";
            echo "<db>".$this->connectionDB."</db>";
            echo "</SQLConfig>";
            echo "<encoding>".$this->connectionEncoding."</encoding>";
         }
         else
         {
            echo "<h4>Database Connection Parameters</h4>\n";
            echo "<table border='1'>\n";
            echo '<tr><td>constr</td><td>'.$this->connectionString.'</td></tr>';
            echo '<tr><td>UID</td><td>'.$this->connectionUID.'</td></tr>';
            echo '<tr><td>PWD</td><td>'.$this->connectionPWD.'</td></tr>';
            echo '<tr><td>DBType</td><td>'.$this->connectionDB.'</td></tr>';
            echo '<tr><td>Encoding</td><td>'.$this->connectionEncoding.'</td></tr>';
            echo '</table>';
         }
    }

    function StartTable($name, $keyField, $parentJoinField)
    {
        //$keyField = $keyField;
        //$parentJoinField = $parentJoinField;
        if (strlen($name) > 0)
        {
            $tbl = new SQLTable();
            $tbl->m_name = $name;
            $tbl->m_key = $keyField;
            if ($this->m_cTable == NULL)
            {
            }
            else
            {
                $tbl->m_parentName = $this->m_cTable;
                //$tbl->m_parentKey = $this->m_cTable->m_key;
                $tbl->m_parentKey = $parentJoinField;
            }
            $this->m_tables[$name] = $tbl;
            $this->m_cTable = $name;
        }
    }

    function EndTable()
    {
        if ($this->m_cTable)
        {
            $ctab = $this->m_tables[$this->m_cTable];
            $this->m_cTable = $ctab->m_parentName;
        }
    }

    function SetFilter($filterStr)
    {
        $this->m_filter = $filterStr;
    }

    function IsSelectableType($typ)
    {
        switch ($typ)
        {
            case type_BBOUNDING:
                return false;
            case type_JULIANDAY:
                return false;
        }
        return true;
    }


    //generates the select field list - basically just enumerates the fields in the root table
    function SELECTfields()
    {
        $ntables = count($this->m_tables);
        $keys = array_keys($this->m_tables);

        //the root table is *always* the zeroth table in the table list.
        $rootName = $this->m_tables[$keys[0]]->m_name;
        $flds = "";
        if ($ntables > 1)
            $flds = "DISTINCT ";
        //$flds .= "[".$rootName."].[".$this->m_tables[$keys[0]]->m_key."]";
        $flds .= $rootName.".".$this->m_tables[$keys[0]]->m_key;
        $aRootConcepts = array();
        foreach ($this->m_concepts as $ky => $concept)
        {
            if (strcasecmp($concept[1],$rootName) == 0)
            {
                if (strcasecmp($this->m_tables[$keys[0]]->m_key,$concept[2]) != 0)
                {
                    //check if this concept is selectable and
                    //returnable.
                    if ($this->IsSelectableType($concept[0]) && ($concept[7]))
                        array_push($aRootConcepts,array($concept[1],$concept[2]));
                }
            }
        }
        foreach ($aRootConcepts as $concept)
        {
           $flds .= ", ".$concept[0].".".$concept[1];
        }
        return $flds;
    }

    function WMSSELECTfields()
    {
        //now type=0, dbtable=1, dbField=2, name=3, id=4
        $fldLon = $this->m_concepts[$this->getConceptByZid(21)]; //21 = ZID for longitude
        $fldLat = $this->m_concepts[$this->getConceptByZid(22)]; //22 = latitude
        $res = $fldLat[1].".".$fldLat[2].", ".$fldLon[1].".".$fldLon[2];
        return $res;
    }

    //generate a FROM clause given the list of table names
    /**
    This needs to be extended tow work with AND operations on concepts
    within the same field.
    1. get concepts that appear on opposite sides of AND or ANDNOT LOPs
    2. for each coincidence ->

    mult as mt_1 right join (...) ON root.rowID = mt_1.rowID
    

    */
    function FROMclause()
    {
        $keys = array_keys($this->m_tables);
        $ntab = 0;
        for ($i=0; $i < count($keys); $i++)
        {
            if ($this->m_tables[$keys[$i]] !== NULL)
                $ntab ++;
        }
        $res = "";
        if ($ntab == 1)
        {
            $res .= $this->m_tables[$keys[0]]->m_name;
        }
        else
        {
            $res .= $this->m_tables[$keys[0]]->m_name;      		
            $res .= "\n";
            for ($i=1; $i<$ntab; $i++)
            {
                $res = "(".$res;
                $res .= " LEFT JOIN ";
                $res .= $this->m_tables[$keys[$i]]->m_name;
                $res .= " ON ";
                $res .= $this->m_tables[$keys[$i]]->m_parentName.".".$this->m_tables[$keys[$i]]->m_parentKey;
                $res .= " = ";
                $res .= $this->m_tables[$keys[$i]]->m_name.".".$this->m_tables[$keys[$i]]->m_key;
                $res .= ")\n";
            }
        }
        $res = " FROM ".$res;
        return $res;
    }

    /**
    * Returns the ORDER BY portion of a SQL statement.
    * @param aConcepts = array of
    */
    //Each array element = [0]type, [1]dbtable, [2]dbField, [3]name, [4]id
    function ORDERBYclause(&$Cfg,$concept,$ascdesc)
    {
        if (is_null($concept) || (strlen($concept) < 1) )
        {
            $keys = array_keys($this->m_tables);
            $rootName = $this->m_tables[$keys[0]]->m_name;
            $res = " ORDER BY ";
            $res .= $rootName.".".$this->m_tables[$keys[0]]->m_key;
            $res .= " ASC";
            return $res;
        }
        $res = "";
        $id = $Cfg->getConceptByName($concept);
        if (($id >= 0) && ($this->IsSelectableType($this->m_concepts[$id][0])))
        {
            $res = " ORDER BY ";
            $res .= $this->m_concepts[$id][1].".".$this->m_concepts[$id][2];
            $res .= " ".$ascdesc;
        }
        return $res;
    }

    /**
    * Returns the portion of a SQL statement related to a local filter.
    */
    function localFilter()
    {
        $res = '';
        if ($this->m_filter)
        {
            # Fix me? 
            # (should we start using namespaces in the config file to deal with filters?)
            $namespaces = array();

            $isLocalFilter = true;
            $flt = new filter($isLocalFilter);
            $flt->parse($this->m_filter, $namespaces);
            $res = $flt->toSQL($this);
        }
        return $res;
    }
}


?>
