<?php
/**
 * Magento
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is bundled with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://opensource.org/licenses/osl-3.0.php
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@magento.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade Magento to newer
 * versions in the future. If you wish to customize Magento for your
 * needs please refer to http://www.magento.com for more information.
 *
 * @category    Mage
 * @package     Mage_Connect
 * @copyright  Copyright (c) 2006-2014 X.commerce, Inc. (http://www.magento.com)
 * @license    http://opensource.org/licenses/osl-3.0.php  Open Software License (OSL 3.0)
 */

/**
 * Class to work with Magento Connect packages
 *
 * @category    Mage
 * @package     Mage_Connect
 * @author      Magento Core Team <core@magentocommerce.com>
 */
class Mage_Connect_Package
{

    const PACKAGE_XML_DIR = 'var/package';

    /**
     * Contain SimpleXMLElement for composing document.
     *
     * @var SimpleXMLElement
     */
    protected $_packageXml;

    /**
     * Internal cache
     *
     * @var array
     */
    protected $_authors;

    /**
     * Internal cache
     *
     * @var array
     */
    protected $_contents;

    /**
     * Internal cache
     *
     * @var array
     */
    protected $_hashContents;

    /**
     * Internal cache
     *
     * @var array
     */
    protected $_compatible;

    /**
     * Internal cache
     *
     * @var array
     */
    protected $_dependencyPhpExtensions;

    /**
     * Internal cache
     *
     * @var array
     */
    protected $_dependencyPackages;

    /**
     * A helper object that can read from a package archive
     *
     * @var Mage_Connect_Package_Reader
     */
    protected $_reader;

    /**
     * A helper object that can create and write to a package archive
     *
     * @var Mage_Connect_Package_Writer
     */
    protected $_writer;

    /**
     * Validator object
     *
     * @var Mage_Connect_Validator
     */
    protected $_validator = null;

    /**
     * Validation errors
     *
     * @var array
     */
    protected $_validationErrors = array();

    /**
    * Object with target
    *
    * @var Mage_Connect_Package_Target
    */
    protected $_target = null;

    /**
    * Config object
    *
    * @var Mage_Connect_Config
    */
    protected $_config = null;

    /**
     * Creates a package object (empty, or from existing archive, or from package definition xml)
     *
     * @param null|string|resource $source
     */
    public function __construct($source=null)
    {
        libxml_use_internal_errors(true);

        if (is_string($source)) {
            // check what's in the string (a package definition or a package filename)
            if (0 === strpos($source, "<?xml")) {
                // package definition xml
                $this->_init($source);
            } elseif (is_file($source) && is_readable($source)) {
                // package archive filename
                $this->_loadFile($source);
            } else {
                throw new Mage_Exception('Invalid package source');
            }
        } elseif (is_resource($source)) {
            $this->_loadResource($source);
        } elseif (is_null($source)) {
            $this->_init();
        } else {
            throw new Mage_Exception('Invalid package source');
        }
    }

    /**
     * Initializes an empty package object
     *
     * @param null|string $definition optional package definition xml
     * @return Mage_Connect_Package
     */
    protected function _init($definition=null)
    {

        if (!is_null($definition)) {
            $this->_packageXml = simplexml_load_string($definition);
        } else {
            $packageXmlStub = <<<END
<?xml version="1.0"?>
<package>
    <name />
    <version />
    <stability />
    <license />
    <channel />
    <extends />
    <summary />
    <description />
    <notes />
    <authors />
    <date />
    <time />
    <contents />
    <compatible />
    <dependencies />
</package>
END;
            $this->_packageXml = simplexml_load_string($packageXmlStub);
        }
        return $this;
    }

    /**
     * Loads a package from specified file
     *
     * @param string $filename
     * @return Mage_Connect_Package
     */
    protected function _loadFile($filename='')
    {
        if (is_null($this->_reader)) {
            $this->_reader = new Mage_Connect_Package_Reader($filename);
        }
        $content = $this->_reader->load();
        $this->_packageXml = simplexml_load_string($content);
        return $this;
    }

    /**
     * Creates a package and saves it
     *
     * @param string $path
     * @return Mage_Connect_Package
     */
    public function save($path)
    {
        $this->validate();
        $path = rtrim($path, "\\/") . DS;
        $this->_savePackage($path);
        return $this;
    }

    /**
     * Creates a package archive and saves it to specified path
     *
     * @param string $path
     * @return Mage_Connect_Package
     */
    protected function _savePackage($path)
    {
        $fileName = $this->getReleaseFilename();
        if (is_null($this->_writer)) {
            $this->_writer = new Mage_Connect_Package_Writer($this->getContents(), $path.$fileName);
        }
        $this->_writer
            ->composePackage()
            ->addPackageXml($this->getPackageXml())
            ->archivePackage();
        return $this;
    }

