<?php
// +----------------------------------------------------------------------+
// | PHP versions 4 and 5                                                 |
// +----------------------------------------------------------------------+
// | Copyright (c) 1998-2008 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.                                          |
// +----------------------------------------------------------------------+
// | Author: Christian Weiske <cweiske@php.net>                           |
// +----------------------------------------------------------------------+
//
// $Id: Tool.php,v 1.3 2008/02/06 23:13:51 ifeghali Exp $
//

require_once 'MDB2/Schema.php';
require_once 'MDB2/Schema/Tool/ParameterException.php';

/**
* Command line tool to work with database schemas
*
* Functionality:
* - dump a database schema to stdout
* - import schema into database
* - create a diff between two schemas
* - apply diff to database
*
* @category Database
* @package  MDB2_Schema
* @author   Christian Weiske <cweiske@php.net>
* @license  http://www.opensource.org/licenses/bsd-license.php BSD License
* @link     http://pear.php.net/package/MDB2_Schema/
*/
class MDB2_Schema_Tool
{
    /**
    * Run the schema tool
    *
    * @param array $args Array of command line arguments
    */
    public function __construct($args)
    {
        $strAction = $this->getAction($args);
        try {
            $this->{'do' . ucfirst($strAction)}($args);
        } catch (MDB2_Schema_Tool_ParameterException $e) {
            $this->{'doHelp' . ucfirst($strAction)}($e->getMessage());
        }
    }//public function __construct($args)



    /**
    * Runs the tool with command line arguments
    *
    * @return void
    */
    public static function run()
    {
        $args = $GLOBALS['argv'];
        array_shift($args);

        try {
            $tool = new MDB2_Schema_Tool($args);
        } catch (Exception $e) {
            self::toStdErr($e->getMessage() . "\n");
        }
    }//public static function run()



    /**
    * Reads the first parameter from the argument array and
    * returns the action.
    *
    * @param array &$args Command line parameters
    *
    * @return string Action to execute
    */
    protected function getAction(&$args)
    {
        if (count($args) == 0) {
            return 'help';
        }
        $arg = array_shift($args);
        switch ($arg) {
        case 'd':
        case 'dump':
        case '-d':
        case '--dump':
            return 'dump';
        case 'l':
        case 'load':
        case '--l':
        case '--load':
            return 'load';
        case 'i':
        case 'diff':
        case '-i':
        case '--diff':
            return 'diff';
        case 'a':
        case 'apply':
        case '-a':
        case '--apply':
            return 'apply';
        default:
            throw new MDB2_Schema_Tool_ParameterException('Unknown mode "' . $arg . '"');
        }
    }//protected function getAction(&$args)



    /**
    * Writes the message to stderr
    *
    * @param string $msg Message to print
    *
    * @return void
    */
    protected static function toStdErr($msg)
    {
        file_put_contents('php://stderr', $msg);
    }//protected static function toStdErr($msg)



    /**
    * Displays generic help to stdout
    *
    * @return void
    */
    protected function doHelp()
    {
        self::toStdErr(<<<EOH
Usage: mdb2_schematool mode parameters

Works with database schemas

mode: (- and -- are optional)
 h,  help     Show this help screen
 d,  dump     Dump a schema to stdout
 l,  load     Load a schema into database
 i,  diff     Create a diff between two schemas and dump it to stdout
 a,  apply    Apply a diff to a database

EOH
        );
    }//protected function doHelp()



    /**
    * Displays the help screen for "dump" command
    *
    * @return void
    */
    protected function doHelpDump()
    {
        self::toStdErr( <<<EOH
Usage: mdb2_schematool dump [-p] DSN

Dumps a database schema to stdout

DSN: Data source name in the form of
 driver://user:password@host/database

User and password may be omitted.
Using -p reads password from stdin which is more secure than passing it in the parameter.

EOH
        );
    }//protected function doHelpDump()



    /**
    * Displays the help screen for "load" command
    *
    * @return void
    */
    protected function doHelpLoad()
    {
        self::toStdErr( <<<EOH
Usage: mdb2_schematool load [-p] source [-p] destination

Loads a database schema from source to destination
 (Creates the database schema at destination)

source can be a DSN or a schema file,
destination should be a DSN

DSN: Data source name in the form of
 driver://user:password@host/database

User and password may be omitted.
Using -p reads password from stdin which is more secure than passing it in the parameter.

EOH
        );
    }//protected function doHelpLoad()



    /**
    * Returns an array of options for MDB2_Schema constructor
    *
    * @return array Options for MDB2_Schema constructor
    */
    protected function getSchemaOptions()
    {
        $options = array(
            'log_line_break' => '<br>',
            'idxname_format' => '%s',
            'debug' => true,
            'quote_identifier' => true,
            'force_defaults' => false,
            'portability' => true
        );
        return $options;
    }//protected function getSchemaOptions()



    /**
    * Checks if the passed parameter is a PEAR_Error object
    * and throws an exception in that case.
    *
    * @param mixed  $object   Some variable to check
    * @param string $location Where the error occured
    *
    * @return void
    */
    protected function throwExceptionOnError($object, $location = '')
    {
        if (PEAR::isError($object)) {
            //FIXME: exception class
            //debug_print_backtrace();
            throw new Exception('Error ' . $location
                . "\n" . $object->getMessage()
                . "\n" . $object->getUserInfo()
            );
        }
    }//protected function throwExceptionOnError($object, $location = '')



