<?php /* vim: se et ts=4 sw=4 sts=4 fdm=marker tw=80: */
/**
 * Copyright (c) 1998-2010 Manuel Lemos, Tomas V.V.Cox,
 * Stig. S. Bakken, Lukas Smith, Igor Feghali
 * All rights reserved.
 *
 * MDB2_Schema enables users to maintain RDBMS independant schema files
 * in XML that can be used to manipulate both data and database schemas
 * This LICENSE is in the BSD license style.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *
 * Neither the name of Manuel Lemos, Tomas V.V.Cox, Stig. S. Bakken,
 * Lukas Smith, Igor Feghali nor the names of his contributors may be
 * used to endorse or promote products derived from this software
 * without specific prior written permission.
 *
 * 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
 * REGENTS 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.
 *
 * PHP version 5
 *
 * @category Database
 * @package  MDB2_Schema
 * @author   Lukas Smith <smith@pooteeweet.org>
 * @author   Igor Feghali <ifeghali@php.net>
 * @license  BSD http://www.opensource.org/licenses/bsd-license.php
 * @version  SVN: $Id$
 * @link     http://pear.php.net/packages/MDB2_Schema
 */

/**
 * Writes an XML schema file
 *
 * @category Database
 * @package  MDB2_Schema
 * @author   Lukas Smith <smith@pooteeweet.org>
 * @license  BSD http://www.opensource.org/licenses/bsd-license.php
 * @link     http://pear.php.net/packages/MDB2_Schema
 */
class MDB2_Schema_Writer
{
    // {{{ properties

    var $valid_types = array();

    // }}}
    // {{{ constructor

    /**
     * PHP 5 constructor
     *
     * @param array $valid_types information of all valid fields 
     *                           types
     *
     * @return void
     *
     * @access public
     * @static
     */
    function __construct($valid_types = array())
    {
        $this->valid_types = $valid_types;
    }

    /**
     * PHP 4 compatible constructor
     *
     * @param array $valid_types information of all valid fields 
     *                           types
     *
     * @return void
     *
     * @access public
     * @static
     */
    function MDB2_Schema_Writer($valid_types = array())
    {
        $this->__construct($valid_types);
    }

    // }}}
    // {{{ raiseError()

    /**
     * This method is used to communicate an error and invoke error
     * callbacks etc.  Basically a wrapper for PEAR::raiseError
     * without the message string.
     *
     * @param int|PEAR_Error $code     integer error code or and PEAR_Error 
     *                                 instance
     * @param int            $mode     error mode, see PEAR_Error docs error
     *                                 level (E_USER_NOTICE etc).  If error mode
     *                                 is PEAR_ERROR_CALLBACK, this is the
     *                                 callback function, either as a function
     *                                 name, or as an array of an object and
     *                                 method name.  For other error modes this
     *                                 parameter is ignored.
     * @param string         $options  Extra debug information. Defaults to the
     *                                 last query and native error code.
     * @param string         $userinfo User-friendly error message
     *
     * @return object  a PEAR error object
     * @access  public
     * @see PEAR_Error
     */
    function &raiseError($code = null, $mode = null, $options = null, $userinfo = null)
    {
        $error = MDB2_Schema::raiseError($code, $mode, $options, $userinfo);
        return $error;
    }

    // }}}
    // {{{ _escapeSpecialChars()

    /**
     * add escapecharacters to all special characters in a string
     *
     * @param string $string string that should be escaped
     *
     * @return string escaped string
     * @access protected
     */
    function _escapeSpecialChars($string)
    {
        if (!is_string($string)) {
            $string = strval($string);
        }

        $escaped = '';
        for ($char = 0, $count = strlen($string); $char < $count; $char++) {
            switch ($string[$char]) {
            case '&':
                $escaped .= '&amp;';
                break;
            case '>':
                $escaped .= '&gt;';
                break;
            case '<':
                $escaped .= '&lt;';
                break;
            case '"':
                $escaped .= '&quot;';
                break;
            case '\'':
                $escaped .= '&apos;';
                break;
            default:
                $code = ord($string[$char]);
                if ($code < 32 || $code > 127) {
                    $escaped .= "&#$code;";
                } else {
                    $escaped .= $string[$char];
                }
                break;
            }
        }
        return $escaped;
    }