    /**
    * Retrieve Target object
    *
    * @return Mage_Connect_Package_Target
    */
    protected function getTarget()
    {
        if (!$this->_target instanceof Mage_Connect_Package_Target) {
            $this->_target = new Mage_Connect_Package_Target();
        }
        return $this->_target;
    }

    public function setTarget($arg)
    {
        if ($arg instanceof Mage_Connect_Package_Target) {
            $this->_target = $arg;
        }
    }

    /* Mutators */

    /**
     * Puts value to name
     *
     * @param string $name
     * @return Mage_Connect_Package
     */
    public function setName($name)
    {
        $this->_packageXml->name = $name;
        return $this;
    }

    /**
     * Puts value to <channel />
     *
     * @param string $channel
     * @return Mage_Connect_Package
     */
    public function setChannel($channel)
    {
        $this->_packageXml->channel = $channel;
        return $this;
    }

    /**
     * Puts value to <summary />
     *
     * @param string $summary
     * @return Mage_Connect_Package
     */
    public function setSummary($summary)
    {
        $this->_packageXml->summary = $summary;
        return $this;
    }

    /**
     * Puts value to <description />
     *
     * @param string $description
     * @return Mage_Connect_Package
     */
    public function setDescription($description)
    {
        $this->_packageXml->description = $description;
        return $this;
    }

    /**
     * Puts value to <authors />
     *
     * array(
     *     array('name'=>'Name1', 'user'=>'User1', 'email'=>'email1@email.com'),
     *     array('name'=>'Name2', 'user'=>'User2', 'email'=>'email2@email.com'),
     * );
     *
     * @param array $authors
     * @return Mage_Connect_Package
     */
    public function setAuthors($authors)
    {
        $this->_authors = $authors;
        foreach ($authors as $_author) {
            $this->addAuthor($_author['name'], $_author['user'], $_author['email']);
        }
        return $this;
    }

    /**
    * Add author to <authors/>
    *
    * @param string $name
    * @param string $user
    * @param string $email
    * @return Mage_Connect_Package
    */
    public function addAuthor($name=null, $user=null, $email=null)
    {
        $this->_authors[] = array(
            'name' =>$name,
            'user' =>$user,
            'email'=>$email
        );
        $author = $this->_packageXml->authors->addChild('author');
        $author->addChild('name', $name);
        $author->addChild('user', $user);
        $author->addChild('email', $email);
        return $this;
    }

    /**
     * Puts value to <date/>. Format should be Y-M-D.
     *
     * @param string $date
     * @return Mage_Connect_Package
     */
    public function setDate($date)
    {
        $this->_packageXml->date = $date;
        return $this;
    }

    /**
     * Puts value to <time />. Format should be H:i:s.
     *
     * @param string $time
     * @return Mage_Connect_Package
     */
    public function setTime($time)
    {
        $this->_packageXml->time = $time;
        return $this;
    }

    /**
     * Puts value to <version/>. Format should be X.Y.Z.
     *
     * @param string $version
     * @return Mage_Connect_Package
     */
    public function setVersion($version)
    {
        $this->_packageXml->version = $version;
        return $this;
    }

    /**
     * Puts value to <stability/>. It can be alpha, beta, devel and stable.
     *
     * @param string $stability
     * @return Mage_Connect_Package
     */
    public function setStability($stability)
    {
        $this->_packageXml->stability = $stability;
        return $this;
    }

    /**
     * Puts value to <license/>, also method can used for set attribute URI.
     *
     * @param string $license
     * @param string $uri
     * @return Mage_Connect_Package
     */
    public function setLicense($license, $uri=null)
    {
        $this->_packageXml->license = $license;
        if ($uri) {
            $this->_packageXml->license['uri'] = $uri;
        }
        return $this;
    }

    /**
     * Puts value to <notes/>.
     *
     * @param string $notes
     * @return Mage_Connect_Package
     */
    public function setNotes($notes)
    {
        $this->_packageXml->notes = $notes;
        return $this;
    }

    /**
    * Retrieve SimpleXMLElement node by xpath. If it absent, create new.
    * For comparing nodes method uses attribute "name" in each nodes.
    * If attribute "name" is same for both nodes, nodes are same.
    *
    * @param string $tag
    * @param SimpleXMLElement $parent
    * @param string $name
    * @return SimpleXMLElement
    */
    protected function _getNode($tag, $parent, $name='')
    {
        $found = false;
        foreach ($parent->xpath($tag) as $_node) {
            if ($_node['name'] == $name) {
                $node = $_node;
                $found = true;
                break;
            }
        }
        if (!$found) {
            $node = $parent->addChild($tag);
            if ($name) {
                $node->addAttribute('name', $name);
            }
        }
        return $node;
    }

