From 0164592b8c2ef73974303c4ec4bf254f05c270ef Mon Sep 17 00:00:00 2001 From: David Mudrak Date: Thu, 5 May 2011 02:21:23 +0200 Subject: [PATCH] MDL-27376 MDL-27377 Backup converters API refactored * Several base_converter methods made protected when there was no obvious reason why they should be public (subject of eventual change still). * The conversion chain now constructed in advance before any converter class is instantiated, using Dijkstra's algorithm. --- backup/backup.class.php | 2 + .../controller/restore_controller.class.php | 6 +- backup/converter/moodle1/converter.class.php | 54 ++-- .../moodle1/simpletest/testconverter.php | 15 +- backup/converter/moodle1/taskslib.php | 2 + .../util/converter/base_converter.class.php | 204 ++++++++++---- .../util/converter/plan_converter.class.php | 37 +-- .../util/factories/convert_factory.class.php | 17 -- .../helper/backup_general_helper.class.php | 44 ++- backup/util/helper/convert_helper.class.php | 261 +++++++++++++++++- .../helper/simpletest/testconverthelper.php | 146 ++++++++++ backup/util/plan/convert_plan.class.php | 2 +- .../backup/moodle1/convert_forum_stepslib.php | 1 + 13 files changed, 616 insertions(+), 175 deletions(-) create mode 100644 backup/util/helper/simpletest/testconverthelper.php diff --git a/backup/backup.class.php b/backup/backup.class.php index 31ae5b13715..936ff8a4e1c 100644 --- a/backup/backup.class.php +++ b/backup/backup.class.php @@ -40,6 +40,8 @@ abstract class backup implements checksumable { // Backup format const FORMAT_MOODLE = 'moodle2'; + const FORMAT_MOODLE1 = 'moodle1'; + const FORMAT_IMSCC = 'imscc'; const FORMAT_UNKNOWN = 'unknown'; // Interactive diff --git a/backup/controller/restore_controller.class.php b/backup/controller/restore_controller.class.php index 93806276314..c2078172d98 100644 --- a/backup/controller/restore_controller.class.php +++ b/backup/controller/restore_controller.class.php @@ -377,17 +377,17 @@ class restore_controller extends backup implements loggable { } /** - * convert from current format to backup::MOODLE format + * Converts from current format to backup::MOODLE format */ public function convert() { global $CFG; + require_once($CFG->dirroot . '/backup/util/includes/convert_includes.php'); if ($this->status != backup::STATUS_REQUIRE_CONV) { throw new restore_controller_exception('cannot_convert_not_required_status'); } - require_once($CFG->dirroot.'/backup/util/includes/convert_includes.php'); - // Run conversion until we have the proper format + // Run conversion to the proper format convert_helper::to_moodle2_format($this->get_tempdir(), $this->format); // If no exceptions were thrown, then we are in the proper format diff --git a/backup/converter/moodle1/converter.class.php b/backup/converter/moodle1/converter.class.php index 28a52271fbd..4395c103613 100644 --- a/backup/converter/moodle1/converter.class.php +++ b/backup/converter/moodle1/converter.class.php @@ -16,8 +16,6 @@ // along with Moodle. If not, see . /** - * All of the task and step classes specific to moodle1 conversion - * * @package core * @subpackage backup-convert * @copyright 2011 Mark Nielsen @@ -26,49 +24,47 @@ defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot.'/backup/converter/moodle1/taskslib.php'); -require_once($CFG->dirroot.'/backup/converter/moodle1/stepslib.php'); +require_once($CFG->dirroot . '/backup/converter/moodle1/taskslib.php'); +require_once($CFG->dirroot . '/backup/converter/moodle1/stepslib.php'); /** - * This will be the Moodle 1 to Moodle 2 Converter + * Converter of Moodle 1.9 backup into Moodle 2.x format */ class moodle1_converter extends plan_converter { - /** - * The current module being processed - * - * @var string - */ + + /** @var string the current module being processed */ protected $currentmod = ''; - /** - * The current block being processed - * - * @var string - */ + /** @var string the current block being processed */ protected $currentblock = ''; /** - * @return boolean + * Detects the Moodle 1.9 format of the backup directory + * + * @param string $tempdir the name of the backup directory + * @return null|string backup::FORMAT_MOODLE1 if the Moodle 1.9 is detected, null otherwise */ - public function can_convert() { - // Then look for MOODLE1 (moodle1) format - $filepath = $this->get_tempdir() . '/moodle.xml'; - if (file_exists($filepath)) { // Looks promising, lets load some information - $handle = fopen($filepath, "r"); - $first_chars = fread($handle,200); + public static function detect_format($tempdir) { + global $CFG; + + $filepath = $CFG->dataroot . '/temp/backup/' . $tempdir . '/moodle.xml'; + if (file_exists($filepath)) { + // looks promising, lets load some information + $handle = fopen($filepath, 'r'); + $first_chars = fread($handle, 200); fclose($handle); - // Check if it has the required strings - if (strpos($first_chars,'') !== false && - strpos($first_chars,'') !== false && + // check if it has the required strings + if (strpos($first_chars,'') !== false and + strpos($first_chars,'') !== false and strpos($first_chars,'') !== false) { - return true; + return backup::FORMAT_MOODLE1; } } - return false; - } + return null; + } /** * Path transformation for modules and blocks. Here we @@ -116,7 +112,7 @@ class moodle1_converter extends plan_converter { public function build_plan() { $this->xmlparser = new progressive_parser(); - $this->xmlparser->set_file($this->get_tempdir() . '/moodle.xml'); + $this->xmlparser->set_file($this->get_tempdir_path() . '/moodle.xml'); $this->xmlprocessor = new convert_structure_parser_processor($this); // @todo Probably move this $this->xmlparser->set_processor($this->xmlprocessor); diff --git a/backup/converter/moodle1/simpletest/testconverter.php b/backup/converter/moodle1/simpletest/testconverter.php index 20969ea4006..7c07530d83f 100644 --- a/backup/converter/moodle1/simpletest/testconverter.php +++ b/backup/converter/moodle1/simpletest/testconverter.php @@ -26,15 +26,13 @@ defined('MOODLE_INTERNAL') || die(); -require_once($CFG->dirroot.'/backup/util/includes/convert_includes.php'); +require_once($CFG->dirroot . '/backup/converter/moodle1/converter.class.php'); class moodle1_converter_test extends UnitTestCase { public static $includecoverage = array(); - /** - * @var string - */ + /** @var string the name of the directory containing the unpacked Moodle 1.9 backup */ protected $tempdir; public function setUp() { @@ -43,7 +41,7 @@ class moodle1_converter_test extends UnitTestCase { $this->tempdir = convert_helper::generate_id('simpletest'); check_dir_exists("$CFG->dataroot/temp/backup/$this->tempdir"); copy( - $CFG->dirroot.'/backup/converter/moodle1/simpletest/files/moodle.xml', + "$CFG->dirroot/backup/converter/moodle1/simpletest/files/moodle.xml", "$CFG->dataroot/temp/backup/$this->tempdir/moodle.xml" ); } @@ -55,10 +53,9 @@ class moodle1_converter_test extends UnitTestCase { } } - public function test_can_convert() { - $converter = convert_factory::converter('moodle1', $this->tempdir); - $this->assertIsA($converter, 'moodle1_converter'); - $this->assertTrue($converter->can_convert()); + public function test_detect_format() { + $detected = moodle1_converter::detect_format($this->tempdir); + $this->assertEqual(backup::FORMAT_MOODLE1, $detected); } public function test_convert() { diff --git a/backup/converter/moodle1/taskslib.php b/backup/converter/moodle1/taskslib.php index bd301ca5c36..a6dc80d9050 100644 --- a/backup/converter/moodle1/taskslib.php +++ b/backup/converter/moodle1/taskslib.php @@ -24,6 +24,8 @@ defined('MOODLE_INTERNAL') || die(); +require_once($CFG->dirroot . '/backup/util/includes/convert_includes.php'); + class moodle1_root_task extends convert_task { /** * Function responsible for building the steps of any task diff --git a/backup/util/converter/base_converter.class.php b/backup/util/converter/base_converter.class.php index 3ed93608617..364dcb2308c 100644 --- a/backup/util/converter/base_converter.class.php +++ b/backup/util/converter/base_converter.class.php @@ -25,95 +25,195 @@ defined('MOODLE_INTERNAL') || die(); /** - * Base Abstract Converter + * Base abstract converter * * @throws backup_exception|Exception|null */ abstract class base_converter { + /** @var string unique identifier of this converter instance */ protected $id; + /** @var string the name of the directory containing the unpacked backup being converted */ protected $tempdir; - protected $convertdir; + /** @var string the name of the directory where the backup is converted to */ + protected $workdir; - // do we want absolute path instead of tempdir? - // Do we need to create a new tempdir to convert into? EG: target... + /** + * Constructor + * + * @param string $tempdir the relative path to the directory containing the unpacked backup to convert + */ public function __construct($tempdir) { - $this->tempdir = $tempdir; - $this->convertdir = $this->tempdir.'_'.$this->get_name(); - $this->id = convert_helper::generate_id($this->convertdir); + + $this->tempdir = $tempdir; + $this->id = convert_helper::generate_id($this->workdir); + $this->workdir = $tempdir . '_' . $this->get_name() . '_' . $this->id; $this->init(); } - public function init() { - } - + /** + * Get instance identifier + * + * @return string the unique identifier of this converter instance + */ public function get_id() { return $this->id; } + /** + * Get converter name + * + * @return string the system name of the converter + */ public function get_name() { return array_shift(explode('_', get_class($this))); } - public function get_convertdir() { - global $CFG; - - return "$CFG->dataroot/temp/backup/$this->convertdir"; - } - - public function get_tempdir() { - global $CFG; - - return "$CFG->dataroot/temp/backup/$this->tempdir"; - } - - public function delete_convertdir() { - fulldelete($this->get_convertdir()); - } - - public function create_convertdir() { - $this->delete_convertdir(); - if (!check_dir_exists($this->get_convertdir())) { - throw new backup_exception('failedtomakeconvertdir'); // @todo Define this string - } - } - - public function replace_tempdir() { - fulldelete($this->get_tempdir()); - - if (!rename($this->get_convertdir(), $this->get_tempdir())) { - throw new backup_exception('failedmoveconvertedintoplace'); // @todo Define this string - } - } - /** - * @abstract - * @return boolean + * Converts the backup directory */ - abstract public function can_convert(); - - // Kicks things off public function convert() { - $e = NULL; + $e = null; + // try to execute the converter try { - $this->create_convertdir(); + $this->create_workdir(); $this->execute(); $this->replace_tempdir(); } catch (Exception $e) { } - // Do cleanup... + + // clean-up stuff if needed $this->destroy(); + // eventually re-throw the execution exception if ($e instanceof Exception) { throw $e; } } - abstract public function execute(); + /// public static methods ////////////////////////////////////////////////// - public function destroy() { - $this->delete_convertdir(); + /** + * Makes sure that this converter is available at this site + * + * This is intended for eventual PHP extensions check, environment check etc. + * All checks that do not depend on actual backup data should be done here. + * + * @return boolean true if this converter should be considered as available + */ + public static function is_available() { + return true; + } + + /** + * Detects the format of the backup directory + * + * Moodle 2.x format is being detected by the core itself. The converters are + * therefore supposed to detect the source format. Eventually, if the target + * format os not {@link backup::FORMAT_MOODLE} then they should be able to + * detect both source and target formats. + * + * @param string $tempdir the name of the backup directory + * @return null|string null if not recognized, backup::FORMAT_xxx otherwise + */ + public static function detect_format($tempdir) { + return null; + } + + /** + * Returns the basic information about the converter + * + * The returned array must contain the following keys: + * 'from' - the supported source format, eg. backup::FORMAT_MOODLE1 + * 'to' - the supported target format, eg. backup::FORMAT_MOODLE + * 'cost' - the cost of the conversion, non-negative non-zero integer + */ + public static function description() { + + return array( + 'from' => null, + 'to' => null, + 'cost' => null, + ); + } + + /// end of public API ////////////////////////////////////////////////////// + + /** + * Initialize the instance if needed, called by the constructor + */ + protected function init() { + } + + /** + * Converts the contents of the tempdir into the target format in the workdir + */ + protected abstract function execute(); + + /** + * @return string the full path to the working directory + */ + protected function get_workdir_path() { + global $CFG; + + return "$CFG->dataroot/temp/backup/$this->workdir"; + } + + /** + * @return string the full path to the directory with the source backup + */ + protected function get_tempdir_path() { + global $CFG; + + return "$CFG->dataroot/temp/backup/$this->tempdir"; + } + + /** + * Prepares a new empty working directory + */ + protected function create_workdir() { + + fulldelete($this->get_workdir_path()); + if (!check_dir_exists($this->get_workdir_path())) { + throw new backup_exception('failedtocreateworkdir'); + } + } + + /** + * Replaces the source backup directory with the converted version + * + * If $CFG->keeptempdirectoriesonbackup is defined, the original source + * source backup directory is kept for debugging purposes. + */ + protected function replace_tempdir() { + global $CFG; + + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($this->get_tempdir_path); + } else { + if (!rename($this->get_tempdir_path, $this->get_tempdir_path . '_' . $this->get_name() . '_' . $this->id . '_source')) { + throw new backup_exception('failedrenamesourcetempdir'); + } + } + + if (!rename($this->get_workdir_path(), $this->get_tempdir_path())) { + throw new backup_exception('failedmoveconvertedintoplace'); + } + } + + /** + * Cleans up stuff after the execution + * + * Note that we do not know if the execution was successful or not. + * An exception might have been thrown. + */ + protected function destroy() { + global $CFG; + + if (empty($CFG->keeptempdirectoriesonbackup)) { + fulldelete($this->get_workdir_path); + } } } diff --git a/backup/util/converter/plan_converter.class.php b/backup/util/converter/plan_converter.class.php index 00742e6321b..3301cc690b7 100644 --- a/backup/util/converter/plan_converter.class.php +++ b/backup/util/converter/plan_converter.class.php @@ -25,40 +25,31 @@ defined('MOODLE_INTERNAL') || die(); /** - * Plan based abstract converter + * Base class for all converters using plan/tasks/steps pattern * - * All backup converters that use {@link convert_plan} should extend this class. + * All converters that use {@link convert_plan} must extend this class. */ -abstract class plan_converter extends base_converter { +abstract class planned_converter extends base_converter { - /** - * @var convert_plan - */ + /** @var convert_plan */ protected $plan; - - /** - * @var progressive_parser - */ + /** @var progressive_parser */ protected $xmlparser; - - /** - * @var convert_structure_parser_processor - */ + /** @var convert_structure_parser_processor */ protected $xmlprocessor; + /** @var array path elements to process */ + protected $pathelements = array(); + /** @todo needed? redo? path currently locking processing of children */ + protected $pathlock; /** - * @var array + * Instructs the dispatcher to ignore all children below path processor returning it */ - protected $pathelements = array(); // Array of pathelements to process - - // @todo needed? redo? - protected $pathlock; // Path currently locking processing of children - - // @todo IDK what this is really... - const SKIP_ALL_CHILDREN = -991399; // To instruct the dispatcher about to ignore - // all children below path processor returning it + const SKIP_ALL_CHILDREN = -991399; /** + * Return the plan instance, instatinate it if it does not exist yet + * * @return convert_plan */ public function get_plan() { diff --git a/backup/util/factories/convert_factory.class.php b/backup/util/factories/convert_factory.class.php index 414674343cc..6dd6bbab19c 100644 --- a/backup/util/factories/convert_factory.class.php +++ b/backup/util/factories/convert_factory.class.php @@ -55,23 +55,6 @@ abstract class convert_factory { } return new $classname($tempdir); } - - /** - * Instantiates a list of all installed converters operating on a given directory - * - * @param string $tempdir The temp directory to operate on - * @return array - */ - public static function converters($tempdir) { - global $CFG; - - $converters = array(); - $plugins = get_list_of_plugins('backup/converter'); - foreach ($plugins as $name) { - $converters[$name] = self::converter($name, $tempdir); - } - return $converters; - } /** * Runs through all plugins of a specific type and instantiates their task class diff --git a/backup/util/helper/backup_general_helper.class.php b/backup/util/helper/backup_general_helper.class.php index 00c18fcbfac..4fd2d082e6b 100644 --- a/backup/util/helper/backup_general_helper.class.php +++ b/backup/util/helper/backup_general_helper.class.php @@ -228,42 +228,34 @@ abstract class backup_general_helper extends backup_helper { } /** - * Given one temp/backup/xxx dir, detect its format + * Detects the format of the given unpacked backup directory * - * TODO: Move harcoded detection here to delegated classes under backup/format (moodle1, imscc..) - * conversion code will be there too. + * @param string $tempdir the name of the backup directory + * @return string one of backup::FORMAT_xxx constants */ public static function detect_backup_format($tempdir) { global $CFG; - $filepath = $CFG->dataroot . '/temp/backup/' . $tempdir . '/moodle_backup.xml'; + require_once($CFG->dirroot . '/backup/util/includes/convert_includes.php'); - // Does tempdir exist and is dir - if (!is_dir(dirname($filepath))) { - throw new backup_helper_exception('tmp_backup_directory_not_found', dirname($filepath)); + if (convert_helper::detect_moodle2_format($tempdir)) { + return backup::FORMAT_MOODLE; } - // First look for MOODLE (moodle2) format - if (file_exists($filepath)) { // Looks promising, lets load some information - $handle = fopen ($filepath, "r"); - $first_chars = fread($handle,200); - $status = fclose ($handle); - // Check if it has the required strings - if (strpos($first_chars,'') !== false && - strpos($first_chars,'') !== false && - strpos($first_chars,'') !== false) { - return backup::FORMAT_MOODLE; + // see if a converter can identify the format + $converters = convert_factory::available_converters(); + foreach ($converters as $name) { + $classname = "{$name}_converter"; + if (!class_exists($classname)) { + throw new coding_exception("available_converters() is supposed to load + converter classes but class $classname not found"); + } + + $detected = call_user_func($classname .'::detect_format', $tempdir); + if (!empty($detected)) { + return $detected; } } - // See if a converter can identify the format as its own - $converters = convert_factory::converters($tempdir); - foreach ($converters as $name => $converter) { - if ($converter->can_convert()) { - return $name; - } - } - - // Arrived here, unknown format return backup::FORMAT_UNKNOWN; } } diff --git a/backup/util/helper/convert_helper.class.php b/backup/util/helper/convert_helper.class.php index 2ac07850e0b..86184618cdf 100644 --- a/backup/util/helper/convert_helper.class.php +++ b/backup/util/helper/convert_helper.class.php @@ -16,6 +16,8 @@ // along with Moodle. If not, see . /** + * Provides {@link convert_helper} class + * * @package core * @subpackage backup-convert * @copyright 2011 Mark Nielsen @@ -25,37 +27,129 @@ defined('MOODLE_INTERNAL') || die(); /** - * General convert helper + * Provides various functionality via its static methods */ abstract class convert_helper { + + /** + * @param string $entropy + * @return string random identifier + */ public static function generate_id($entropy) { return md5(time() . '-' . $entropy . '-' . random_string(20)); } /** - * @static + * Returns the list of all available converters and loads their classes + * + * Converter must be installed as a directory in backup/converter/ and its + * method is_available() must return true to get to the list. + * + * @see base_converter::is_available() + * @return array of strings + */ + public static function available_converters() { + global $CFG; + + $converters = array(); + $plugins = get_list_of_plugins('backup/converter'); + foreach ($plugins as $name) { + $classfile = "$CFG->dirroot/backup/converter/$name/converter.class.php"; + $classname = "{$name}_converter"; + + if (!file_exists($classfile)) { + throw new coding_exception("Converter factory error: class file not found $classfile"); + } + require_once($classfile); + + if (!class_exists($classname)) { + throw new coding_exception("Converter factory error: class not found $classname"); + } + + if (call_user_func($classname .'::is_available')) { + $converters[] = $name; + } + } + + return $converters; + } + + /** + * Detects if the given folder contains an unpacked moodle2 backup + * + * @param string $tempdir the name of the backup directory + * @return boolean true if moodle2 format detected, false otherwise + */ + public static function detect_moodle2_format($tempdir) { + global $CFG; + + $dirpath = $CFG->dataroot . '/temp/backup/' . $tempdir; + $filepath = $dirpath . '/moodle_backup.xml'; + + if (!is_dir($dirpath)) { + throw new backup_helper_exception('tmp_backup_directory_not_found', $dirpath); + } + + if (!file_exists($filepath)) { + return false; + } + + $handle = fopen($filepath, 'r'); + $firstchars = fread($handle, 200); + $status = fclose($handle); + + if (strpos($firstchars,'') !== false and + strpos($firstchars,'') !== false and + strpos($firstchars,'') !== false) { + return true; + } + + return false; + } + + /** + * Converts the given directory with the backup into moodle2 format + * * @throws coding_exception|restore_controller_exception * @param string $tempdir The directory to convert * @param string $format The current format, if already detected * @return void */ - public static function to_moodle2_format($tempdir, $format = NULL) { + public static function to_moodle2_format($tempdir, $format = null) { + if (is_null($format)) { $format = backup_general_helper::detect_backup_format($tempdir); } - while (!in_array($format, array(backup::FORMAT_MOODLE, backup::FORMAT_UNKNOWN))) { - $converter = convert_factory::converter($format, $tempdir); - if (!$converter->can_convert()) { - throw new coding_exception('Converter detection failed, the loaded converter cannot convert this format'); + // get the supported conversion paths from all available converters + $converters = convert_factory::available_converters(); + $descriptions = array(); + foreach ($converters as $name) { + $classname = "{$name}_converter"; + if (!class_exists($classname)) { + throw new coding_exception("available_converters() is supposed to load + converter classes but class $classname not found"); } - $converter->convert(); - - // Re-detect format - $format = backup_general_helper::detect_backup_format($tempdir); + $descriptions[$name] = call_user_func($classname .'::description'); } - if ($format == backup::FORMAT_UNKNOWN) { - throw new restore_controller_exception('cannot_convert_from_unknown_format'); // @todo Change exception class + + // choose the best conversion path for the given format + $path = self::choose_conversion_path($format, $descriptions); + + if (empty($path)) { + // unable to convert + // todo throwing exception is not a good way to control the flow here + throw new coding_exception('Unable to find conversion path'); + } + + foreach ($path as $name) { + $converter = convert_factory::converter($name, $tempdir); + $converter->convert(); + } + + // make sure we ended with moodle2 format + if (!self::detect_moodle2_format($tempdir)) { + throw new coding_exception('Conversion failed'); } } @@ -64,8 +158,6 @@ abstract class convert_helper { */ public static function set_inforef($contextid) { global $DB; - - } public static function get_inforef($contextid) { @@ -122,4 +214,143 @@ abstract class convert_helper { throw new Exception(sprintf("Could not insert context record into temp table: %s", $msg)); } } + + /// end of public API ////////////////////////////////////////////////////// + + /** + * Choose the best conversion path for the given format + * + * Given the source format and the list of available converters and their properties, + * this methods picks the most effective way how to convert the source format into + * the target moodle2 format. The method returns a list of converters that should be + * called, in order. + * + * This implementation uses Dijkstra's algorithm to find the shortest way through + * the oriented graph. + * + * @see http://en.wikipedia.org/wiki/Dijkstra's_algorithm + * @param string $format the source backup format, one of backup::FORMAT_xxx + * @param array $descriptions list of {@link base_converter::description()} indexed by the converter name + * @return array ordered list of converter names to call (may be empty if not reachable) + */ + protected static function choose_conversion_path($format, array $descriptions) { + + // construct an oriented graph of conversion paths. backup formats are nodes + // and the the converters are edges of the graph. + $paths = array(); // [fromnode][tonode] => converter + foreach ($descriptions as $converter => $description) { + $from = $description['from']; + $to = $description['to']; + $cost = $description['cost']; + + if (is_null($from) or $from === backup::FORMAT_UNKNOWN or + is_null($to) or $to === backup::FORMAT_UNKNOWN or + is_null($cost) or $cost <= 0) { + throw new coding_exception('Invalid converter description:' . $converter); + } + + if (!isset($paths[$from][$to])) { + $paths[$from][$to] = $converter; + } else { + // if there are two converters available for the same conversion + // path, choose the one with the lowest cost. if there are more + // available converters with the same cost, the chosen one is + // undefined (depends on the order of processing) + if ($descriptions[$paths[$from][$to]]['cost'] > $cost) { + $paths[$from][$to] = $converter; + } + } + } + + if (empty($paths)) { + // no conversion paths available + return array(); + } + + // now use Dijkstra's algorithm and find the shortest conversion path + + $dist = array(); // list of nodes and their distances from the source format + $prev = array(); // list of previous nodes in optimal path from the source format + foreach ($paths as $fromnode => $tonodes) { + $dist[$fromnode] = null; // infinitive distance, can't be reached + $prev[$fromnode] = null; // unknown + foreach ($tonodes as $tonode => $converter) { + $dist[$tonode] = null; // infinitive distance, can't be reached + $prev[$tonode] = null; // unknown + } + } + + if (!array_key_exists($format, $dist)) { + return array(); + } else { + $dist[$format] = 0; + } + + $queue = array_flip(array_keys($dist)); + while (!empty($queue)) { + // find the node with the smallest distance from the source in the queue + // in the first iteration, this will find the original format node itself + $closest = null; + foreach ($queue as $node => $undefined) { + if (is_null($dist[$node])) { + continue; + } + if (is_null($closest) or ($dist[$node] < $dist[$closest])) { + $closest = $node; + } + } + + if (is_null($closest) or is_null($dist[$closest])) { + // all remaining nodes are inaccessible from source + break; + } + + if ($closest === backup::FORMAT_MOODLE) { + // bingo we can break now + break; + } + + unset($queue[$closest]); + + // visit all neighbors and update distances to them eventually + + if (!isset($paths[$closest])) { + continue; + } + $neighbors = array_keys($paths[$closest]); + // keep just neighbors that are in the queue yet + foreach ($neighbors as $ix => $neighbor) { + if (!array_key_exists($neighbor, $queue)) { + unset($neighbors[$ix]); + } + } + + foreach ($neighbors as $neighbor) { + // the alternative distance to the neighbor if we went thru the + // current $closest node + $alt = $dist[$closest] + $descriptions[$paths[$closest][$neighbor]]['cost']; + + if (is_null($dist[$neighbor]) or $alt < $dist[$neighbor]) { + // we found a shorter way to the $neighbor, remember it + $dist[$neighbor] = $alt; + $prev[$neighbor] = $closest; + } + } + } + + if (is_null($dist[backup::FORMAT_MOODLE])) { + // unable to find a conversion path, the target format not reachable + return array(); + } + + // reconstruct the optimal path from the source format to the target one + $conversionpath = array(); + $target = backup::FORMAT_MOODLE; + while (isset($prev[$target])) { + array_unshift($conversionpath, $paths[$prev[$target]][$target]); + $target = $prev[$target]; + } + + return $conversionpath; + } } diff --git a/backup/util/helper/simpletest/testconverthelper.php b/backup/util/helper/simpletest/testconverthelper.php new file mode 100644 index 00000000000..90b83d57a23 --- /dev/null +++ b/backup/util/helper/simpletest/testconverthelper.php @@ -0,0 +1,146 @@ +. + +/** + * Unit tests for backup/util/helper/convert_helper.class.php + * + * @package core + * @subpackage backup-convert + * @copyright 2011 David Mudrak + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot . '/backup/util/includes/convert_includes.php'); + +/** + * Provides access to the protected methods we need to test + */ +class testable_convert_helper extends convert_helper { + + public static function choose_conversion_path($format, array $descriptions) { + return parent::choose_conversion_path($format, $descriptions); + } +} + +/** + * Defines the test methods + */ +class convert_helper_test extends UnitTestCase { + + public static $includecoverage = array(); + + public function test_choose_conversion_path() { + + // no converters available + $descriptions = array(); + $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions); + $this->assertEqual($path, array()); + + // missing source and/or targets + $descriptions = array( + // some custom converter + 'exporter' => array( + 'from' => backup::FORMAT_MOODLE1, + 'to' => 'some_custom_format', + 'cost' => 10, + ), + // another custom converter + 'converter' => array( + 'from' => 'yet_another_crazy_custom_format', + 'to' => backup::FORMAT_MOODLE, + 'cost' => 10, + ), + ); + $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions); + $this->assertEqual($path, array()); + + $path = testable_convert_helper::choose_conversion_path('some_other_custom_format', $descriptions); + $this->assertEqual($path, array()); + + // single step conversion + $path = testable_convert_helper::choose_conversion_path('yet_another_crazy_custom_format', $descriptions); + $this->assertEqual($path, array('converter')); + + // no conversion needed - this is supposed to be detected by the caller + $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE, $descriptions); + $this->assertEqual($path, array()); + + // two alternatives + $descriptions = array( + // standard moodle 1.9 -> 2.x converter + 'moodle1' => array( + 'from' => backup::FORMAT_MOODLE1, + 'to' => backup::FORMAT_MOODLE, + 'cost' => 10, + ), + // alternative moodle 1.9 -> 2.x converter + 'alternative' => array( + 'from' => backup::FORMAT_MOODLE1, + 'to' => backup::FORMAT_MOODLE, + 'cost' => 8, + ) + ); + $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions); + $this->assertEqual($path, array('alternative')); + + // complex case + $descriptions = array( + // standard moodle 1.9 -> 2.x converter + 'moodle1' => array( + 'from' => backup::FORMAT_MOODLE1, + 'to' => backup::FORMAT_MOODLE, + 'cost' => 10, + ), + // alternative moodle 1.9 -> 2.x converter + 'alternative' => array( + 'from' => backup::FORMAT_MOODLE1, + 'to' => backup::FORMAT_MOODLE, + 'cost' => 8, + ), + // custom converter from 1.9 -> custom 'CFv1' format + 'cc1' => array( + 'from' => backup::FORMAT_MOODLE1, + 'to' => 'CFv1', + 'cost' => 2, + ), + // custom converter from custom 'CFv1' format -> moodle 2.0 format + 'cc2' => array( + 'from' => 'CFv1', + 'to' => backup::FORMAT_MOODLE, + 'cost' => 5, + ), + // custom converter from CFv1 -> CFv2 format + 'cc3' => array( + 'from' => 'CFv1', + 'to' => 'CFv2', + 'cost' => 2, + ), + // custom converter from CFv2 -> moodle 2.0 format + 'cc4' => array( + 'from' => 'CFv2', + 'to' => backup::FORMAT_MOODLE, + 'cost' => 2, + ), + ); + + // ask the helper to find the most effective way + $path = testable_convert_helper::choose_conversion_path(backup::FORMAT_MOODLE1, $descriptions); + $this->assertEqual($path, array('cc1', 'cc3', 'cc4')); + } +} diff --git a/backup/util/plan/convert_plan.class.php b/backup/util/plan/convert_plan.class.php index 835423dbebb..633bbde8cbd 100644 --- a/backup/util/plan/convert_plan.class.php +++ b/backup/util/plan/convert_plan.class.php @@ -48,7 +48,7 @@ class convert_plan extends base_plan implements loggable { } public function get_basepath() { - return $this->converter->get_convertdir(); + return $this->converter->get_workdir_path(); } /** diff --git a/mod/forum/backup/moodle1/convert_forum_stepslib.php b/mod/forum/backup/moodle1/convert_forum_stepslib.php index 218c6f992d1..acc2acaba15 100644 --- a/mod/forum/backup/moodle1/convert_forum_stepslib.php +++ b/mod/forum/backup/moodle1/convert_forum_stepslib.php @@ -30,6 +30,7 @@ defined('MOODLE_INTERNAL') || die(); * Convert forum */ class moodle1_forum_activity_structure_step extends convert_structure_step { + /** * Function that will return the structure to be processed by this convert_step. * Must return one array of @convert_path_element elements