    // }}}
    // {{{ _dumpBoolean()

    /**
     * dump the structure of a sequence
     *
     * @param string $boolean boolean value or variable definition
     *
     * @return string with xml boolea definition
     * @access private
     */
    function _dumpBoolean($boolean)
    {
        if (is_string($boolean)) {
            if ($boolean !== 'true' || $boolean !== 'false'
                || preg_match('/<variable>.*</variable>/', $boolean)
            ) {
                return $boolean;
            }
        }
        return $boolean ? 'true' : 'false';
    }

    // }}}
    // {{{ dumpSequence()

    /**
     * dump the structure of a sequence
     *
     * @param string  $sequence_definition sequence definition
     * @param string  $sequence_name       sequence name
     * @param string  $eol                 end of line characters
     * @param integer $dump                determines what data to dump
     *                      MDB2_SCHEMA_DUMP_ALL       : the entire db
     *                      MDB2_SCHEMA_DUMP_STRUCTURE : only the structure of the db
     *                      MDB2_SCHEMA_DUMP_CONTENT   : only the content of the db
     *
     * @return mixed string xml sequence definition on success, or a error object
     * @access public
     */
    function dumpSequence($sequence_definition, $sequence_name, $eol, $dump = MDB2_SCHEMA_DUMP_ALL)
    {
        $buffer = "$eol <sequence>$eol  <name>$sequence_name</name>$eol";
        if ($dump == MDB2_SCHEMA_DUMP_ALL || $dump == MDB2_SCHEMA_DUMP_CONTENT) {
            if (!empty($sequence_definition['start'])) {
                $start   = $sequence_definition['start'];
                $buffer .= "  <start>$start</start>$eol";
            }
        }

        if (!empty($sequence_definition['on'])) {
            $buffer .= "  <on>$eol";
            $buffer .= "   <table>".$sequence_definition['on']['table'];
            $buffer .= "</table>$eol   <field>".$sequence_definition['on']['field'];
            $buffer .= "</field>$eol  </on>$eol";
        }
        $buffer .= " </sequence>$eol";

        return $buffer;
    }

    // }}}
    // {{{ dumpDatabase()