    /**
     * Add directory or file to <contents />.
     *
     * @param string $path Path to directory or file
     * @param string $targetName Target name.
     * @param string $hash MD5 hash of the file
     * @return Mage_Connect_Package
     */
    public function addContent($path, $targetName)
    {
        $found = false;
        $parent = $this->_getNode('target', $this->_packageXml->contents, $targetName);
        $source = str_replace('\\', '/', $path);
        $directories = explode('/', dirname($source));
        foreach ($directories as $directory) {
            $parent = $this->_getNode('dir', $parent, $directory);
        }
        $fileName = basename($source);
        if ($fileName!='') {
            $fileNode = $parent->addChild('file');
            $fileNode->addAttribute('name', $fileName);
            $targetDir = $this->getTarget()->getTargetUri($targetName);
            $hash = md5_file($targetDir.DS.$path);
            $fileNode->addAttribute('hash', $hash);
        }
        return $this;
    }

    /**
     * Add directory recursively (with subdirectory and file).
     * Exclude and Include can be add using Regular Expression.
     *
     * @param string $targetName Target name
     * @param string $targetDir Path for target name
     * @param string $path Path to directory
     * @param string $exclude Exclude
     * @param string $include Include
     * @return Mage_Connect_Package
     */
    public function addContentDir($targetName, $path, $exclude=null, $include=null)
    {
        $targetDir = $this->getTarget()->getTargetUri($targetName);
        $targetDirLen = strlen($targetDir . DS);
        //get all subdirectories and files.
        $entries = @glob($targetDir. DS . $path . DS . "{,.}*", GLOB_BRACE);
        if (!empty($entries)) {
            foreach ($entries as $entry) {
                $filePath = substr($entry, $targetDirLen);
                if (!empty($include) && !preg_match($include, $filePath)) {
                    continue;
                }
                if (!empty($exclude) && preg_match($exclude, $filePath)) {
                    continue;
                }
                if (is_dir($entry)) {
                    $baseName = basename($entry);
                    if ('.'===$baseName || '..'===$baseName) {
                        continue;
                    }
                    //for subdirectory call method recursively
                    $this->addContentDir($targetName, $filePath, $exclude, $include);
                } elseif (is_file($entry)) {
                    $this->addContent($filePath, $targetName);
                }
            }
        }
        return $this;
    }

    /**
     * Add value to <compatible />.
     *
     * @param string $packageName
     * @param string $channel
     * @param string $minVersion
     * @param string $maxVersion
     * @return Mage_Connect_Package
     */
    public function addCompatible($packageName, $channel, $minVersion, $maxVersion)
    {
        $package = $this->_packageXml->compatible->addChild('package');
        $package->addChild('name', $packageName);
        $package->addChild('channel', $channel);
        $package->addChild('min', $minVersion);
        $package->addChild('max', $maxVersion);
        return $this;
    }

    /**
     * Set dependency from php version.
     *
     * @param string $minVersion
     * @param string $maxVersion
     * @return Mage_Connect_Package
     */
    public function setDependencyPhpVersion($minVersion, $maxVersion)
    {
        $parent = $this->_packageXml->dependencies;
        $parent = $this->_getNode('required', $parent);
        $parent = $this->_getNode('php', $parent);
        $parent->addChild('min', $minVersion);
        $parent->addChild('max', $maxVersion);
        return $this;
    }


    /**
     * Check PHP version restriction
     * @param $phpVersion PHP_VERSION by default
     * @return true | string
     */
    public function checkPhpVersion()
    {
        $min = $this->getDependencyPhpVersionMin();
        $max = $this->getDependencyPhpVersionMax();

        $minOk = $min? version_compare(PHP_VERSION, $min, ">=") : true;
        $maxOk = $max? version_compare(PHP_VERSION, $max, "<=") : true;

        if(!$minOk || !$maxOk) {
            $err = "requires PHP version ";
            if($min && $max) {
                $err .= " >= $min and <= $max ";
            } elseif($min) {
                $err .= " >= $min ";
            } elseif($max) {
                $err .=  " <= $max ";
            }
            $err .= " current is: ".PHP_VERSION;
            return $err;
        }
        return true;
    }