    /**
    * Loads a file or a dsn from the arguments
    *
    * @param array &$args Array of arguments to the program
    *
    * @return array Array of ('file'|'dsn', $value)
    */
    protected function getFileOrDsn(&$args)
    {
        if (count($args) == 0) {
            throw new MDB2_Schema_Tool_ParameterException('File or DSN expected');
        }

        $arg = array_shift($args);
        if ($arg == '-p') {
            $bAskPassword = true;
            $arg          = array_shift($args);
        } else {
            $bAskPassword = false;
        }

        if (strpos($arg, '://') === false) {
            if (file_exists($arg)) {
                //File
                return array('file', $arg);
            } else {
                throw new Exception('Schema file does not exist');
            }
        }

        //read password if necessary
        if ($bAskPassword) {
            $password = $this->readPasswordFromStdin($arg);
            $arg      = self::setPasswordIntoDsn($arg, $password);
            self::toStdErr($arg);
        }
        return array('dsn', $arg);
    }//protected function getFileOrDsn(&$args)



    /**
    * Takes a DSN data source name and integrates the given
    * password into it.
    *
    * @param string $dsn      Data source name
    * @param string $password Password
    *
    * @return string DSN with password
    */
    protected function setPasswordIntoDsn($dsn, $password)
    {
        //simple try to integrate password
        if (strpos($dsn, '@') === false) {
            //no @ -> no user and no password
            return str_replace('://', '://:' . $password . '@', $dsn);
        } else if (preg_match('|://[^:]+@|', $dsn)) {
            //user only, no password
            return str_replace('@', ':' . $password . '@', $dsn);
        } else if (strpos($dsn, ':@') !== false) {
            //abstract version
            return str_replace(':@', ':' . $password . '@', $dsn);
        }

        return $dsn;
    }//protected function setPasswordIntoDsn($dsn, $password)



    /**
    * Reads a password from stdin
    *
    * @param string $dsn DSN name to put into the message
    *
    * @return string Password
    */
    protected function readPasswordFromStdin($dsn)
    {
        $stdin = fopen('php://stdin', 'r');
        self::toStdErr('Please insert password for ' . $dsn . "\n");
        $password = '';
        $breakme  = false;
        while (false !== ($char = fgetc($stdin))) {
            if (ord($char) == 10 || $char == "\n" || $char == "\r") {
                break;
            }
            $password .= $char;
        }
        fclose($stdin);

        return trim($password);
    }//protected function readPasswordFromStdin()



    /**
    * Creates a database schema dump and sends it to stdout
    *
    * @param array $args Command line arguments
    *
    * @return void
    */
    protected function doDump($args)
    {
        list($type, $dsn) = $this->getFileOrDsn($args);

        if ($type == 'file') {
            throw new MDB2_Schema_Tool_ParameterException(
                'Dumping a schema file as a schema file does not make much sense'
            );
        }

        $schema = MDB2_Schema::factory($dsn, $this->getSchemaOptions());
        $this->throwExceptionOnError($schema);

        $definition = $schema->getDefinitionFromDatabase();
        $this->throwExceptionOnError($definition);


        $dump_options = array(
            'output_mode' => 'file',
            'output' => 'php://stdout',
            'end_of_line' => "\r\n"
        );
        $op = $schema->dumpDatabase(
            $definition, $dump_options, MDB2_SCHEMA_DUMP_STRUCTURE
        );
        $this->throwExceptionOnError($op);

        $schema->disconnect();
    }//protected function doDump($args)



    /**
    * Loads a database schema
    *
    * @param array $args Command line arguments
    *
    * @return void
    */
    protected function doLoad($args)
    {
        list($typeSource, $dsnSource) = $this->getFileOrDsn($args);
        list($typeDest,   $dsnDest)   = $this->getFileOrDsn($args);

        if ($typeDest == 'file') {
            throw new MDB2_Schema_Tool_ParameterException(
                'A schema can only be loaded into a database, not a file'
            );
        }


        $schemaDest = MDB2_Schema::factory($dsnDest, $this->getSchemaOptions());
        $this->throwExceptionOnError($schemaDest);

        //load definition
        if ($typeSource == 'file') {
            $definition = $schemaDest->parseDatabaseDefinitionFile($dsnSource);
            $where      = 'loading schema file';
        } else {
            $schemaSource = MDB2_Schema::factory($dsnSource, $this->getSchemaOptions());
            $this->throwExceptionOnError($schemaSource, 'connecting to source database');

            $definition = $schemaSource->getDefinitionFromDatabase();
            $where      = 'loading definition from database';
        }
        $this->throwExceptionOnError($definition, $where);


        //create destination database from definition
        $simulate = false;
        $op       = $schemaDest->createDatabase($definition, array(), $simulate);
        $this->throwExceptionOnError($op, 'creating the database');
    }//protected function doLoad($args)


}//class MDB2_Schema_Tool

?>