AutorÃa | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** @package moodlecore* @subpackage xml* @copyright 2010 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/simplified_parser_processor.class.php');/*** Abstract xml parser processor able to group chunks as configured* and dispatch them to other arbitrary methods** This @progressive_parser_processor handles the requested paths,* allowing to group information under any of them, dispatching them* to the methods specified** Note memory increases as you group more and more paths, so use it for* well-known structures being smaller enough (never to group MBs into one* in-memory structure)** TODO: Complete phpdocs*/abstract class grouped_parser_processor extends simplified_parser_processor {protected $groupedpaths; // Paths we are requesting groupedprotected $currentdata; // Where we'll be acummulating data// We create a array that stores each of the paths in a tree fashion// like the filesystem. Each element stores all the child elements that are// part of a full path that builds the grouped parent path we are storing.// eg Array keys are stored as follows;// root => a => b// => b// => c => d// => e => f.// Grouped paths here are; /a/b, /b, /c/d, /c/e/f.// There are no nested parent paths, that is an enforced rule so// we store an empty array to designate that the particular XML path element// is in fact a grouped path.// eg; $this->groupedparentprefixtree['a']['b'] = array();/** @var array Search tree storing the grouped paths. */protected $groupedparentprefixtree;/*** Keep cache of parent directory paths for XML parsing.* @var array*/protected $parentcache = array();/*** Remaining space for parent directory paths.* @var integer*/protected $parentcacheavailablesize = 2048;public function __construct(array $paths = array()) {$this->groupedpaths = array();$this->currentdata = null;parent::__construct($paths);}public function add_path($path, $grouped = false) {if ($grouped) {// Check there is no parent in the branch being groupedif ($found = $this->grouped_parent_exists($path)) {$a = new stdclass();$a->path = $path;$a->parent = $found;throw new progressive_parser_exception('xml_grouped_parent_found', $a);}// Check there is no child in the branch being groupedif ($found = $this->grouped_child_exists($path)) {$a = new stdclass();$a->path = $path;$a->child = $found;throw new progressive_parser_exception('xml_grouped_child_found', $a);}$this->groupedpaths[$path] = true;// We check earlier in the function if there is a parent that is above the path// to be added so we can be sure no parent exists in the tree.$patharray = explode('/', $path);$currentpos = &$this->groupedparentprefixtree;foreach ($patharray as $item) {if (!isset($currentpos[$item])) {$currentpos[$item] = array();}// Update the current array position using a reference to allow in-place updates to the array.$currentpos = &$currentpos[$item];}}parent::add_path($path);}/*** The parser fires this each time one path is going to be parsed** @param string $path xml path which parsing has started*/public function before_path($path) {if ($this->path_is_grouped($path) and !isset($this->currentdata[$path])) {// If the grouped element itself does not contain any final tags,// we would not get any chunk data for it. So we add an artificial// empty data chunk here that will be eventually replaced with// real data later in {@link self::postprocess_chunk()}.$this->currentdata[$path] = array('path' => $path,'level' => substr_count($path, '/') + 1,'tags' => array(),);}if (!$this->grouped_parent_exists($path)) {parent::before_path($path);}}/*** The parser fires this each time one path has been parsed** @param string $path xml path which parsing has ended*/public function after_path($path) {// Have finished one grouped path, dispatch itif ($this->path_is_grouped($path)) {// Any accumulated information must be in// currentdata, properly built$data = $this->currentdata[$path];unset($this->currentdata[$path]);// Always, before dispatching any chunk, send all pending start notifications.$this->process_pending_startend_notifications($path, 'start');// TODO: If running under DEBUG_DEVELOPER notice about >1MB grouped chunks// And, finally, dispatch it.$this->dispatch_chunk($data);}// Normal notification of path end// Only if path is selected and not child of groupedif (!$this->grouped_parent_exists($path)) {parent::after_path($path);}}// Protected API starts here/*** Override this method so grouping will be happening here* also deciding between accumulating/dispatching*/protected function postprocess_chunk($data) {$path = $data['path'];// If the chunk is a grouped one, simply put it into currentdataif ($this->path_is_grouped($path)) {$this->currentdata[$path] = $data;// If the chunk is child of grouped one, add it to currentdata} else if ($grouped = $this->grouped_parent_exists($path)) {$this->build_currentdata($grouped, $data);$this->chunks--; // not counted, as it's accumulated// No grouped nor child of grouped, dispatch it} else {$this->dispatch_chunk($data);}}protected function path_is_grouped($path) {return isset($this->groupedpaths[$path]);}/*** Function that will look for any grouped* parent for the given path, returning it if found,* false if not*/protected function grouped_parent_exists($path) {// Search the tree structure to find out if one of the paths// above the $path is a grouped path.$patharray = explode('/', $this->get_parent_path($path));$groupedpath = '';$currentpos = &$this->groupedparentprefixtree;foreach ($patharray as $item) {// When the item isn't set in the array we know// there is no parent grouped path.if (!isset($currentpos[$item])) {return false;}// When we aren't at the start of the path, continue to build// a string representation of the path that is traversed. We will// return the grouped path to the caller if we find one.if ($item != '') {$groupedpath .= '/'.$item;}if ($currentpos[$item] == array()) {return $groupedpath;}$currentpos = &$currentpos[$item];}return false;}/*** Get the parent path using a local cache for performance.** @param $path string The pathname you wish to obtain the parent name for.* @return string The parent pathname.*/protected function get_parent_path($path) {if (!isset($this->parentcache[$path])) {$this->parentcache[$path] = progressive_parser::dirname($path);$this->parentcacheavailablesize--;if ($this->parentcacheavailablesize < 0) {// Older first is cheaper than LRU. We use 10% as items are grouped together and the large quiz// restore from MDL-40585 used only 600 parent paths. This is an XML heirarchy, so common paths// are grouped near each other. eg; /question_bank/question_category/question/element. After keeping// question_bank paths in the cache when we move to another area and the question_bank cache is not// useful any longer.$this->parentcache = array_slice($this->parentcache, 200, null, true);$this->parentcacheavailablesize += 200;}}return $this->parentcache[$path];}/*** Function that will look for any grouped* child for the given path, returning it if found,* false if not*/protected function grouped_child_exists($path) {$childpath = $path . '/';foreach ($this->groupedpaths as $groupedpath => $set) {if (strpos($groupedpath, $childpath) === 0) {return $groupedpath;}}return false;}/*** This function will accumulate the chunk into the specified* grouped element for later dispatching once it is complete*/protected function build_currentdata($grouped, $data) {// Check the grouped already exists into currentdataif (!is_array($this->currentdata) or !array_key_exists($grouped, $this->currentdata)) {$a = new stdclass();$a->grouped = $grouped;$a->child = $data['path'];throw new progressive_parser_exception('xml_cannot_add_to_grouped', $a);}$this->add_missing_sub($grouped, $data['path'], $data['tags']);}/*** Add non-existing subarray elements*/protected function add_missing_sub($grouped, $path, $tags) {// Remember tag being processed$processedtag = basename($path);$info =& $this->currentdata[$grouped]['tags'];$hierarchyarr = explode('/', str_replace($grouped . '/', '', $path));$previouselement = '';$currentpath = '';foreach ($hierarchyarr as $index => $element) {$currentpath = $currentpath . '/' . $element;// If element is already set and it's not// the processed one (with tags) fast move the $info// pointer and continueif ($element !== $processedtag && isset($info[$element])) {$previouselement = $element;$info =& $info[$element];continue;}// If previous element already has occurrences// we move $info pointer there (only if last is// numeric occurrence)if (!empty($previouselement) && is_array($info) && count($info) > 0) {end($info);$key = key($info);if ((int) $key === $key) {$info =& $info[$key];}}// Create element if not definedif (!isset($info[$element])) {// First into last element if present$info[$element] = array();}// If element is the current one, add informationif ($element === $processedtag) {$info[$element][] = $tags;}$previouselement = $element;$info =& $info[$element];}}}