    /**
     * Check PHP extensions availability
     * @throws Exceptiom on failure
     * @return true | array
     */
    public function checkPhpDependencies()
    {
        $errors = array();
        foreach($this->getDependencyPhpExtensions() as $dep)
        {
            if(!extension_loaded($dep['name'])) {
                $errors[] = $dep;
            }
        }
        if(count($errors)) {
            return $errors;
        }
        return true;
    }


    /**
     * Set dependency from php extensions.
     *
     * $extension has next view:
     * array('curl', 'mysql')
     *
     * @param array|string $extensions
     * @return Mage_Connect_Package
     */
    public function setDependencyPhpExtensions($extensions)
    {
        foreach($extensions as $_extension) {
            $this->addDependencyExtension(
                $_extension['name'],
                $_extension['min_version'],
                $_extension['max_version']
            );
        }
        return $this;
    }

    /**
    * Set dependency from another packages.
    *
    * $packages should contain:
    * array(
    *     array('name'=>'test1', 'channel'=>'test1', 'min_version'=>'0.0.1', 'max_version'=>'0.1.0'),
    *     array('name'=>'test2', 'channel'=>'test2', 'min_version'=>'0.0.1', 'max_version'=>'0.1.0'),
    * )
    *
    * @param array $packages
    * @param bool $clear
    * @return Mage_Connect_Package
    */
    public function setDependencyPackages($packages, $clear = false)
    {
        if($clear) {
            unset($this->_packageXml->dependencies->required->package);
        }

        foreach($packages as $_package) {

            $filesArrayCondition = isset($_package['files']) && is_array($_package['files']);
            $filesArray = $filesArrayCondition ? $_package['files'] : array();

            $this->addDependencyPackage(
                $_package['name'],
                $_package['channel'],
                $_package['min_version'],
                $_package['max_version'],
                $filesArray
            );
        }
        return $this;
    }



    /**
     * Add package to dependency packages.
     *
     * @param string $package
     * @param string $channel
     * @param string $minVersion
     * @param string $maxVersion
     * @return Mage_Connect_Package
     */
    public function addDependencyPackage($name, $channel, $minVersion, $maxVersion, $files = array())
    {
        $parent = $this->_packageXml->dependencies;
        $parent = $this->_getNode('required', $parent);
        $parent = $parent->addChild('package');
        $parent->addChild('name', $name);
        $parent->addChild('channel', $channel);
        $parent->addChild('min', $minVersion);
        $parent->addChild('max', $maxVersion);
        if(count($files)) {
            $parent = $parent->addChild('files');
            foreach($files as $row) {
                if(!empty($row['target']) && !empty($row['path'])) {
                    $node = $parent->addChild("file");
                    $node["target"] = $row['target'];
                    $node["path"] =  $row['path'];

                }
            }
        }
        return $this;
    }



    /**
     * Add package to dependency extension.
     *
     * @param string $package
     * @param string $minVersion
     * @param string $maxVersion
     * @return Mage_Connect_Package
     */
    public function addDependencyExtension($name, $minVersion, $maxVersion)
    {
        $parent = $this->_packageXml->dependencies;
        $parent = $this->_getNode('required', $parent);
        $parent = $parent->addChild('extension');
        $parent->addChild('name', $name);
        $parent->addChild('min', $minVersion);
        $parent->addChild('max', $maxVersion);
        return $this;
    }

    /* Accessors */

    /**
     * Getter
     *
     * @return string
     */
    public function getName()
    {
        return (string)$this->_packageXml->name;
    }

    /**
     * Getter
     *
     * @return string
     */
    public function getChannel()
    {
        return (string)$this->_packageXml->channel;
    }

    /**
     * Getter
     *
     * @return string
     */
    public function getSummary()
    {
        return (string)$this->_packageXml->summary;
    }

    /**
     * Getter
     *
     * @return string
     */
    public function getDescription()
    {
        return (string)$this->_packageXml->description;
    }

    /**
     * Get list of authors in associative array.
     *
     * @return array
     */
    public function getAuthors()
    {
        if (is_array($this->_authors)) return $this->_authors;
        $this->_authors = array();
        if(!isset($this->_packageXml->authors->author)) {
            return array();
        }
        foreach ($this->_packageXml->authors->author as $_author) {
            $this->_authors[] = array(
                'name' => (string)$_author->name,
                'user' => (string)$_author->user,
                'email'=> (string)$_author->email
            );
        }
        return $this->_authors;
    }

    /**
     * Getter
     *
     * @return string
     */
    public function getDate()
    {
        return (string)$this->_packageXml->date;
    }

    /**
     * Getter
     *
     * @return string
     */
    public function getTime()
    {
        return (string)$this->_packageXml->time;
    }