    /**
     * Dump a previously parsed database structure in the Metabase schema
     * XML based format suitable for the Metabase parser. This function
     * may optionally dump the database definition with initialization
     * commands that specify the data that is currently present in the tables.
     *
     * @param array   $database_definition unknown
     * @param array   $arguments           associative array that takes pairs of tag
     *              names and values that define dump options.
     *                 array (
     *                     'output_mode'    =>    String
     *                         'file' :   dump into a file
     *                         default:   dump using a function
     *                     'output'        =>    String
     *                         depending on the 'Output_Mode'
     *                                  name of the file
     *                                  name of the function
     *                     'end_of_line'        =>    String
     *                         end of line delimiter that should be used
     *                         default: "\n"
     *                 );
     * @param integer $dump                determines what data to dump
     *                      MDB2_SCHEMA_DUMP_ALL       : the entire db
     *                      MDB2_SCHEMA_DUMP_STRUCTURE : only the structure of the db
     *                      MDB2_SCHEMA_DUMP_CONTENT   : only the content of the db
     *
     * @return mixed MDB2_OK on success, or a error object
     * @access public
     */
    function dumpDatabase($database_definition, $arguments, $dump = MDB2_SCHEMA_DUMP_ALL)
    {
        if (!empty($arguments['output'])) {
            if (!empty($arguments['output_mode']) && $arguments['output_mode'] == 'file') {
                $fp = fopen($arguments['output'], 'w');
                if ($fp === false) {
                    return $this->raiseError(MDB2_SCHEMA_ERROR_WRITER, null, null,
                        'it was not possible to open output file');
                }

                $output = false;
            } elseif (is_callable($arguments['output'])) {
                $output = $arguments['output'];
            } else {
                return $this->raiseError(MDB2_SCHEMA_ERROR_WRITER, null, null,
                    'no valid output function specified');
            }
        } else {
            return $this->raiseError(MDB2_SCHEMA_ERROR_WRITER, null, null,
                'no output method specified');
        }

        $eol = isset($arguments['end_of_line']) ? $arguments['end_of_line'] : "\n";

        $sequences = array();
        if (!empty($database_definition['sequences'])
            && is_array($database_definition['sequences'])
        ) {
            foreach ($database_definition['sequences'] as $sequence_name => $sequence) {
                $table = !empty($sequence['on']) ? $sequence['on']['table'] :'';

                $sequences[$table][] = $sequence_name;
            }
        }

        $buffer  = '<?xml version="1.0" encoding="ISO-8859-1" ?>'.$eol;
        $buffer .= "<database>$eol$eol <name>".$database_definition['name']."</name>";
        $buffer .= "$eol <create>".$this->_dumpBoolean($database_definition['create'])."</create>";
        $buffer .= "$eol <overwrite>".$this->_dumpBoolean($database_definition['overwrite'])."</overwrite>$eol";
        $buffer .= "$eol <charset>".$database_definition['charset']."</charset>$eol";

        if ($output) {
            call_user_func($output, $buffer);
        } else {
            fwrite($fp, $buffer);
        }

        if (!empty($database_definition['tables']) && is_array($database_definition['tables'])) {
            foreach ($database_definition['tables'] as $table_name => $table) {
                $buffer = "$eol <table>$eol$eol  <name>$table_name</name>$eol";
                if ($dump == MDB2_SCHEMA_DUMP_ALL || $dump == MDB2_SCHEMA_DUMP_STRUCTURE) {
                    $buffer .= "$eol  <declaration>$eol";
                    if (!empty($table['fields']) && is_array($table['fields'])) {
                        foreach ($table['fields'] as $field_name => $field) {
                            if (empty($field['type'])) {
                                return $this->raiseError(MDB2_SCHEMA_ERROR_VALIDATE, null, null,
                                    'it was not specified the type of the field "'.
                                    $field_name.'" of the table "'.$table_name.'"');
                            }
                            if (!empty($this->valid_types) && !array_key_exists($field['type'], $this->valid_types)) {
                                return $this->raiseError(MDB2_SCHEMA_ERROR_UNSUPPORTED, null, null,
                                    'type "'.$field['type'].'" is not yet supported');
                            }
                            $buffer .= "$eol   <field>$eol    <name>$field_name</name>$eol    <type>";
                            $buffer .= $field['type']."</type>$eol";
                            if (!empty($field['fixed']) && $field['type'] === 'text') {
                                $buffer .= "    <fixed>".$this->_dumpBoolean($field['fixed'])."</fixed>$eol";
                            }
                            if (array_key_exists('default', $field)
                                && $field['type'] !== 'clob' && $field['type'] !== 'blob'
                            ) {
                                $buffer .= '    <default>'.$this->_escapeSpecialChars($field['default'])."</default>$eol";
                            }
                            if (!empty($field['notnull'])) {
                                $buffer .= "    <notnull>".$this->_dumpBoolean($field['notnull'])."</notnull>$eol";
                            } else {
                                $buffer .= "    <notnull>false</notnull>$eol";
                            }
                            if (!empty($field['autoincrement'])) {
                                $buffer .= "    <autoincrement>" . $field['autoincrement'] ."</autoincrement>$eol";
                            }
                            if (!empty($field['unsigned'])) {
                                $buffer .= "    <unsigned>".$this->_dumpBoolean($field['unsigned'])."</unsigned>$eol";
                            }
                            if (!empty($field['length'])) {
                                $buffer .= '    <length>'.$field['length']."</length>$eol";
                            }
                            $buffer .= "   </field>$eol";
                        }
                    }

                    if (!empty($table['indexes']) && is_array($table['indexes'])) {
                        foreach ($table['indexes'] as $index_name => $index) {
                            if (strtolower($index_name) === 'primary') {
                                $index_name = $table_name . '_pKey';
                            }
                            $buffer .= "$eol   <index>$eol    <name>$index_name</name>$eol";
                            if (!empty($index['unique'])) {
                                $buffer .= "    <unique>".$this->_dumpBoolean($index['unique'])."</unique>$eol";
                            }

                            if (!empty($index['primary'])) {
                                $buffer .= "    <primary>".$this->_dumpBoolean($index['primary'])."</primary>$eol";
                            }

                            foreach ($index['fields'] as $field_name => $field) {
                                $buffer .= "    <field>$eol     <name>$field_name</name>$eol";
                                if (!empty($field) && is_array($field)) {
                                    $buffer .= '     <sorting>'.$field['sorting']."</sorting>$eol";
                                }
                                $buffer .= "    </field>$eol";
                            }
                            $buffer .= "   </index>$eol";
                        }
                    }

                    if (!empty($table['constraints']) && is_array($table['constraints'])) {
                        foreach ($table['constraints'] as $constraint_name => $constraint) {
                            $buffer .= "$eol   <foreign>$eol    <name>$constraint_name</name>$eol";
                            if (empty($constraint['fields']) || !is_array($constraint['fields'])) {
                                return $this->raiseError(MDB2_SCHEMA_ERROR_VALIDATE, null, null,
                                    'it was not specified a field for the foreign key "'.
                                    $constraint_name.'" of the table "'.$table_name.'"');
                            }
                            if (!is_array($constraint['references']) || empty($constraint['references']['table'])) {
                                return $this->raiseError(MDB2_SCHEMA_ERROR_VALIDATE, null, null,
                                    'it was not specified the referenced table of the foreign key "'.
                                    $constraint_name.'" of the table "'.$table_name.'"');
                            }
                            if (!empty($constraint['match'])) {
                                $buffer .= "    <match>".$constraint['match']."</match>$eol";
                            }
                            if (!empty($constraint['ondelete'])) {
                                $buffer .= "    <ondelete>".$constraint['ondelete']."</ondelete>$eol";
                            }
                            if (!empty($constraint['onupdate'])) {
                                $buffer .= "    <onupdate>".$constraint['onupdate']."</onupdate>$eol";
                            }
                            if (!empty($constraint['deferrable'])) {
                                $buffer .= "    <deferrable>".$constraint['deferrable']."</deferrable>$eol";
                            }
                            if (!empty($constraint['initiallydeferred'])) {
                                $buffer .= "    <initiallydeferred>".$constraint['initiallydeferred']."</initiallydeferred>$eol";
                            }
                            foreach ($constraint['fields'] as $field_name => $field) {
                                $buffer .= "    <field>$field_name</field>$eol";
                            }
                            $buffer .= "    <references>$eol     <table>".$constraint['references']['table']."</table>$eol";
                            foreach ($constraint['references']['fields'] as $field_name => $field) {
                                $buffer .= "     <field>$field_name</field>$eol";
                            }
                            $buffer .= "    </references>$eol";

                            $buffer .= "   </foreign>$eol";
                        }
                    }

                    $buffer .= "$eol  </declaration>$eol";
                }

                if ($output) {
                    call_user_func($output, $buffer);
                } else {
                    fwrite($fp, $buffer);
                }

                $buffer = '';
                if ($dump == MDB2_SCHEMA_DUMP_ALL || $dump == MDB2_SCHEMA_DUMP_CONTENT) {
                    if (!empty($table['initialization']) && is_array($table['initialization'])) {
                        $buffer = "$eol  <initialization>$eol";
                        foreach ($table['initialization'] as $instruction) {
                            switch ($instruction['type']) {
                            case 'insert':
                                $buffer .= "$eol   <insert>$eol";
                                foreach ($instruction['data']['field'] as $field) {
                                    $field_name = $field['name'];

                                    $buffer .= "$eol    <field>$eol     <name>$field_name</name>$eol";
                                    $buffer .= $this->writeExpression($field['group'], 5, $arguments);
                                    $buffer .= "    </field>$eol";
                                }
                                $buffer .= "$eol   </insert>$eol";
                                break;
                            case 'update':
                                $buffer .= "$eol   <update>$eol";
                                foreach ($instruction['data']['field'] as $field) {
                                    $field_name = $field['name'];

                                    $buffer .= "$eol    <field>$eol     <name>$field_name</name>$eol";
                                    $buffer .= $this->writeExpression($field['group'], 5, $arguments);
                                    $buffer .= "    </field>$eol";
                                }

                                if (!empty($instruction['data']['where'])
                                    && is_array($instruction['data']['where'])
                                ) {
                                    $buffer .= "    <where>$eol";
                                    $buffer .= $this->writeExpression($instruction['data']['where'], 5, $arguments);
                                    $buffer .= "    </where>$eol";
                                }

                                $buffer .= "$eol   </update>$eol";
                                break;
                            case 'delete':
                                $buffer .= "$eol   <delete>$eol$eol";
                                if (!empty($instruction['data']['where'])
                                    && is_array($instruction['data']['where'])
                                ) {
                                    $buffer .= "    <where>$eol";
                                    $buffer .= $this->writeExpression($instruction['data']['where'], 5, $arguments);
                                    $buffer .= "    </where>$eol";
                                }
                                $buffer .= "$eol   </delete>$eol";
                                break;
                            }
                        }
                        $buffer .= "$eol  </initialization>$eol";
                    }
                }
                $buffer .= "$eol </table>$eol";
                if ($output) {
                    call_user_func($output, $buffer);
                } else {
                    fwrite($fp, $buffer);
                }

                if (isset($sequences[$table_name])) {
                    foreach ($sequences[$table_name] as $sequence) {
                        $result = $this->dumpSequence($database_definition['sequences'][$sequence],
                                                      $sequence, $eol, $dump);
                        if (PEAR::isError($result)) {
                            return $result;
                        }

                        if ($output) {
                            call_user_func($output, $result);
                        } else {
                            fwrite($fp, $result);
                        }
                    }
                }
            }
        }

        if (isset($sequences[''])) {
            foreach ($sequences[''] as $sequence) {
                $result = $this->dumpSequence($database_definition['sequences'][$sequence],
                                              $sequence, $eol, $dump);
                if (PEAR::isError($result)) {
                    return $result;
                }

                if ($output) {
                    call_user_func($output, $result);
                } else {
                    fwrite($fp, $result);
                }
            }
        }

        $buffer = "$eol</database>$eol";
        if ($output) {
            call_user_func($output, $buffer);
        } else {
            fwrite($fp, $buffer);
            fclose($fp);
        }

        return MDB2_OK;
    }

