diff --git a/backup/util/xml/parser/processors/findpaths_parser_processor.class.php b/backup/util/xml/parser/processors/findpaths_parser_processor.class.php new file mode 100644 index 00000000000..9b59821e040 --- /dev/null +++ b/backup/util/xml/parser/processors/findpaths_parser_processor.class.php @@ -0,0 +1,60 @@ +. + +/** + * @package moodlecore + * @subpackage xml + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->dirroot.'/backup/util/xml/parser/processors/progressive_parser_processor.class.php'); + +/** + * Find paths progressive_parser_processor that will search for all the paths present in + * the chunks being returned. Useful to know the overal structure of the XML file. + */ +class findpaths_parser_processor extends progressive_parser_processor { + + protected $foundpaths; // array of paths foudn in the chunks received from the parser + + public function __construct() { + parent::__construct(); + $this->foundpaths = array(); + } + + public function process_chunk($data) { + if (isset($data['tags'])) { + foreach ($data['tags'] as $tag) { + $tagpath = $data['path'] . '/' . $tag['name']; + if (!array_key_exists($tagpath, $this->foundpaths)) { + $this->foundpaths[$tagpath] = 1; + } else { + $this->foundpaths[$tagpath]++; + } + } + } + } + + public function debug_info() { + $debug = array(); + foreach($this->foundpaths as $path => $chunks) { + $debug['paths'][$path] = $chunks; + } + return array_merge($debug, parent::debug_info()); + } +} diff --git a/backup/util/xml/parser/processors/null_parser_processor.class.php b/backup/util/xml/parser/processors/null_parser_processor.class.php new file mode 100644 index 00000000000..cbaeab77dc3 --- /dev/null +++ b/backup/util/xml/parser/processors/null_parser_processor.class.php @@ -0,0 +1,35 @@ +. + +/** + * @package moodlecore + * @subpackage xml + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->dirroot.'/backup/util/xml/parser/processors/progressive_parser_processor.class.php'); + +/** + * Null progressive_parser_processor that won't process chunks at all. + * Useful for comparing memory use/execution time. + */ +class null_parser_processor extends progressive_parser_processor { + + public function process_chunk($data) { + } +} diff --git a/backup/util/xml/parser/processors/progressive_parser_processor.class.php b/backup/util/xml/parser/processors/progressive_parser_processor.class.php new file mode 100644 index 00000000000..922c1099e52 --- /dev/null +++ b/backup/util/xml/parser/processors/progressive_parser_processor.class.php @@ -0,0 +1,79 @@ +. + +/** + * @package moodlecore + * @subpackage backup-xml + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * This abstract class implements one progressive_parser_processor + * + * Processor that will receive chunks of data from the @progressive_parser + * and will perform all sort of operations with them (join, split, invoke + * other methods, output, whatever... + * + * You will need to extend this class to get the expected functionality + * by implementing the @process_chunk() method to handle different + * chunks of information and, optionally, the @process_cdata() to + * process each cdata piece individually before being "published" to + * the chunk processor. + * + * The "propietary array format" that the parser publishes to the @progressive_parser_procesor + * is this: + * array ( + * 'path' => path where the tags belong to, + * 'level'=> level (1-based) of the tags + * 'tags => array ( + * 'name' => name of the tag, + * 'attrs'=> array( name of the attr => value of the attr), + * 'cdata => cdata of the tag + * ) + * ) + * + * TODO: Finish phpdocs + */ +abstract class progressive_parser_processor { + + protected $inittime; // Initial microtime + protected $chunks; // Number of chunks processed + + public function __construct() { + $this->inittime= microtime(true); + $this->chunks = 0; + } + + abstract public function process_chunk($data); + + public function process_cdata($cdata) { + return $cdata; + } + + public function debug_info() { + return array('memory' => memory_get_peak_usage(true), + 'time' => microtime(true) - $this->inittime, + 'chunks' => $this->chunks); + } + + public function receive_chunk($data) { + $this->chunks++; + $this->process_chunk($data); + } + +} diff --git a/backup/util/xml/parser/processors/selective_exact_parser_processor.class.php b/backup/util/xml/parser/processors/selective_exact_parser_processor.class.php new file mode 100644 index 00000000000..b26198dc8fe --- /dev/null +++ b/backup/util/xml/parser/processors/selective_exact_parser_processor.class.php @@ -0,0 +1,53 @@ +. + +/** + * @package moodlecore + * @subpackage xml + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->dirroot.'/backup/util/xml/parser/processors/progressive_parser_processor.class.php'); + +/** + * Selective progressive_parser_processor that will send chunks straight + * to output but only for chunks matching (in an exact way) some defined paths + */ +class selective_exact_parser_processor extends progressive_parser_processor { + + protected $paths; // array of paths we are interested on + + public function __construct(array $paths) { + parent::__construct(); + $this->paths = $paths; + } + + public function process_chunk($data) { + if ($this->path_is_selected($data['path'])) { + print_r($data); // Simply output chunk, for testing purposes + } else { + $this->chunks--; // Chunk skipped + } + } + +// Protected API starts here + + protected function path_is_selected($path) { + return in_array($path, $this->paths); + } +} diff --git a/backup/util/xml/parser/processors/selective_like_parser_processor.class.php b/backup/util/xml/parser/processors/selective_like_parser_processor.class.php new file mode 100644 index 00000000000..1948ab66206 --- /dev/null +++ b/backup/util/xml/parser/processors/selective_like_parser_processor.class.php @@ -0,0 +1,53 @@ +. + +/** + * @package moodlecore + * @subpackage xml + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->dirroot.'/backup/util/xml/parser/processors/progressive_parser_processor.class.php'); + +/** + * Selective progressive_parser_processor that will send chunks straight + * to output but only for chunks matching (in a left padded way - like) some defined paths + */ +class selective_like_parser_processor extends progressive_parser_processor { + + protected $paths; // array of paths we are interested on + + public function __construct(array $paths) { + parent::__construct(); + $this->paths = '=>' . implode('=>', $paths); + } + + public function process_chunk($data) { + if ($this->path_is_selected($data['path'])) { + print_r($data); // Simply output chunk, for testing purposes + } else { + $this->chunks--; // Chunk skipped + } + } + +// Protected API starts here + + protected function path_is_selected($path) { + return strpos('@=>' . $path, $this->paths); + } +} diff --git a/backup/util/xml/parser/processors/simple_parser_processor.class.php b/backup/util/xml/parser/processors/simple_parser_processor.class.php new file mode 100644 index 00000000000..63c46bba501 --- /dev/null +++ b/backup/util/xml/parser/processors/simple_parser_processor.class.php @@ -0,0 +1,36 @@ +. + +/** + * @package moodlecore + * @subpackage xml + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->dirroot.'/backup/util/xml/parser/processors/progressive_parser_processor.class.php'); + +/** + * Simple progressive_parser_processor that will send chunks straight + * to output. Useful for testing, compare memory use/execution time. + */ +class simple_parser_processor extends progressive_parser_processor { + + public function process_chunk($data) { + print_r($data); // Simply output chunk, for testing purposes + } +} diff --git a/backup/util/xml/parser/processors/simplified_parser_processor.class.php b/backup/util/xml/parser/processors/simplified_parser_processor.class.php new file mode 100644 index 00000000000..21de6b0aa54 --- /dev/null +++ b/backup/util/xml/parser/processors/simplified_parser_processor.class.php @@ -0,0 +1,129 @@ +. + +/** + * @package moodlecore + * @subpackage xml + * @copyright 2003 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once($CFG->dirroot.'/backup/util/xml/parser/processors/progressive_parser_processor.class.php'); + +/** + * Abstract xml parser processor to be to simplify and dispatch parsed chunks + * + * This @progressive_parser_processor handles the requested paths, + * performing some conversions from the original "propietary array format" + * used by the @progressive_parser to a simplified structure to be used + * easily. Found attributes are converted automatically to tags and cdata + * to simpler values. + * + * Note: final tag attributes are discarded completely! + * + * TODO: Complete phpdocs + */ +abstract class simplified_parser_processor extends progressive_parser_processor { + protected $paths; // array of paths we are interested on + protected $parentpaths; // array of parent paths of the $paths + protected $parentsinfo; // array of parent attributes to be added as child tags + + public function __construct(array $paths) { + parent::__construct(); + $this->paths = $paths; + $this->parentpaths = array(); + $this->parentsinfo = array(); + // Add parent paths. We are looking for attributes there + foreach ($paths as $key => $path) { + $this->parentpaths[$key] = dirname($path); + } + } + + /** + * Get the already simplified chunk and dispatch it + */ + abstract public function dispatch_chunk($data); + + /** + * Get one chunk of parsed data and make it simpler + * adding attributes as tags and delegating to + * dispatch_chunk() the procesing of the resulting chunk + */ + public function process_chunk($data) { + // Precalculate some vars for readability + $path = $data['path']; + $parentpath = dirname($path); + $tag = basename($path); + + // If the path is a registered parent one, store all its tags + // so, we'll be able to find attributes later when processing + // (child) registered paths (to get attributes if present) + if ($this->path_is_selected_parent($path)) { // if path is parent + if (isset($data['tags'])) { // and has tags, save them + $this->parentsinfo[$path] = $data['tags']; + } + } + + // If the path is a registered one, let's process it + if ($this->path_is_selected($path)) { + // First of all, look for attributes available at parentsinfo + // in order to get them available as normal tags + if (isset($this->parentsinfo[$parentpath][$tag]['attrs'])) { + $data['tags'] = array_merge($this->parentsinfo[$parentpath][$tag]['attrs'], $data['tags']); + unset($this->parentsinfo[$parentpath][$tag]['attrs']); + } + // Now, let's simplify the tags array, ignoring tag attributtes and + // reconverting to simpler name => value array + foreach ($data['tags'] as $key => $value) { + // If the value is already a single value, do nothing + // surely was added above from parentsinfo + if (!is_array($value)) { + continue; + } + // If the path including the tag name matches another selected path + // (registered or parent) delete it, another chunk will contain that info + if ($this->path_is_selected($path . '/' . $key) || + $this->path_is_selected_parent($path . '/' . $key)) { + unset($data['tags'][$key]); + continue; + } + // Convert to simple name => value array + $data['tags'][$key] = isset($value['cdata']) ? $value['cdata'] : null; + } + + // Arrived here, if the chunk has tags, send it to dispatcher + if (!empty($data['tags'])) { + return $this->dispatch_chunk($data); + } else { + $this->chunks--; // Chunk skipped + } + } else { + $this->chunks--; // Chunk skipped + } + return true; + } + +// Protected API starts here + + protected function path_is_selected($path) { + return in_array($path, $this->paths); + } + + protected function path_is_selected_parent($path) { + return in_array($path, $this->parentpaths); + } +} diff --git a/backup/util/xml/parser/progressive_parser.class.php b/backup/util/xml/parser/progressive_parser.class.php new file mode 100644 index 00000000000..d12db43495d --- /dev/null +++ b/backup/util/xml/parser/progressive_parser.class.php @@ -0,0 +1,245 @@ +. + +/** + * @package moodlecore + * @subpackage backup-xml + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Class implementing one SAX progressive push parser. + * + * SAX parser able to process XML content from files/variables. It supports + * attributes and case folding and works only with UTF-8 content. It's one + * progressive push parser because, intead of loading big crunchs of information + * in memory, it "publishes" (pushes) small information in a "propietary array format" througt + * the corresponding @progressive_parser_procesor, that will be the responsibe for + * returning information into handy formats to higher levels. + * + * Note that, while this progressive parser is able to process any XML file, it is + * 100% progressive so it publishes the information in the original order it's parsed (that's + * the expected behaviour) so information belonging to the same path can be returned in + * different chunks if there are inner levels/paths in the middle. Be warned! + * + * The "propietary array format" that the parser publishes to the @progressive_parser_procesor + * is this: + * array ( + * 'path' => path where the tags belong to, + * 'level'=> level (1-based) of the tags + * 'tags => array ( + * 'name' => name of the tag, + * 'attrs'=> array( name of the attr => value of the attr), + * 'cdata => cdata of the tag + * ) + * ) + * + * TODO: Finish phpdocs + */ +class progressive_parser { + + protected $xml_parser; // PHP's low level XML SAX parser + protected $file; // full path to file being progressively parsed | => mutually exclusive + protected $contents; // contents being progressively parsed | + protected $procesor; // progressive_parser_procesor to be used to publish processed information + + protected $level; // level of the current tag + protected $path; // path of the current tag + protected $accum; // accumulated char data of the current tag + protected $attrs; // attributes of the current tag + + protected $topush; // array containing current level information being parsed to be "pushed" + protected $prevlevel; // level of the previous tag processed - to detect pushing places + protected $currtag; // name/value/attributes of the tag being processed + + public function __construct($case_folding = false) { + $this->xml_parser = xml_parser_create('UTF-8'); + xml_parser_set_option($this->xml_parser, XML_OPTION_CASE_FOLDING, $case_folding); + xml_set_object($this->xml_parser, $this); + xml_set_element_handler($this->xml_parser, array($this, 'start_tag'), array($this, 'end_tag')); + xml_set_character_data_handler($this->xml_parser, array($this, 'char_data')); + + $this->file = null; + $this->contents = null; + $this->procesor = null; + $this->level = 0; + $this->path = ''; + $this->accum = ''; + $this->attrs = array(); + $this->topush = array(); + $this->prevlevel = 0; + $this->currtag = array(); + } + + /* + * Sets the XML file to be processed by the parser + */ + public function set_file($file) { + if (!file_exists($file) || (!is_readable($file))) { + throw new progressive_parser_exception('invalid_file_to_parse'); + } + $this->file = $file; + $this->contents = null; + } + + /* + * Sets the XML contents to be processed by the parser + */ + public function set_contents($contents) { + if (empty($contents)) { + throw new progressive_parser_exception('invalid_contents_to_parse'); + } + $this->contents = $contents; + $this->file = null; + } + + /* + * Define the @progressive_parser_processor in charge of processing the parsed chunks + */ + public function set_processor($processor) { + if (!$processor instanceof progressive_parser_processor) { + throw new progressive_parser_exception('invalid_parser_processor'); + } + $this->processor = $processor; + } + + /* + * Process the XML, delegating found chunks to the @progressive_parser_processor + */ + public function process() { + if (empty($this->processor)) { + throw new progressive_parser_exception('undefined_parser_processor'); + } + if (empty($this->file) && empty($this->contents)) { + throw new progressive_parser_exception('undefined_xml_to_parse'); + } + if (is_null($this->xml_parser)) { + throw new progressive_parser_exception('progressive_parser_already_used'); + } + if ($this->file) { + $fh = fopen($this->file, 'r'); + while ($buffer = fread($fh, 8192)) { + $this->parse($buffer, feof($fh)); + } + fclose($fh); + } else { + $this->parse($this->contents, true); + } + xml_parser_free($this->xml_parser); + $this->xml_parser = null; + } + +// Protected API starts here + + protected function parse($data, $eof) { + if (!xml_parse($this->xml_parser, $data, $eof)) { + throw new progressive_parser_exception( + 'xml_parsing_error', null, + sprintf('XML error: %s at line %d, column %d', + xml_error_string(xml_get_error_code($this->xml_parser)), + xml_get_current_line_number($this->xml_parser), + xml_get_current_column_number($this->xml_parser))); + } + } + + protected function publish($data) { + $this->processor->receive_chunk($data); + } + + protected function postprocess_cdata($data) { + return $this->processor->process_cdata($data); + } + + protected function start_tag($parser, $tag, $attributes) { + + // Normal update of parser internals + $this->level++; + $this->path .= '/' . $tag; + $this->accum = ''; + $this->attrs = !empty($attributes) ? $attributes : array(); + + // Entering a new inner level, publish all the information available + if ($this->level > $this->prevlevel) { + if (!empty($this->currtag) && (!empty($this->currtag['attrs']) || !empty($this->currtag['cdata']))) { + $this->topush['tags'][$this->currtag['name']] = $this->currtag; + } + if (!empty($this->topush['tags'])) { + $this->publish($this->topush); + } + $this->currtag = array(); + $this->topush = array(); + } + + // If not set, build to push common header + if (empty($this->topush)) { + $this->topush['path'] = dirname($this->path); + $this->topush['level'] = $this->level; + $this->topush['tags'] = array(); + } + + // Handling a new tag, create it + $this->currtag['name'] = $tag; + // And add attributes if present + if ($this->attrs) { + $this->currtag['attrs'] = $this->attrs; + } + + // For the records + $this->prevlevel = $this->level; + } + + protected function end_tag($parser, $tag) { + + // Ending rencently started tag, add value to current tag + if ($this->level == $this->prevlevel) { + $this->currtag['cdata'] = $this->postprocess_cdata($this->accum); + $this->topush['tags'][$this->currtag['name']] = $this->currtag; + $this->currtag = array(); + } + + // Leaving one level, publish all the information available + if ($this->level < $this->prevlevel) { + if (!empty($this->topush['tags'])) { + $this->publish($this->topush); + } + $this->currtag = array(); + $this->topush = array(); + } + + // For the records + $this->prevlevel = $this->level; + + // Normal update of parser internals + $this->level--; + $this->path = dirname($this->path); + } + + protected function char_data($parser, $data) { + $this->accum .= $data; + } +} + +/* + * Exception class used by all the @progressive_parser stuff + */ +class progressive_parser_exception extends moodle_exception { + + public function __construct($errorcode, $a=NULL, $debuginfo=null) { + parent::__construct($errorcode, 'error', '', $a, null, $debuginfo); + } +} diff --git a/backup/util/xml/parser/simpletest/fixtures/test1.xml b/backup/util/xml/parser/simpletest/fixtures/test1.xml new file mode 100644 index 00000000000..f763f8554e7 --- /dev/null +++ b/backup/util/xml/parser/simpletest/fixtures/test1.xml @@ -0,0 +1,5 @@ + + + secondvalue + secondvalue + diff --git a/backup/util/xml/parser/simpletest/fixtures/test2.xml b/backup/util/xml/parser/simpletest/fixtures/test2.xml new file mode 100644 index 00000000000..a8703d60115 --- /dev/null +++ b/backup/util/xml/parser/simpletest/fixtures/test2.xml @@ -0,0 +1,5 @@ + + + secondvalue + secondvalue + diff --git a/backup/util/xml/parser/simpletest/fixtures/test3.xml b/backup/util/xml/parser/simpletest/fixtures/test3.xml new file mode 100644 index 00000000000..69ec9782127 --- /dev/null +++ b/backup/util/xml/parser/simpletest/fixtures/test3.xml @@ -0,0 +1,26 @@ + + + secondvalue + + onevalue + anothervalue + yetanothervalue + twovalue + + innervalue + + + superinnervalue + + + + + + seventh + + + finalvalue + + + + diff --git a/backup/util/xml/parser/simpletest/fixtures/test4.xml b/backup/util/xml/parser/simpletest/fixtures/test4.xml new file mode 100755 index 00000000000..9ff98ac12b6 --- /dev/null +++ b/backup/util/xml/parser/simpletest/fixtures/test4.xml @@ -0,0 +1,93 @@ + + + + One glossary + <p>One simple glossary to test backup &amp; restore. Here it's the standard image:</p> +<p><img src="@@PLUGINFILE@@/88_31.png" alt="pwd by moodle" width="88" height="31" /></p> + 0 + dictionary + 0 + 1 + 1 + 1 + 0 + 1 + 1 + 1 + 0 + 10 + 0 + 0 + 0 + 1 + 0 + 0 + 10 + 1275638215 + 1275639747 + + + 2 + dog + <p>Traditional enemies of cats</p> + 1 + 0 + + 1275638279 + 1275638279 + 1 + 0 + 1 + 0 + 0 + 1 + + + dogs + + + + + 10 + 6 + 5 + 1275639785 + 1275639797 + + + + + 2 + cat + <p>traditional enemies of dogs</p> + 1 + 0 + + 1275638304 + 1275638304 + 1 + 0 + 1 + 0 + 0 + 1 + + + cats + + + + + 10 + 5 + 5 + 1275639779 + 1275639779 + + + + + + + + \ No newline at end of file diff --git a/backup/util/xml/parser/simpletest/testparser.php b/backup/util/xml/parser/simpletest/testparser.php new file mode 100644 index 00000000000..f819afe07c4 --- /dev/null +++ b/backup/util/xml/parser/simpletest/testparser.php @@ -0,0 +1,348 @@ +. + +/** + * @package moodlecore + * @subpackage backup-tests + * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com} + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// Prevent direct access to this file +if (!defined('MOODLE_INTERNAL')) { + die('Direct access to this script is forbidden.'); +} + +// Include all the needed stuff +require_once($CFG->dirroot . '/backup/util/xml/parser/progressive_parser.class.php'); +require_once($CFG->dirroot . '/backup/util/xml/parser/processors/progressive_parser_processor.class.php'); +require_once($CFG->dirroot . '/backup/util/xml/parser/processors/simplified_parser_processor.class.php'); + +/* + * progressive_parser and progressive_parser_processor tests + */ +class progressive_parser_test extends UnitTestCase { + + public static $includecoverage = array('backup/util/xml/parser'); + public static $excludecoverage = array('backup/util/xml/parser/simpletest'); + + /* + * test progressive_parser public methods + */ + function test_parser_public_api() { + global $CFG; + // Instantiate progressive_parser + $pp = new progressive_parser(); + $this->assertTrue($pp instanceof progressive_parser); + $pr = new mock_parser_processor(); + $this->assertTrue($pr instanceof progressive_parser_processor); + + // Try to process without processor + try { + $pp->process(); + $this->assertTrue(false); + } catch (exception $e) { + $this->assertTrue($e instanceof progressive_parser_exception); + $this->assertEqual($e->errorcode, 'undefined_parser_processor'); + } + + // Assign processor to parser + $pp->set_processor($pr); + + // Try to process without file and contents + try { + $pp->process(); + $this->assertTrue(false); + } catch (exception $e) { + $this->assertTrue($e instanceof progressive_parser_exception); + $this->assertEqual($e->errorcode, 'undefined_xml_to_parse'); + } + + // Assign *invalid* processor to parser + try { + $pp->set_processor(new stdClass()); + $this->assertTrue(false); + } catch (exception $e) { + $this->assertTrue($e instanceof progressive_parser_exception); + $this->assertEqual($e->errorcode, 'invalid_parser_processor'); + } + + // Set file from fixtures (test1.xml) and process it + $pp = new progressive_parser(); + $pr = new mock_parser_processor(); + $pp->set_processor($pr); + $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/simpletest/fixtures/test1.xml'); + $pp->process(); + $serfromfile = serialize($pr->get_chunks()); // Get serialized results (to compare later) + // Set *unexisting* file from fixtures + try { + $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/simpletest/fixtures/test0.xml'); + $this->assertTrue(false); + } catch (exception $e) { + $this->assertTrue($e instanceof progressive_parser_exception); + $this->assertEqual($e->errorcode, 'invalid_file_to_parse'); + } + + // Set contents from fixtures (test1.xml) and process it + $pp = new progressive_parser(); + $pr = new mock_parser_processor(); + $pp->set_processor($pr); + $pp->set_contents(file_get_contents($CFG->dirroot . '/backup/util/xml/parser/simpletest/fixtures/test1.xml')); + $pp->process(); + $serfrommemory = serialize($pr->get_chunks()); // Get serialized results (to compare later) + // Set *empty* contents + try { + $pp->set_contents(''); + $this->assertTrue(false); + } catch (exception $e) { + $this->assertTrue($e instanceof progressive_parser_exception); + $this->assertEqual($e->errorcode, 'invalid_contents_to_parse'); + } + + // Check that both results from file processing and content processing are equal + $this->assertEqual($serfromfile, $serfrommemory); + + // Check case_folding is working ok + $pp = new progressive_parser(true); + $pr = new mock_parser_processor(); + $pp->set_processor($pr); + $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/simpletest/fixtures/test1.xml'); + $pp->process(); + $chunks = $pr->get_chunks(); + $this->assertTrue($chunks[0]['path'] === '/FIRSTTAG'); + $this->assertTrue($chunks[0]['tags']['SECONDTAG']['name'] === 'SECONDTAG'); + $this->assertTrue($chunks[0]['tags']['SECONDTAG']['attrs']['NAME'] === 'secondtag'); + + // Check invalid XML exception is working ok + $pp = new progressive_parser(true); + $pr = new mock_parser_processor(); + $pp->set_processor($pr); + $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/simpletest/fixtures/test2.xml'); + try { + $pp->process(); + } catch (exception $e) { + $this->assertTrue($e instanceof progressive_parser_exception); + $this->assertEqual($e->errorcode, 'xml_parsing_error'); + } + + // Check double process throws exception + $pp = new progressive_parser(true); + $pr = new mock_parser_processor(); + $pp->set_processor($pr); + $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/simpletest/fixtures/test1.xml'); + $pp->process(); + try { // Second process, will throw exception + $pp->process(); + $this->assertTrue(false); + } catch (exception $e) { + $this->assertTrue($e instanceof progressive_parser_exception); + $this->assertEqual($e->errorcode, 'progressive_parser_already_used'); + } + } + + /* + * test progressive_parser parsing results using testing_parser_processor and test1.xml + * auto-described file from fixtures + */ + function test_parser_results() { + global $CFG; + // Instantiate progressive_parser + $pp = new progressive_parser(); + // Instantiate processor, passing the unit test as param + $pr = new mock_auto_parser_processor($this); + $this->assertTrue($pr instanceof progressive_parser_processor); + // Assign processor to parser + $pp->set_processor($pr); + // Set file from fixtures + $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/simpletest/fixtures/test3.xml'); + // Process the file, the autotest processor will perform a bunch of automatic tests + $pp->process(); + // Get processor debug info + $debug = $pr->debug_info(); + $this->assertTrue(is_array($debug)); + $this->assertTrue(array_key_exists('chunks', $debug)); + // Check the number of chunks is correct for the file + $this->assertEqual($debug['chunks'], 10); + } + + /* + * test progressive_parser parsing results using simplified_parser_processor and test4.xml + * (one simple glossary backup file example) + */ + function test_simplified_parser_results() { + global $CFG; + // Instantiate progressive_parser + $pp = new progressive_parser(); + // Instantiate simplified_parser_processor declaring the interesting paths + $pr = new mock_simplified_parser_processor(array( + '/activity', + '/activity/glossary', + '/activity/glossary/entries/entry', + '/activity/glossary/entries/entry/aliases/alias', + '/activity/glossary/entries/entry/ratings/rating', + '/activity/glossary/categories/category')); + $this->assertTrue($pr instanceof progressive_parser_processor); + // Assign processor to parser + $pp->set_processor($pr); + // Set file from fixtures + $pp->set_file($CFG->dirroot . '/backup/util/xml/parser/simpletest/fixtures/test4.xml'); + // Process the file + $pp->process(); + // Get processor debug info + $debug = $pr->debug_info(); + $this->assertTrue(is_array($debug)); + $this->assertTrue(array_key_exists('chunks', $debug)); + + // Check the number of chunks is correct for the file + $this->assertEqual($debug['chunks'], 8); + // Get all the simplified chunks and perform various validations + $chunks = $pr->get_chunks(); + + // chunk[0] (/activity) tests + $this->assertEqual(count($chunks[0]), 3); + $this->assertEqual($chunks[0]['path'], '/activity'); + $this->assertEqual($chunks[0]['level'],'2'); + $tags = $chunks[0]['tags']; + $this->assertEqual(count($tags), 4); + $this->assertEqual($tags['id'], 1); + $this->assertEqual($tags['moduleid'], 5); + $this->assertEqual($tags['modulename'], 'glossary'); + $this->assertEqual($tags['contextid'], 26); + $this->assertEqual($chunks[0]['level'],'2'); + + // chunk[1] (/activity/glossary) tests + $this->assertEqual(count($chunks[1]), 3); + $this->assertEqual($chunks[1]['path'], '/activity/glossary'); + $this->assertEqual($chunks[1]['level'],'3'); + $tags = $chunks[1]['tags']; + $this->assertEqual(count($tags), 24); + $this->assertEqual($tags['id'], 1); + $this->assertEqual($tags['intro'], '

One simple glossary to test backup & restore. Here it\'s the standard image:

'. + "\n". + '

pwd by moodle

'); + $this->assertEqual($tags['timemodified'], 1275639747); + $this->assertTrue(!isset($tags['categories'])); + + // chunk[5] (second /activity/glossary/entries/entry) tests + $this->assertEqual(count($chunks[5]), 3); + $this->assertEqual($chunks[5]['path'], '/activity/glossary/entries/entry'); + $this->assertEqual($chunks[5]['level'],'5'); + $tags = $chunks[5]['tags']; + $this->assertEqual(count($tags), 15); + $this->assertEqual($tags['id'], 2); + $this->assertEqual($tags['concept'], 'cat'); + $this->assertTrue(!isset($tags['aliases'])); + $this->assertTrue(!isset($tags['entries'])); + + // chunk[6] (second /activity/glossary/entries/entry/aliases/alias) tests + $this->assertEqual(count($chunks[6]), 3); + $this->assertEqual($chunks[6]['path'], '/activity/glossary/entries/entry/aliases/alias'); + $this->assertEqual($chunks[6]['level'],'7'); + $tags = $chunks[6]['tags']; + $this->assertEqual(count($tags), 2); + $this->assertEqual($tags['id'], 2); + $this->assertEqual($tags['alias_text'], 'cats'); + + // chunk[7] (second /activity/glossary/entries/entry/ratings/rating) tests + $this->assertEqual(count($chunks[7]), 3); + $this->assertEqual($chunks[7]['path'], '/activity/glossary/entries/entry/ratings/rating'); + $this->assertEqual($chunks[7]['level'],'7'); + $tags = $chunks[7]['tags']; + $this->assertEqual(count($tags), 6); + $this->assertEqual($tags['id'], 1); + $this->assertEqual($tags['timemodified'], '1275639779'); + } +} + +/* + * helper processor able to perform various auto-cheks based on attributes while processing + * the test1.xml file available in the fixtures dir. It performs these checks: + * - name equal to "name" attribute of the tag (if present) + * - level equal to "level" attribute of the tag (if present) + * - path + tagname equal to "path" attribute of the tag (if present) + * - cdata, if not empty is: + * - equal to "value" attribute of the tag (if present) + * - else, equal to tag name + * + * We pass the whole UnitTestCase object to the processor in order to be + * able to perform the tests in the straight in the process + */ +class mock_auto_parser_processor extends progressive_parser_processor { + + private $utc = null; // To store the unit test case + + public function __construct($unit_test_case) { + parent::__construct(); + $this->utc = $unit_test_case; + } + + public function process_chunk($data) { + // Perform auto-checks based in the rules above + if (isset($data['tags'])) { + foreach ($data['tags'] as $tag) { + if (isset($tag['attrs']['name'])) { // name tests + $this->utc->assertEqual($tag['name'], $tag['attrs']['name']); + } + if (isset($tag['attrs']['level'])) { // level tests + $this->utc->assertEqual($data['level'], $tag['attrs']['level']); + } + if (isset($tag['attrs']['path'])) { // path tests + $this->utc->assertEqual(rtrim($data['path'], '/') . '/' . $tag['name'], $tag['attrs']['path']); + } + if (!empty($tag['cdata'])) { // cdata tests + if (isset($tag['attrs']['value'])) { + $this->utc->assertEqual($tag['cdata'], $tag['attrs']['value']); + } else { + $this->utc->assertEqual($tag['cdata'], $tag['name']); + } + } + } + } + } +} + +/* + * helper processor that accumulates all the chunks, resturning them with the get_chunks() method + */ +class mock_parser_processor extends progressive_parser_processor { + + private $chunksarr = array(); // To accumulate the found chunks + + public function process_chunk($data) { + $this->chunksarr[] = $data; + } + + public function get_chunks() { + return $this->chunksarr; + } +} + +/* + * helper processor that accumulates simplified chunks, returning them with the get_chunks() method + */ +class mock_simplified_parser_processor extends simplified_parser_processor { + + private $chunksarr = array(); // To accumulate the found chunks + + public function dispatch_chunk($data) { + $this->chunksarr[] = $data; + } + + public function get_chunks() { + return $this->chunksarr; + } +}