    /**
     * Getter
     *
     * @return string
     */
    public function getVersion()
    {
        return (string)$this->_packageXml->version;
    }

    /**
     * Getter
     *
     * @return string
     */
    public function getStability()
    {
        return (string)$this->_packageXml->stability;
    }

    /**
     * Getter
     *
     * @return string
     */
    public function getLicense()
    {
        return (string)$this->_packageXml->license;
    }

    /**
     * Getter
     *
     * @return string
     */
    public function getLicenseUri()
    {
        return (string)$this->_packageXml->license['uri'];
    }

    /**
     * Getter
     *
     * @return string
     */
    public function getNotes()
    {
        return (string)$this->_packageXml->notes;
    }

    /**
     * Create list of all files from package.xml
     *
     * @return array
     */
    public function getContents()
    {
        if (is_array($this->_contents)) return $this->_contents;
        $this->_contents = array();
        if(!isset($this->_packageXml->contents->target)) {
            return $this->_contents;
        }
        foreach($this->_packageXml->contents->target as $target) {
            $targetUri = $this->getTarget()->getTargetUri($target['name']);
            $this->_getList($target, $targetUri);
        }
        return $this->_contents;
    }

    /**
    * Helper for getContents(). Create recursively list.
    *
    * @param SimpleXMLElement $parent
    * @param string $path
    */
    protected function _getList($parent, $path)
    {
        if (count($parent) == 0) {
            $this->_contents[] = $path;
        } else {
            foreach($parent as $_content) {
                $this->_getList($_content, ($path ? $path . DS : '')  . $_content['name']);
            }
        }
    }

    /**
     * Create list of all files from package.xml with hash
     *
     * @return array
     */
    public function getHashContents()
    {
        if (is_array($this->_hashContents)) return $this->_hashContents;
        $this->_hashContents = array();
        if(!isset($this->_packageXml->contents->target)) {
            return $this->_hashContents;
        }
        foreach($this->_packageXml->contents->target as $target) {
            $targetUri = $this->getTarget()->getTargetUri($target['name']);
            $this->_getHashList($target, $targetUri);
        }
        return $this->_hashContents;
    }

    /**
    * Helper for getHashContents(). Create recursively list.
    *
    * @param SimpleXMLElement $parent
    * @param string $path
    */
    protected function _getHashList($parent, $path, $hash='')
    {
        if (count($parent) == 0) {
            $this->_hashContents[$path] = $hash;
        } else {
            foreach($parent as $_content) {
                $contentHash = '';
                if (isset($_content['hash'])) {
                    $contentHash = (string)$_content['hash'];
                }
                $this->_getHashList($_content, ($path ? $path . DS : '')  . $_content['name'], $contentHash);
            }
        }
    }

    /**
     * Get compatible packages.
     *
     * @return array
     */
    public function getCompatible()
    {
        if (is_array($this->_compatible)) return $this->_compatible;
        $this->_compatible = array();
        if(!isset($this->_packageXml->compatible->package)) {
            return array();
        }
        foreach ($this->_packageXml->compatible->package as $_package) {
            $this->_compatible[] = array(
                'name'    => (string)$_package->name,
                'channel' => (string)$_package->channel,
                'min'     => (string)$_package->min,
                'max'     => (string)$_package->max
            );
        }
        return $this->_compatible;
    }

    /**
     * Getter
     *
     * @return string
     */
    public function getDependencyPhpVersionMin()
    {
        if(!isset($this->_packageXml->dependencies->required->php->min)) {
            return false;
        }
        return (string)$this->_packageXml->dependencies->required->php->min;
    }

    /**
     * Getter
     *
     * @return string
     */
    public function getDependencyPhpVersionMax()
    {
        if(!isset($this->_packageXml->dependencies->required->php->max)) {
            return false;
        }
        return (string)$this->_packageXml->dependencies->required->php->max;
    }

    /**
     * Get list of php extensions.
     *
     * @return array
     */
    public function getDependencyPhpExtensions()
    {
        if (is_array($this->_dependencyPhpExtensions)) return $this->_dependencyPhpExtensions;
        $this->_dependencyPhpExtensions = array();
        if (!isset($this->_packageXml->dependencies->required->extension)) {
            return $this->_dependencyPhpExtensions;
        }
        foreach($this->_packageXml->dependencies->required->extension as $_package) {
            $this->_dependencyPhpExtensions[] = array(
                'name'    => (string)$_package->name,
                'min'     => (string)$_package->min,
                'max'     => (string)$_package->max,
            );
        }
        return $this->_dependencyPhpExtensions;
    }