    // }}}
    // {{{ writeExpression()

    /**
     * Dumps the structure of an element. Elements can be value, column,
     * function or expression.
     *
     * @param array   $element   multi dimensional array that represents the parsed element
     *                           of a DML instruction.
     * @param integer $offset    base indentation width
     * @param array   $arguments associative array that takes pairs of tag
     *                           names and values that define dump options.
     *
     * @return string
     *
     * @access public
     * @see MDB2_Schema_Writer::dumpDatabase()
     */
    function writeExpression($element, $offset = 0, $arguments = null)
    {
        $eol = isset($arguments['end_of_line']) ? $arguments['end_of_line'] : "\n";
        $str = '';

        $indent  = str_repeat(' ', $offset);
        $noffset = $offset + 1;

        switch ($element['type']) {
        case 'value':
            $str .= "$indent<value>".$this->_escapeSpecialChars($element['data'])."</value>$eol";
            break;
        case 'column':
            $str .= "$indent<column>".$this->_escapeSpecialChars($element['data'])."</column>$eol";
            break;
        case 'function':
            $str .= "$indent<function>$eol$indent <name>".$this->_escapeSpecialChars($element['data']['name'])."</name>$eol";

            if (!empty($element['data']['arguments'])
                && is_array($element['data']['arguments'])
            ) {
                foreach ($element['data']['arguments'] as $v) {
                    $str .= $this->writeExpression($v, $noffset, $arguments);
                }
            }

            $str .= "$indent</function>$eol";
            break;
        case 'expression':
            $str .= "$indent<expression>$eol";
            $str .= $this->writeExpression($element['data']['operants'][0], $noffset, $arguments);
            $str .= "$indent <operator>".$element['data']['operator']."</operator>$eol";
            $str .= $this->writeExpression($element['data']['operants'][1], $noffset, $arguments);
            $str .= "$indent</expression>$eol";
            break;
        }
        return $str;
    }

    // }}}
}