    /**
     * Get list of dependency packages.
     *
     * @return array
     */
    public function getDependencyPackages()
    {
        $this->_dependencyPackages = array();
        if (!isset($this->_packageXml->dependencies->required->package)) {
            return $this->_dependencyPackages;
        }
        foreach($this->_packageXml->dependencies->required->package as $_package) {
            $add = array(
                'name'    => (string)$_package->name,
                'channel' => (string)$_package->channel,
                'min'     => (string)$_package->min,
                'max'     => (string)$_package->max,
            );
            if(isset($_package->files)) {
                $add['files'] = array();
                foreach($_package->files as $node) {
                    if(isset($node->file)) {

                        $add['files'][] = array('target' => (string) $node->file['target'], 'path'=> (string) $node->file['path']);
                    }
                }
            }
            $this->_dependencyPackages[] = $add;
        }
        return $this->_dependencyPackages;
    }




    /**
     * Get string with XML content.
     *
     * @return string
     */
    public function getPackageXml()
    {
        return $this->_packageXml->asXml();
    }


    /**
     * Validator instance (single)
     *
     *  @return Mage_Connect_Validator
     */
    protected function validator()
    {
        if(is_null($this->_validator)) {
            $this->_validator = new Mage_Connect_Validator();
        }
        return $this->_validator;
    }

    /**
     * Get validation error strings
     *
     * @return array
     */
    public function getErrors()
    {
        return $this->_validationErrors;
    }

    /**
     * Setter for validation errors
     *
     * @param array $errors
     * @return
     */
    protected function setErrors(array $errors)
    {
        $this->_validationErrors = $errors;
    }

    /**
     * Check validation result.
     * Returns true if package data is invalid.
     *
     * @return bool
     */
    public function hasErrors()
    {
        return count($this->_validationErrors) != 0;
    }

    /**
     * Validate package. Errors can be
     * retreived by calling getErrors();
     *
     * @return bool
     */
    public function validate()
    {
        $v = $this->validator();

        /**
         * Validation map
         *
         * Format:
         *
         * 'key' =>  array(
         *    'method' => this class method name to call, string, required
         *    'method_args' => optional args for 'method' call, array, optional
         *    'v_method' => validator method to call, string, required
         *    'error' => custom error string when validation fails, optional
         *               if not set, error string fprmatted as "Invalid '$key' specified"
         *    'v_error_method' => validator method - when called returned error string
         *                        prepared by validator, optional,
         *                        if not set => see 'error'
         *    'optional' => optional value, if it's empty validation result ignored
         *
         */
        $validateMap = array(
           'name' => array('method' => 'getName',
                           'v_method' => 'validatePackageName',
                           'error'=>"Invalid package name, allowed: [a-zA-Z0-9_-] chars"),
           'version' => array('method' => 'getVersion',
                           'v_method' => 'validateVersion',
                           'error'=>"Invalid version, should be like: x.x.x"),
           'stability' => array('method' => 'getStability',
                           'v_method' => 'validateStability',
                           'error'=>"Invalid stability"),
           'date' => array('method' => 'getDate',
                           'v_method' => 'validateDate',
                           'error'=>"Invalid date, should be YYYY-DD-MM"),
           'license_uri' => array('method' => 'getLicenseUri',
                           'v_method' => 'validateLicenseUrl',
                           'error'=>"Invalid license URL"),
           'channel' => array('method' => 'getChannel',
                           'v_method' => 'validateChannelNameOrUri',
                           'error'=>"Invalid channel URL"),
           'authors' => array('method' => 'getAuthors',
                           'v_method' => 'validateAuthors',
                           'v_error_method' => 'getErrors'),
           'php_min' => array('method' => 'getDependencyPhpVersionMin',
                           'v_method' => 'validateVersion',
                           'error' => 'PHP minimum version invalid',
                           'optional' => true ),
           'php_max' => array('method' => 'getDependencyPhpVersionMax',
                           'v_method' => 'validateVersion',
                           'error' => 'PHP maximum version invalid',
                           'optional' => true ),
           'compatible' => array('method' => 'getCompatible',
                           'v_method' => 'validateCompatible',
                           'v_error_method' => 'getErrors'),
           'content' => array('method' => 'getContents',
                           'v_method' => 'validateContents',
                           'v_args' => array('config' => $this->getConfig()),
                           'v_error_method' => 'getErrors'),
        );

        $errors = array();
        /**
         * Iterate validation map
         */
         foreach($validateMap as $name=>$data) {

            /**
             * Check mandatory rules fields
             */
             if(!isset($data['method'], $data['v_method'])) {
             throw new Mage_Exception("Invalid rules specified!");
         }

            $method = $data['method'];
            $validatorMethod = $data['v_method'];

            /**
             * If $optional === false, value is mandatory
              */
            $optional = isset($data['optional']) ? (bool) $data['optional'] : false;

            /**
             * Check for method availability, package
             */
            if(!method_exists($this, $method)) {
                throw new Mage_Exception("Invalid method specified for Package : $method");
            }

            /**
             * Check for method availability, validator
             */
            if(!method_exists($v, $validatorMethod)) {
                throw new Mage_Exception("Invalid method specified for Validator : $validatorMethod");
            }

            /**
             * If    $data['error'] => get error string from $data['error']
             * Else  concatenate "Invalid '{$name}' specified"
             */
            $errorString = isset($data['error']) ? $data['error'] : "Invalid '{$name}' specified";

            /**
             * Additional method args check
             * array() by default
             */
            $methodArgs = isset($data['method_args']) ? $data['method_args'] : array();

            /**
             * Call package method
             */
            $out = @call_user_func_array(array($this, $method), $methodArgs);

            /**
             * Skip if result is empty and value is optional
             */
            if(empty($out) && $optional) {
               continue;
            }

            /**
            * Additional validator arguments, merged with array($out)
            */
            $validatorArgs = isset($data['v_args']) ? array_merge(array($out), $data['v_args']) : array($out);

            /**
             * Get validation result
             */
            $result = call_user_func_array(array($v, $validatorMethod), $validatorArgs);

            /**
             * Skip if validation success
             */
            if($result) {
                continue;
            }

            /**
             * From where to get error string?
             * If    validator callback method specified, call it to get errors array
             * Else  get it from $errorString - local error string
             */
            $validatorFetchErrorsMethod = isset($data['v_error_method']) ? $data['v_error_method'] : false;
            if (false !== $validatorFetchErrorsMethod) {
                $errorString = call_user_func_array(array($v, $validatorFetchErrorsMethod), array());
            }

            /**
             * If   errors is array => merge
             * Else append
             */
            if(is_array($errorString)) {
                $errors = array_merge($errors, $errorString);
            } else {
                $errors[] = $errorString;
            }
        }
        /**
         * Set local errors
         */
        $this->setErrors($errors);
        /**
         * Return true if there's no errors :)
         */
        return ! $this->hasErrors();
    }

    /**
     * Return package release filename w/o extension
     * @return string
     */
    public function getReleaseFilename()
    {
        return $this->getName()."-".$this->getVersion();
    }

    /**
     * Return release filepath w/o extension
     * @return string
     */
    public function getRelaseDirFilename()
    {
        return $this->getName() . DS . $this->getVersion() . DS . $this->getReleaseFilename();
    }

    /**
    * Clear dependencies
    *
    * @return Mage_Connect_Package
    */
    public function clearDependencies()
    {
        $this->_packageXml->dependencies = null;
        return $this;
    }

    /**
    * Clear contents
    *
    * @return Mage_Connect_Package
    */
    public function clearContents()
    {
        $this->_packageXml->contents = null;
        return $this;
    }

    /**
     * Get config
     *
     * @return Mage_Connect_Config
     */
    public function getConfig()
    {
        return $this->_config;
    }

    /**
     * Set config
     *
     * @param Mage_Connect_Config $config
     */
    public function setConfig($config)
    {
        $this->_config = $config;
    }

    /**
     * Import package information from previous version of Magento Connect Manager
     *
     * @param array $data
     *
     * @return Mage_Connect_Package
     */
    public function importDataV1x(array $data)
    {
        $this->_packageXml = null;
        $this->_init();
        // Import simple data
        if (isset($data['name'])) {
            $this->setName($data['name']);
        }
        if (isset($data['summary'])) {
            $this->setSummary($data['summary']);
        }
        if (isset($data['description'])) {
            $this->setDescription($data['description']);
        }
        if (isset($data['channel'])) {
            $this->setChannel($this->convertChannelFromV1x($data['channel']));
        }
        if (isset($data['license'])) {
            if (is_array($data['license'])) {
                $this->setLicense($data['license']['_content'], $data['license']['attribs']['uri']);
            } else {
                $this->setLicense($data['license']);
            }
        }
        if (isset($data['version'])) {
            $this->setVersion($data['version']['release']);
        }
        if (isset($data['stability'])) {
            $this->setStability($data['stability']['release']);
        }
        if (isset($data['notes'])) {
            $this->setNotes($data['notes']);
        }
        if (isset($data['date'])) {
            $this->setDate($data['date']);
        }
        if (isset($data['time'])) {
            $this->setTime($data['time']);
        }

        // Import authors
        $authors = array();
        $authorRoles = array('lead', 'developer', 'contributor', 'helper');
        foreach ($authorRoles as $authorRole) {
            if (isset($data[$authorRole])) {
                $authorList = $data[$authorRole];
                if (!is_array($authorList) || isset($authorList['name'])) {
                    $authorList = array($authorList);
                }
                foreach ($authorList as $authorRawData) {
                    $author = array();
                    $author['name'] = $authorRawData['name'];
                    $author['user'] = $authorRawData['user'];
                    $author['email'] = $authorRawData['email'];
                    array_push($authors, $author);
                }
            }
        }
        $this->setAuthors($authors);

        // Import dependencies
        $packages = array();
        $extensions = array();
        if (isset($data['dependencies']) && is_array($data['dependencies'])) {
            $dependencySections = array('required', 'optional');
            $elementTypes = array('package', 'extension');
            foreach ($dependencySections as $dependencySection) {
                if (isset($data['dependencies'][$dependencySection])) {
                    // Handle required PHP version
                    if ($dependencySection == 'required' && isset($data['dependencies']['required']['php'])) {
                        $this->setDependencyPhpVersion($data['dependencies']['required']['php']['min'], $data['dependencies']['required']['php']['max']);
                    }
                    // Handle extensions
                    if (isset($data['dependencies'][$dependencySection]['extension'])) {
                        $extensionList = $data['dependencies'][$dependencySection]['extension'];
                        if (!is_array($extensionList) || isset($extensionList['name'])) {
                            $extensionList = array($extensionList);
                        }
                        foreach ($extensionList as $extensionRawData) {
                            $extension = array();
                            $extension['name'] = $extensionRawData['name'];
                            $extension['min_version'] = isset($extensionRawData['min']) ? $extensionRawData['min'] : null;
                            $extension['max_version'] = isset($extensionRawData['max']) ? $extensionRawData['max'] : null;
                            array_push($extensions, $extension);
                        }
                    }
                    // Handle packages
                    if (isset($data['dependencies'][$dependencySection]['package'])) {
                        $packageList = $data['dependencies'][$dependencySection]['package'];
                        if (!is_array($packageList) || isset($packageList['name'])) {
                            $packageList = array($packageList);
                        }
                        foreach ($packageList as $packageRawData) {
                            $package = array();
                            $package['name'] = $packageRawData['name'];
                            $package['channel'] = $this->convertChannelFromV1x($packageRawData['channel']);
                            $package['min_version'] = isset($packageRawData['min']) ? $packageRawData['min'] : null;
                            $package['max_version'] = isset($packageRawData['max']) ? $packageRawData['max'] : null;
                            array_push($packages, $package);
                        }
                    }
                }
            }
        }
        $this->setDependencyPackages($packages);
        $this->setDependencyPhpExtensions($extensions);

        // Import contents
        if (isset($data['contents']) && is_array($data['contents']) && is_array($data['contents']['dir'])) {
            // Handle files
            $root = $data['contents']['dir'];
            if (isset($data['contents']['dir']['file'])) {
                $fileList = $data['contents']['dir']['file'];
                if (!is_array($fileList) || isset($fileList['attribs'])) {
                    $fileList = array($fileList);
                }
                foreach ($fileList as $fileRawData) {
                    $targetName = $fileRawData['attribs']['role'];
                    $parentTargetNode = $this->_getNode('target', $this->_packageXml->contents, $targetName);
                    $filePath = $fileRawData['attribs']['name'];
                    $filePathParts = explode('/', $filePath);
                    $fileName = array_pop($filePathParts);
                    $parentDirNode = null;
                    if (!empty($filePathParts)) {
                        $parentDirNode = $parentTargetNode;
                        foreach ($filePathParts as $directoryName) {
                            $parentDirNode = $this->_getNode('dir', $parentDirNode, $directoryName);
                        }
                    } else {
                        $parentDirNode = $this->_getNode('dir', $parentTargetNode, '.');
                    }
                    $fileNode = $parentDirNode->addChild('file');
                    $fileNode->addAttribute('name', $fileName);
                    $fileNode->addAttribute('hash', $fileRawData['attribs']['md5sum']);
                }
            }
        }

        return $this;
    }

    /**
     * Convert package channel in order for it to be compatible with current version of Magento Connect Manager
     *
     * @param string $channel
     *
     * @return string
     */
    public function convertChannelFromV1x($channel)
    {
        $channelMap = array(
            'connect.magentocommerce.com/community' => 'community',
            'connect.magentocommerce.com/core' => 'community'
        );
        if (!empty($channel) && isset($channelMap[$channel])) {
            $channel = $channelMap[$channel];
        }
        return $channel;
    }
}
