tao-test/app/taoQtiItem/model/qti/ParserFactory.php

1742 lines
74 KiB
PHP

<?php
/**
* This program 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; under version 2
* of the License (non-upgradable).
*
* This program 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 this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2013 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
*
*
*/
namespace oat\taoQtiItem\model\qti;
use oat\taoQtiItem\model\qti\Element;
use oat\taoQtiItem\model\qti\container\Container;
use oat\taoQtiItem\model\qti\exception\UnsupportedQtiElement;
use oat\taoQtiItem\model\qti\exception\ParsingException;
use oat\taoQtiItem\model\qti\container\ContainerInteractive;
use oat\taoQtiItem\model\qti\container\ContainerItemBody;
use oat\taoQtiItem\model\qti\container\ContainerGap;
use oat\taoQtiItem\model\qti\container\ContainerHottext;
use oat\taoQtiItem\model\qti\Item;
use oat\taoQtiItem\model\qti\response\Custom;
use oat\taoQtiItem\model\qti\interaction\BlockInteraction;
use oat\taoQtiItem\model\qti\interaction\ObjectInteraction;
use oat\taoQtiItem\model\qti\interaction\CustomInteraction;
use oat\taoQtiItem\model\qti\interaction\PortableCustomInteraction;
use oat\taoQtiItem\model\CustomInteractionRegistry;
use oat\taoQtiItem\model\qti\InfoControl;
use oat\taoQtiItem\model\qti\PortableInfoControl;
use oat\taoQtiItem\model\InfoControlRegistry;
use oat\taoQtiItem\model\qti\choice\ContainerChoice;
use oat\taoQtiItem\model\qti\choice\TextVariableChoice;
use oat\taoQtiItem\model\qti\choice\GapImg;
use oat\taoQtiItem\model\qti\ResponseDeclaration;
use oat\taoQtiItem\model\qti\OutcomeDeclaration;
use oat\taoQtiItem\model\qti\response\Template;
use oat\taoQtiItem\model\qti\exception\UnexpectedResponseProcessing;
use oat\taoQtiItem\model\qti\response\TemplatesDriven;
use oat\taoQtiItem\model\qti\response\TakeoverFailedException;
use oat\taoQtiItem\model\qti\response\Summation;
use oat\taoQtiItem\model\qti\expression\ExpressionParserFactory;
use oat\taoQtiItem\model\qti\response\SimpleFeedbackRule;
use oat\taoQtiItem\model\qti\QtiObject;
use oat\taoQtiItem\model\qti\Img;
use oat\taoQtiItem\model\qti\Math;
use oat\taoQtiItem\model\qti\XInclude;
use oat\taoQtiItem\model\qti\Stylesheet;
use oat\taoQtiItem\model\qti\RubricBlock;
use oat\taoQtiItem\model\qti\container\ContainerFeedbackInteractive;
use oat\taoQtiItem\model\qti\container\ContainerStatic;
use \DOMDocument;
use \DOMXPath;
use \DOMElement;
use \common_Logger;
use \SimpleXMLElement;
use oat\oatbox\service\ServiceManager;
use oat\taoQtiItem\model\portableElement\model\PortableModelRegistry;
use oat\oatbox\log\LoggerAwareTrait;
/**
* The ParserFactory provides some methods to build the QTI_Data objects from an
* element.
* SimpleXML is used as source to build the model.
*
* @access public
* @author Joel Bout, <joel.bout@tudor.lu>
* @package taoQTI
*/
class ParserFactory
{
use LoggerAwareTrait;
protected $data = null;
/** @var \oat\taoQtiItem\model\qti\Item */
protected $item = null;
protected $attributeMap = ['lang' => 'xml:lang'];
public function __construct(DOMDocument $data)
{
$this->data = $data;
$this->xpath = new DOMXPath($data);
}
/**
* @param \oat\taoQtiItem\model\qti\Item $item
*/
public function setItem(Item $item)
{
$this->item = $item;
}
public function load()
{
$item = null;
if (!is_null($this->data)) {
$item = $this->buildItem($this->data->documentElement);
}
return $item;
}
protected function saveXML(DOMElement $data)
{
return $data->ownerDocument->saveXML($data);
}
/**
* Get the body data (markups) of an element.
* @param \DOMElement $data the element
* @param boolean $removeNamespace if XML namespaces should be removed
* @param boolean $keepEmptyTags if true, the empty tags are kept expanded (useful when tags are HTML)
* @return string the body data (XML markup)
*/
public function getBodyData(DOMElement $data, $removeNamespace = false, $keepEmptyTags = false)
{
//prepare the data string
$bodyData = '';
$saveOptions = $keepEmptyTags ? LIBXML_NOEMPTYTAG : 0;
$children = $data->childNodes;
foreach ($children as $child) {
$bodyData .= $data->ownerDocument->saveXML($child, $saveOptions);
}
if ($removeNamespace) {
$bodyData = preg_replace('/<(\/)?(\w*):/i', '<$1', $bodyData);
}
return $bodyData;
}
protected function replaceNode(DOMElement $node, Element $element)
{
$placeholder = $this->data->createTextNode($element->getPlaceholder());
$node->parentNode->replaceChild($placeholder, $node);
}
protected function deleteNode(DOMElement $node)
{
$node->parentNode->removeChild($node);
}
public function queryXPath($query, DOMElement $contextNode = null)
{
if (is_null($contextNode)) {
return $this->xpath->query($query);
} else {
return $this->xpath->query($query, $contextNode);
}
}
public function queryXPathChildren($paths = [], DOMElement $contextNode = null, $ns = '')
{
$query = '.';
$ns = empty($ns) ? '' : $ns . ':';
foreach ($paths as $path) {
$query .= "/*[name(.)='" . $ns . $path . "']";
}
return $this->queryXPath($query, $contextNode);
}
public function loadContainerStatic(DOMElement $data, Container $container)
{
$this->parseContainerStatic($data, $container);
}
protected function parseContainerStatic(DOMElement $data, Container $container)
{
//initialize elements array to collect all QTI elements
$bodyElements = [];
//parse for feedback elements
//warning: parse feedback elements before any other because feedback may contain them!
$feedbackNodes = $this->queryXPath(".//*[not(ancestor::feedbackBlock) and not(ancestor::feedbackInline) and contains(name(.), 'feedback')]", $data);
foreach ($feedbackNodes as $feedbackNode) {
$feedback = $this->buildFeedback($feedbackNode);
if (!is_null($feedback)) {
$bodyElements[$feedback->getSerial()] = $feedback;
$this->replaceNode($feedbackNode, $feedback);
}
}
// parse for QTI elements within item body
// parse the remaining tables, those that does not contain any interaction.
//warning: parse table elements before any other because table may contain them!
$tableNodes = $this->queryXPath(".//*[not(ancestor::*[name()='table']) and name()='table']", $data);
foreach ($tableNodes as $tableNode) {
$table = $this->buildTable($tableNode);
if (!is_null($table)) {
$bodyElements[$table->getSerial()] = $table;
$this->replaceNode($tableNode, $table);
}
}
$tooltipNodes = $this->queryXPath(".//*[@data-role='tooltip-target']", $data);
foreach ($tooltipNodes as $tooltipNode) {
$tooltip = $this->buildTooltip($tooltipNode, $data);
if (!is_null($tooltip)) {
$bodyElements[$tooltip->getSerial()] = $tooltip;
$this->replaceNode($tooltipNode, $tooltip);
}
}
$objectNodes = $this->queryXPath(".//*[name(.)='object']", $data);
foreach ($objectNodes as $objectNode) {
if (!in_array('object', $this->getAncestors($objectNode))) {
$object = $this->buildObject($objectNode);
if (!is_null($object)) {
$bodyElements[$object->getSerial()] = $object;
$this->replaceNode($objectNode, $object);
}
}
}
$imgNodes = $this->queryXPath(".//*[name(.)='img']", $data);
foreach ($imgNodes as $imgNode) {
$img = $this->buildImg($imgNode);
if (!is_null($img)) {
$bodyElements[$img->getSerial()] = $img;
$this->replaceNode($imgNode, $img);
}
}
$ns = $this->getMathNamespace();
$ns = empty($ns) ? '' : $ns . ':';
$mathNodes = $this->queryXPath(".//*[name(.)='" . $ns . "math']", $data);
foreach ($mathNodes as $mathNode) {
$math = $this->buildMath($mathNode);
if (!is_null($math)) {
$bodyElements[$math->getSerial()] = $math;
$this->replaceNode($mathNode, $math);
}
}
$ns = $this->getXIncludeNamespace();
$ns = empty($ns) ? '' : $ns . ':';
$xincludeNodes = $this->queryXPath(".//*[name(.)='" . $ns . "include']", $data);
foreach ($xincludeNodes as $xincludeNode) {
$include = $this->buildXInclude($xincludeNode);
if (!is_null($include)) {
$bodyElements[$include->getSerial()] = $include;
$this->replaceNode($xincludeNode, $include);
}
}
$printedVariableNodes = $this->queryXPath(".//*[name(.)='printedVariable']", $data);
foreach ($printedVariableNodes as $printedVariableNode) {
throw new UnsupportedQtiElement($printedVariableNode);
}
$templateNodes = $this->queryXPath(".//*[name(.)='templateBlock'] | *[name(.)='templateInline']", $data);
foreach ($templateNodes as $templateNode) {
throw new UnsupportedQtiElement($templateNode);
}
//finally, add all body elements to the body
$bodyData = $this->getBodyData($data);
//there use to be $bodyData = ItemAuthoring::cleanHTML($bodyData); there
if (empty($bodyElements)) {
$container->edit($bodyData);
} elseif (!$container->setElements($bodyElements, $bodyData)) {
throw new ParsingException('Cannot set elements to the static container');
}
return $data;
}
protected function getAncestors(DOMElement $data, $topNode = 'itemBody')
{
$ancestors = [];
$parentNodeName = '';
$currentNode = $data;
$i = 0;
while (!is_null($currentNode->parentNode) && $parentNodeName != $topNode) {
if ($i > 100) {
throw new ParsingException('maximum recursion of 100 reached');
}
$parentNodeName = $currentNode->parentNode->nodeName;
$ancestors[] = $parentNodeName;
$currentNode = $currentNode->parentNode;
$i++;
}
return $ancestors;
}
protected function parseContainerInteractive(DOMElement $data, ContainerInteractive $container)
{
$bodyElements = [];
//parse the xml to find the interaction nodes
$interactionNodes = $this->queryXPath(".//*[not(ancestor::feedbackBlock) and not(ancestor::feedbackInline) and contains(name(.), 'Interaction')]", $data);
foreach ($interactionNodes as $k => $interactionNode) {
if (strpos($interactionNode->nodeName, 'portableCustomInteraction') === false) {
//build an interaction instance
$interaction = $this->buildInteraction($interactionNode);
if (!is_null($interaction)) {
$bodyElements[$interaction->getSerial()] = $interaction;
$this->replaceNode($interactionNode, $interaction);
}
}
}
//parse for feedback elements interactive!
$feedbackNodes = $this->queryXPath(".//*[not(ancestor::feedbackBlock) and not(ancestor::feedbackInline) and contains(name(.), 'feedback')]", $data);
foreach ($feedbackNodes as $feedbackNode) {
$feedback = $this->buildFeedback($feedbackNode, true);
if (!is_null($feedback)) {
$bodyElements[$feedback->getSerial()] = $feedback;
$this->replaceNode($feedbackNode, $feedback);
}
}
$bodyData = $this->getBodyData($data);
foreach ($bodyElements as $bodyElement) {
if (strpos($bodyData, $bodyElement->getPlaceholder()) === false) {
unset($bodyElements[$bodyElement->getSerial()]);
}
}
if (!$container->setElements($bodyElements, $bodyData)) {
throw new ParsingException('Cannot set elements to the interactive container');
}
return $this->parseContainerStatic($data, $container);
}
protected function setContainerElements(Container $container, DOMElement $data, $bodyElements = [])
{
$bodyData = $this->getBodyData($data);
foreach ($bodyElements as $bodyElement) {
if (strpos($bodyData, $bodyElement->getPlaceholder()) === false) {
unset($bodyElements[$bodyElement->getSerial()]);
}
}
if (!$container->setElements($bodyElements, $bodyData)) {
throw new ParsingException('Cannot set elements to the interactive container');
}
}
protected function parseContainerItemBody(DOMElement $data, ContainerItemBody $container)
{
$bodyElements = [];
//parse for rubricBlocks: rubricBlock only allowed in item body !
$rubricNodes = $this->queryXPath(".//*[name(.)='rubricBlock']", $data);
foreach ($rubricNodes as $rubricNode) {
$rubricBlock = $this->buildRubricBlock($rubricNode);
if (!is_null($rubricBlock)) {
$bodyElements[$rubricBlock->getSerial()] = $rubricBlock;
$this->replaceNode($rubricNode, $rubricBlock);
}
}
//parse for infoControls: infoControl only allowed in item body !
$infoControlNodes = $this->queryXPath(".//*[name(.)='infoControl']", $data);
foreach ($infoControlNodes as $infoControlNode) {
$infoControl = $this->buildInfoControl($infoControlNode);
if (!is_null($infoControl)) {
$bodyElements[$infoControl->getSerial()] = $infoControl;
$this->replaceNode($infoControlNode, $infoControl);
}
}
// parse for tables, but only the ones containing interactions
$tableNodes = $this->queryXPath(".//*[name(.)='table']", $data);
foreach ($tableNodes as $tableNode) {
$interactionsNodes = $this->queryXPath(".//*[contains(name(.), 'Interaction')]", $tableNode);
if ($interactionsNodes->length > 0) {
$table = $this->buildTable($tableNode);
if (!is_null($table)) {
$bodyElements[$table->getSerial()] = $table;
$this->replaceNode($tableNode, $table);
$this->parseContainerInteractive($tableNode, $table->getBody());
}
}
}
$this->setContainerElements($container, $data, $bodyElements);
return $this->parseContainerInteractive($data, $container);
}
private function parseContainerChoice(DOMElement $data, Container $container, $tag)
{
$choices = [];
$gapNodes = $this->queryXPath(".//*[name(.)='" . $tag . "']", $data);
foreach ($gapNodes as $gapNode) {
$gap = $this->buildChoice($gapNode);
if (!is_null($gap)) {
$choices[$gap->getSerial()] = $gap;
$this->replaceNode($gapNode, $gap);
}
}
$bodyData = $this->getBodyData($data);
$container->setElements($choices, $bodyData);
$data = $this->parseContainerStatic($data, $container);
return $data;
}
protected function parseContainerGap(DOMElement $data, ContainerGap $container)
{
return $this->parseContainerChoice($data, $container, 'gap');
}
protected function parseContainerHottext(DOMElement $data, ContainerHottext $container)
{
return $this->parseContainerChoice($data, $container, 'hottext');
}
protected function extractAttributes(DOMElement $data)
{
$options = [];
foreach ($data->attributes as $attr) {
if ($attr->nodeName === 'xsi:schemaLocation') {
continue;
}
$options[isset($this->attributeMap[$attr->nodeName]) ? $this->attributeMap[$attr->nodeName] : $attr->nodeName] = (string) $attr->nodeValue;
}
return $options;
}
public function findNamespace($nsFragment)
{
$returnValue = '';
if (is_null($this->item)) {
foreach ($this->queryXPath('namespace::*') as $node) {
$name = preg_replace('/xmlns(:)?/', '', $node->nodeName);
$uri = $node->nodeValue;
if (strpos($uri, $nsFragment) > 0) {
$returnValue = $name;
break;
}
}
} else {
$namespaces = $this->item->getNamespaces();
foreach ($namespaces as $name => $uri) {
if (strpos($uri, $nsFragment) > 0) {
$returnValue = $name;
break;
}
}
if ($returnValue === '') {
$returnValue = $this->recursivelyFindNamespace($this->data, $nsFragment);
}
}
return $returnValue;
}
private function recursivelyFindNamespace($element, $nsFragment)
{
if (strpos($this->data->saveXML(), $nsFragment) === false) {
return '';
}
$returnValue = '';
foreach ($element->childNodes as $child) {
if ($child->nodeType === XML_ELEMENT_NODE) {
foreach ($this->queryXPath('namespace::*', $child) as $node) {
$name = preg_replace('/xmlns(:)?/', '', $node->nodeName);
$uri = $node->nodeValue;
if (strpos($uri, $nsFragment) > 0) {
$returnValue = $name;
break;
}
}
$value = $this->recursivelyFindNamespace($child, $nsFragment);
if ($value !== '') {
$returnValue = $value;
}
}
}
return $returnValue;
}
protected function getMathNamespace()
{
return $this->findNamespace('MathML');
}
protected function getXIncludeNamespace()
{
return $this->findNamespace('XInclude');
}
/**
* Build a QTI_Item from a DOMElement, the root tag of which is root assessmentItem
*
* @param DOMElement $data
* @return \oat\taoQtiItem\model\qti\Item
* @throws InvalidArgumentException
* @throws ParsingException
* @throws UnsupportedQtiElement
*/
protected function buildItem(DOMElement $data)
{
//check on the root tag.
$itemId = (string) $data->getAttribute('identifier');
$this->logDebug('Started parsing of QTI item' . (isset($itemId) ? ' ' . $itemId : ''), ['TAOITEMS']);
//create the item instance
$this->item = new Item($this->extractAttributes($data));
//load xml ns and schema locations
$this->loadNamespaces();
$this->loadSchemaLocations($data);
//load stylesheets
$styleSheetNodes = $this->queryXPath("*[name(.) = 'stylesheet']", $data);
foreach ($styleSheetNodes as $styleSheetNode) {
$styleSheet = $this->buildStylesheet($styleSheetNode);
$this->item->addStylesheet($styleSheet);
}
//extract the responses
$responseNodes = $this->queryXPath("*[name(.) = 'responseDeclaration']", $data);
foreach ($responseNodes as $responseNode) {
$response = $this->buildResponseDeclaration($responseNode);
if (!is_null($response)) {
$this->item->addResponse($response);
}
}
//extract outcome variables
$outcomes = [];
$outComeNodes = $this->queryXPath("*[name(.) = 'outcomeDeclaration']", $data);
foreach ($outComeNodes as $outComeNode) {
$outcome = $this->buildOutcomeDeclaration($outComeNode);
if (!is_null($outcome)) {
$outcomes[] = $outcome;
}
}
if (count($outcomes) > 0) {
$this->item->setOutcomes($outcomes);
}
//extract modal feedbacks
$feedbackNodes = $this->queryXPath("*[name(.) = 'modalFeedback']", $data);
foreach ($feedbackNodes as $feedbackNode) {
$modalFeedback = $this->buildFeedback($feedbackNode);
if (!is_null($modalFeedback)) {
$this->item->addModalFeedback($modalFeedback);
}
}
//extract the item structure to separate the structural/style content to the item content
$itemBodies = $this->queryXPath("*[name(.) = 'itemBody']", $data); // array with 1 or zero bodies
if ($itemBodies === false) {
$errors = libxml_get_errors();
if (count($errors) > 0) {
$error = array_shift($errors);
$errormsg = $error->message;
} else {
$errormsg = "without errormessage";
}
throw new ParsingException('XML error(' . $errormsg . ') on itemBody read' . (isset($itemId) ? ' for item ' . $itemId : ''));
} elseif ($itemBodies->length) {
$this->parseContainerItemBody($itemBodies->item(0), $this->item->getBody());
$this->item->addClass($itemBodies->item(0)->getAttribute('class'));
}
//warning: extract the response processing at the latest to make oat\taoQtiItem\model\qti\response\TemplatesDriven::takeOverFrom() work
$rpNodes = $this->queryXPath("*[name(.) = 'responseProcessing']", $data);
if ($rpNodes->length === 0) {
//no response processing node found: the template for an empty response processing is simply "NONE"
$rProcessing = new TemplatesDriven();
$rProcessing->setRelatedItem($this->item);
foreach ($this->item->getInteractions() as $interaction) {
$rProcessing->setTemplate($interaction->getResponse(), Template::NONE);
}
$this->item->setResponseProcessing($rProcessing);
} else {
//if there is a response processing node, try parsing it
$rpNode = $rpNodes->item(0);
$rProcessing = $this->buildResponseProcessing($rpNode, $this->item);
if (!is_null($rProcessing)) {
$this->item->setResponseProcessing($rProcessing);
}
}
$this->buildApipAccessibility($data);
return $this->item;
}
/**
* Load xml namespaces into the item model
*/
protected function loadNamespaces()
{
$namespaces = [];
foreach ($this->queryXPath('namespace::*') as $node) {
$name = preg_replace('/xmlns(:)?/', '', $node->nodeName);
if ($name !== 'xml') {//always removed the implicit xml namespace
$namespaces[$name] = $node->nodeValue;
}
}
ksort($namespaces);
foreach ($namespaces as $name => $uri) {
$this->item->addNamespace($name, $uri);
}
}
/**
* Load xml schema locations into the item model
*
* @param DOMElement $itemData
* @throws ParsingException
*/
protected function loadSchemaLocations(DOMElement $itemData)
{
$schemaLoc = preg_replace('/\s+/', ' ', trim($itemData->getAttributeNS($itemData->lookupNamespaceURI('xsi'), 'schemaLocation')));
$schemaLocToken = explode(' ', $schemaLoc);
$schemaCount = count($schemaLocToken);
if ($schemaCount % 2) {
throw new ParsingException('invalid schema location');
}
for ($i = 0; $i < $schemaCount; $i = $i + 2) {
$this->item->addSchemaLocation($schemaLocToken[$i], $schemaLocToken[$i + 1]);
}
}
protected function buildApipAccessibility(DOMElement $data)
{
$ApipNodes = $this->queryXPath("*[name(.) = 'apipAccessibility']|*[name(.) = 'apip:apipAccessibility']", $data);
if ($ApipNodes->length > 0) {
common_Logger::i('is APIP item', ['QTI', 'TAOITEMS']);
$apipNode = $ApipNodes->item(0);
$apipXml = $apipNode->ownerDocument->saveXML($apipNode);
$this->item->setApipAccessibility($apipXml);
}
}
/**
* Build a QTI_Interaction from a DOMElement (the root tag of this is an 'interaction' node)
*
* @access public
* @author Joel Bout, <joel.bout@tudor.lu>
* @param DOMElement $data
* @return \oat\taoQtiItem\model\qti\interaction\Interaction
* @throws ParsingException
* @throws UnsupportedQtiElement
* @throws interaction\InvalidArgumentException
* @see http://www.imsglobal.org/question/qti_v2p0/imsqti_infov2p0.html#element10247
*/
protected function buildInteraction(DOMElement $data)
{
$returnValue = null;
if ($data->nodeName === 'customInteraction') {
$returnValue = $this->buildCustomInteraction($data);
} else {
//build one of the standard interaction
try {
$type = ucfirst($data->nodeName);
$interactionClass = '\\oat\\taoQtiItem\\model\\qti\\interaction\\' . $type;
if (!class_exists($interactionClass)) {
throw new ParsingException('The interaction class cannot be found: ' . $interactionClass);
}
$myInteraction = new $interactionClass($this->extractAttributes($data), $this->item);
if ($myInteraction instanceof BlockInteraction) {
//extract prompt:
$promptNodes = $this->queryXPath("*[name(.) = 'prompt']", $data); //prompt
foreach ($promptNodes as $promptNode) {
//only block interactions have prompt
$this->parseContainerStatic($promptNode, $myInteraction->getPrompt());
$this->deleteNode($promptNode);
}
}
//build the interaction's choices regarding it's type
switch (strtolower($type)) {
case 'matchinteraction':
//extract simpleMatchSet choices
$matchSetNodes = $this->queryXPath("*[name(.) = 'simpleMatchSet']", $data); //simpleMatchSet
$matchSetNumber = 0;
foreach ($matchSetNodes as $matchSetNode) {
$choiceNodes = $this->queryXPath("*[name(.) = 'simpleAssociableChoice']", $matchSetNode); //simpleAssociableChoice
foreach ($choiceNodes as $choiceNode) {
$choice = $this->buildChoice($choiceNode);
if (!is_null($choice)) {
$myInteraction->addChoice($choice, $matchSetNumber);
}
}
if (++$matchSetNumber === 2) {
//matchSet is limited to 2 maximum
break;
}
}
break;
case 'gapmatchinteraction':
//create choices with the gapText nodes
$choiceNodes = $this->queryXPath("*[name(.)='gapText']", $data); //or gapImg!!
$choices = [];
foreach ($choiceNodes as $choiceNode) {
$choice = $this->buildChoice($choiceNode);
if (!is_null($choice)) {
$myInteraction->addChoice($choice);
$this->deleteNode($choiceNode);
}
//remove node so it does not pollute subsequent parsing data
unset($choiceNode);
}
$this->parseContainerGap($data, $myInteraction->getBody());
break;
case 'hottextinteraction':
$this->parseContainerHottext($data, $myInteraction->getBody());
break;
case 'graphicgapmatchinteraction':
//create choices with the gapImg nodes
$choiceNodes = $this->queryXPath("*[name(.)='gapImg']", $data);
$choices = [];
foreach ($choiceNodes as $choiceNode) {
$choice = $this->buildChoice($choiceNode);
if (!is_null($choice)) {
$myInteraction->addGapImg($choice);
}
}
default:
//parse, extract and build the choice nodes contained in the interaction
$exp = "*[contains(name(.),'Choice')] | *[name(.)='associableHotspot']";
$choiceNodes = $this->queryXPath($exp, $data);
foreach ($choiceNodes as $choiceNode) {
$choice = $this->buildChoice($choiceNode);
if (!is_null($choice)) {
$myInteraction->addChoice($choice);
}
unset($choiceNode);
}
break;
}
if ($myInteraction instanceof ObjectInteraction) {
$objectNodes = $this->queryXPath("*[name(.)='object']", $data); //object
foreach ($objectNodes as $objectNode) {
$object = $this->buildObject($objectNode);
if (!is_null($object)) {
$myInteraction->setObject($object);
}
}
}
$returnValue = $myInteraction;
} catch (InvalidArgumentException $iae) {
throw new ParsingException($iae);
}
}
return $returnValue;
}
/**
* Build a QTI_Choice from a DOMElement (the root tag of this element
* an 'choice' node)
*
* @access public
* @author Joel Bout, <joel.bout@tudor.lu>
* @param DOMElement $data
* @return \oat\taoQtiItem\model\qti\choice\Choice
* @throws ParsingException
* @throws UnsupportedQtiElement
* @throws choice\InvalidArgumentException
* @see http://www.imsglobal.org/question/qti_v2p0/imsqti_infov2p0.html#element10254
*/
protected function buildChoice(DOMElement $data)
{
$className = '\\oat\\taoQtiItem\\model\\qti\\choice\\' . ucfirst($data->nodeName);
if (!class_exists($className)) {
throw new ParsingException("The choice class does not exist " . $className);
}
$myChoice = new $className($this->extractAttributes($data));
if ($myChoice instanceof ContainerChoice) {
$this->parseContainerStatic($data, $myChoice->getBody());
} elseif ($myChoice instanceof TextVariableChoice) {
//use getBodyData() instead of $data->nodeValue() to preserve xml entities
$myChoice->setContent($this->getBodyData($data));
} elseif ($myChoice instanceof GapImg) {
//extract the media object tag
$objectNodes = $this->queryXPath("*[name(.)='object']", $data);
foreach ($objectNodes as $objectNode) {
$object = $this->buildObject($objectNode);
$myChoice->setContent($object);
break;
}
}
return $myChoice;
}
/**
* Short description of method buildResponseDeclaration
*
* @access public
* @author Joel Bout, <joel.bout@tudor.lu>
* @param DOMElement $data
* @return \oat\taoQtiItem\model\qti\ResponseDeclaration
* @see http://www.imsglobal.org/question/qti_v2p0/imsqti_infov2p0.html#element10074
*/
protected function buildResponseDeclaration(DOMElement $data)
{
$myResponse = new ResponseDeclaration($this->extractAttributes($data), $this->item);
$data = simplexml_import_dom($data);
//set the correct responses
$correctResponseNodes = $data->xpath("*[name(.) = 'correctResponse']");
$responses = [];
foreach ($correctResponseNodes as $correctResponseNode) {
foreach ($correctResponseNode->value as $value) {
$correct = (string) $value;
$response = new Value();
foreach ($value->attributes() as $attrName => $attrValue) {
$response->setAttribute($attrName, strval($attrValue));
}
$response->setValue($correct);
$responses[] = $response;
}
break;
}
$myResponse->setCorrectResponses($responses);
//set the correct responses
$defaultValueNodes = $data->xpath("*[name(.) = 'defaultValue']");
$defaultValues = [];
foreach ($defaultValueNodes as $defaultValueNode) {
foreach ($defaultValueNode->value as $value) {
$default = (string) $value;
$defaultValue = new Value();
foreach ($value->attributes() as $attrName => $attrValue) {
$defaultValue->setAttribute($attrName, strval($attrValue));
}
$defaultValue->setValue($default);
$defaultValues[] = $defaultValue;
}
break;
}
$myResponse->setDefaultValue($defaultValues);
//set the mapping if defined
$mappingNodes = $data->xpath("*[name(.) = 'mapping']");
foreach ($mappingNodes as $mappingNode) {
if (isset($mappingNode['defaultValue'])) {
$myResponse->setMappingDefaultValue(floatval((string) $mappingNode['defaultValue']));
}
$mappingOptions = [];
foreach ($mappingNode->attributes() as $key => $value) {
if ($key != 'defaultValue') {
$mappingOptions[$key] = (string) $value;
}
}
$myResponse->setAttribute('mapping', $mappingOptions);
$mapping = [];
foreach ($mappingNode->mapEntry as $mapEntry) {
$mapping[(string) htmlspecialchars($mapEntry['mapKey'])] = (string) $mapEntry['mappedValue'];
}
$myResponse->setMapping($mapping);
break;
}
//set the areaMapping if defined
$mappingNodes = $data->xpath("*[name(.) = 'areaMapping']");
foreach ($mappingNodes as $mappingNode) {
if (isset($mappingNode['defaultValue'])) {
$myResponse->setMappingDefaultValue(floatval((string) $mappingNode['defaultValue']));
}
$mappingOptions = [];
foreach ($mappingNode->attributes() as $key => $value) {
if ($key != 'defaultValue') {
$mappingOptions[$key] = (string) $value;
}
}
$myResponse->setAttribute('areaMapping', $mappingOptions);
$mapping = [];
foreach ($mappingNode->areaMapEntry as $mapEntry) {
$mappingAttributes = [];
foreach ($mapEntry->attributes() as $key => $value) {
$mappingAttributes[(string) $key] = (string) $value;
}
$mapping[] = $mappingAttributes;
}
$myResponse->setMapping($mapping, 'area');
break;
}
return $myResponse;
}
/**
* Short description of method buildOutcomeDeclaration
*
* @access public
* @author Joel Bout, <joel.bout@tudor.lu>
* @param DOMElement data
* @return oat\taoQtiItem\model\qti\OutcomeDeclaration
*/
protected function buildOutcomeDeclaration(DOMElement $data)
{
$outcome = new OutcomeDeclaration($this->extractAttributes($data));
$data = simplexml_import_dom($data);
if (isset($data->defaultValue)) {
if (!is_null($data->defaultValue->value)) {
$outcome->setDefaultValue((string) $data->defaultValue->value);
}
}
return $outcome;
}
/**
* Short description of method buildTemplateResponseProcessing
*
* @access public
* @author Joel Bout, <joel.bout@tudor.lu>
* @param DOMElement data
* @return oat\taoQtiItem\model\qti\response\ResponseProcessing
*/
protected function buildTemplateResponseProcessing(DOMElement $data)
{
$returnValue = null;
if ($data->hasAttribute('template') && $data->childNodes->length === 0) {
$templateUri = (string) $data->getAttribute('template');
$returnValue = new Template($templateUri);
} elseif ($data->childNodes->length === 1) {
//check response declaration identifier, which must be RESPONSE in standard rp
$responses = $this->item->getResponses();
if (count($responses) == 1) {
$response = reset($responses);
if ($response->getIdentifier() !== 'RESPONSE') {
throw new UnexpectedResponseProcessing('the response declaration identifier must be RESPONSE');
}
} else {
//invalid number of response declaration
throw new UnexpectedResponseProcessing('the item must have exactly one response declaration');
}
$patternCorrectIMS = 'responseCondition [count(./*) = 2 ] [name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "match" ] [name(./responseIf/match/*[1]) = "variable" ] [name(./responseIf/match/*[2]) = "correct" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] [name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ] [name(./*[2]) = "responseElse" ] [count(./responseElse/*) = 1 ] [name(./responseElse/*[1]) = "setOutcomeValue" ] [name(./responseElse/setOutcomeValue/*[1]) = "baseValue"]';
$patternMappingIMS = 'responseCondition [count(./*) = 2] [name(./*[1]) = "responseIf"] [count(./responseIf/*) = 2] [name(./responseIf/*[1]) = "isNull"] [name(./responseIf/isNull/*[1]) = "variable"] [name(./responseIf/*[2]) = "setOutcomeValue"] [name(./responseIf/setOutcomeValue/*[1]) = "variable"] [name(./*[2]) = "responseElse"] [count(./responseElse/*) = 1] [name(./responseElse/*[1]) = "setOutcomeValue"] [name(./responseElse/setOutcomeValue/*[1]) = "mapResponse"]';
$patternMappingPointIMS = 'responseCondition [count(./*) = 2] [name(./*[1]) = "responseIf"] [count(./responseIf/*) = 2] [name(./responseIf/*[1]) = "isNull"] [name(./responseIf/isNull/*[1]) = "variable"] [name(./responseIf/*[2]) = "setOutcomeValue"] [name(./responseIf/setOutcomeValue/*[1]) = "variable"] [name(./*[2]) = "responseElse"] [count(./responseElse/*) = 1] [name(./responseElse/*[1]) = "setOutcomeValue"] [name(./responseElse/setOutcomeValue/*[1]) = "mapResponsePoint"]';
if (count($this->queryXPath($patternCorrectIMS)) == 1) {
$returnValue = new Template(Template::MATCH_CORRECT);
} elseif (count($this->queryXPath($patternMappingIMS)) == 1) {
$returnValue = new Template(Template::MAP_RESPONSE);
} elseif (count($this->queryXPath($patternMappingPointIMS)) == 1) {
$returnValue = new Template(Template::MAP_RESPONSE_POINT);
} else {
throw new UnexpectedResponseProcessing('not Template, wrong rule');
}
$returnValue->setRelatedItem($this->item);
} else {
throw new UnexpectedResponseProcessing('not Template');
}
return $returnValue;
}
/**
* Short description of method buildResponseProcessing
*
* @access public
* @author Joel Bout, <joel.bout@tudor.lu>
* @param DOMElement data
* @param Item item
* @return oat\taoQtiItem\model\qti\response\ResponseProcessing
*/
protected function buildResponseProcessing(DOMElement $data, Item $item)
{
$returnValue = null;
// try template
try {
$returnValue = $this->buildTemplateResponseProcessing($data);
try {
//warning: require to add interactions to the item to make it work
$returnValue = TemplatesDriven::takeOverFrom($returnValue, $item);
} catch (TakeoverFailedException $e) {
}
} catch (UnexpectedResponseProcessing $e) {
}
//try templatedriven
if (is_null($returnValue)) {
try {
$returnValue = $this->buildTemplatedrivenResponse($data, $item->getInteractions());
} catch (UnexpectedResponseProcessing $e) {
}
}
// build custom
if (is_null($returnValue)) {
try {
$returnValue = $this->buildCustomResponseProcessing($data);
} catch (UnexpectedResponseProcessing $e) {
// not a Template
common_Logger::e('custom response processing failed', ['TAOITEMS', 'QTI']);
}
}
if (is_null($returnValue)) {
common_Logger::w('failed to determine ResponseProcessing');
}
return $returnValue;
}
/**
* Short description of method buildCompositeResponseProcessing
*
* @access public
* @author Joel Bout, <joel.bout@tudor.lu>
* @param DOMElement data
* @param Item item
* @return oat\taoQtiItem\model\qti\response\ResponseProcessing
*/
protected function buildCompositeResponseProcessing(DOMElement $data, Item $item)
{
$returnValue = null;
// STRONGLY simplified summation detection
$patternCorrectTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "match" ] [name(./responseIf/match/*[1]) = "variable" ] [name(./responseIf/match/*[2]) = "correct" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] [count(./responseIf/setOutcomeValue/*) = 1 ] [name(./responseIf/setOutcomeValue/*[1]) = "baseValue"]';
$patternMapTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "not" ] [count(./responseIf/not/*) = 1 ] [name(./responseIf/not/*[1]) = "isNull" ] [count(./responseIf/not/isNull/*) = 1 ] [name(./responseIf/not/isNull/*[1]) = "variable" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] [count(./responseIf/setOutcomeValue/*) = 1 ] [name(./responseIf/setOutcomeValue/*[1]) = "mapResponse"]';
$patternMapPointTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "not" ] [count(./responseIf/not/*) = 1 ] [name(./responseIf/not/*[1]) = "isNull" ] [count(./responseIf/not/isNull/*) = 1 ] [name(./responseIf/not/isNull/*[1]) = "variable" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] [count(./responseIf/setOutcomeValue/*) = 1 ] [name(./responseIf/setOutcomeValue/*[1]) = "mapResponsePoint"]';
$patternNoneTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "isNull" ] [count(./responseIf/isNull/*) = 1 ] [name(./responseIf/isNull/*[1]) = "variable" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] [count(./responseIf/setOutcomeValue/*) = 1 ] [name(./responseIf/setOutcomeValue/*[1]) = "baseValue"]';
$possibleSummation = '/setOutcomeValue [count(./*) = 1 ] [name(./*[1]) = "sum" ]';
$irps = [];
$composition = null;
$data = simplexml_import_dom($data);
foreach ($data as $responseRule) {
if (!is_null($composition)) {
throw new UnexpectedResponseProcessing('Not composite, rules after composition');
}
$subtree = new SimpleXMLElement($responseRule->asXML());
if (count($subtree->xpath($patternCorrectTAO)) > 0) {
$responseIdentifier = (string) $subtree->responseIf->match->variable[0]['identifier'];
$irps[$responseIdentifier] = [
'class' => 'MatchCorrectTemplate',
'outcome' => (string) $subtree->responseIf->setOutcomeValue[0]['identifier']
];
} elseif (count($subtree->xpath($patternMapTAO)) > 0) {
$responseIdentifier = (string) $subtree->responseIf->not->isNull->variable[0]['identifier'];
$irps[$responseIdentifier] = [
'class' => 'MapResponseTemplate',
'outcome' => (string) $subtree->responseIf->setOutcomeValue[0]['identifier']
];
} elseif (count($subtree->xpath($patternMapPointTAO)) > 0) {
$responseIdentifier = (string) $subtree->responseIf->not->isNull->variable[0]['identifier'];
$irps[$responseIdentifier] = [
'class' => 'MapResponsePointTemplate',
'outcome' => (string) $subtree->responseIf->setOutcomeValue[0]['identifier']
];
} elseif (count($subtree->xpath($patternNoneTAO)) > 0) {
$responseIdentifier = (string) $subtree->responseIf->isNull->variable[0]['identifier'];
$irps[$responseIdentifier] = [
'class' => 'None',
'outcome' => (string) $subtree->responseIf->setOutcomeValue[0]['identifier'],
'default' => (string) $subtree->responseIf->setOutcomeValue[0]->baseValue[0]
];
} elseif (count($subtree->xpath($possibleSummation)) > 0) {
$composition = 'Summation';
$outcomesUsed = [];
foreach ($subtree->xpath('/setOutcomeValue/sum/variable') as $var) {
$outcomesUsed[] = (string) $var[0]['identifier'];
}
} else {
throw new UnexpectedResponseProcessing('Not composite, unknown rule');
}
}
if (is_null($composition)) {
throw new UnexpectedResponseProcessing('Not composit, Composition rule missing');
}
$responses = [];
foreach ($item->getInteractions() as $interaction) {
$responses[$interaction->getResponse()->getIdentifier()] = $interaction->getResponse();
}
if (count(array_diff(array_keys($irps), array_keys($responses))) > 0) {
throw new UnexpectedResponseProcessing('Not composite, no responses for rules: ' . implode(',', array_diff(array_keys($irps), array_keys($responses))));
}
if (count(array_diff(array_keys($responses), array_keys($irps))) > 0) {
throw new UnexpectedResponseProcessing('Not composite, no support for unmatched variables yet');
}
//assuming sum is correct
$compositonRP = new Summation($item);
foreach ($responses as $id => $response) {
$outcome = null;
foreach ($item->getOutcomes() as $possibleOutcome) {
if ($possibleOutcome->getIdentifier() == $irps[$id]['outcome']) {
$outcome = $possibleOutcome;
break;
}
}
if (is_null($outcome)) {
throw new ParsingException('Undeclared Outcome in ResponseProcessing');
}
$classname = '\\oat\\taoQtiItem\\model\\qti\\response\\interactionResponseProcessing\\' . $irps[$id]['class'];
$irp = new $classname($response, $outcome);
if ($irp instanceof \oat\taoQtiItem\model\qti\response\interactionResponseProcessing\None && isset($irps[$id]['default'])) {
$irp->setDefaultValue($irps[$id]['default']);
}
$compositonRP->add($irp);
}
$returnValue = $compositonRP;
return $returnValue;
}
/**
* Short description of method buildCustomResponseProcessing
*
* @access public
* @author Joel Bout, <joel.bout@tudor.lu>
* @param DOMElement data
* @return oat\taoQtiItem\model\qti\response\ResponseProcessing
*/
protected function buildCustomResponseProcessing(DOMElement $data)
{
// Parse to find the different response rules
$responseRules = [];
$data = simplexml_import_dom($data);
$returnValue = new Custom($responseRules, $data->asXml());
return $returnValue;
}
/**
* Short description of method buildExpression
*
* @access public
* @author Joel Bout, <joel.bout@tudor.lu>
* @param DOMElement data
* @return oat\taoQtiItem\model\qti\response\Rule
*/
protected function buildExpression(DOMElement $data)
{
$data = simplexml_import_dom($data);
return ExpressionParserFactory::build($data);
}
protected function getModalFeedback($identifier)
{
foreach ($this->item->getModalFeedbacks() as $feedback) {
if ($feedback->getIdentifier() == $identifier) {
return $feedback;
}
}
throw new ParsingException('cannot found the modal feedback with identifier ' . $identifier);
}
protected function getOutcome($identifier)
{
foreach ($this->item->getOutcomes() as $outcome) {
if ($outcome->getIdentifier() == $identifier) {
return $outcome;
}
}
throw new ParsingException('cannot found the outcome with identifier ' . $identifier);
}
protected function getResponse($identifier)
{
foreach ($this->item->getResponses() as $response) {
if ($response->getIdentifier() == $identifier) {
return $response;
}
}
throw new ParsingException('cannot found the response with identifier ' . $identifier);
}
/**
* Short description of method buildTemplatedrivenResponse
*
* @access public
* @author Joel Bout, <joel.bout@tudor.lu>
* @param DOMElement $data
* @param $interactions
* @return TemplatesDriven
* @throws UnexpectedResponseProcessing
* @throws exception\QtiModelException
* @throws response\InvalidArgumentException
*/
protected function buildTemplatedrivenResponse(DOMElement $data, $interactions)
{
$patternCorrectTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "match" ] [name(./responseIf/match/*[1]) = "variable" ] [name(./responseIf/match/*[2]) = "correct" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] [name(./responseIf/setOutcomeValue/*[1]) = "sum" ] [name(./responseIf/setOutcomeValue/sum/*[1]) = "variable" ] [name(./responseIf/setOutcomeValue/sum/*[2]) = "baseValue"]';
$patternMappingTAO = '/responseCondition [count(./*) = 1] [name(./*[1]) = "responseIf"] [count(./responseIf/*) = 2] [name(./responseIf/*[1]) = "not"] [name(./responseIf/not/*[1]) = "isNull"] [name(./responseIf/not/isNull/*[1]) = "variable"] [name(./responseIf/*[2]) = "setOutcomeValue"] [name(./responseIf/setOutcomeValue/*[1]) = "sum"] [name(./responseIf/setOutcomeValue/sum/*[1]) = "variable"] [name(./responseIf/setOutcomeValue/sum/*[2]) = "mapResponse"]';
$patternMappingPointTAO = '/responseCondition [count(./*) = 1] [name(./*[1]) = "responseIf"] [count(./responseIf/*) = 2] [name(./responseIf/*[1]) = "not"] [name(./responseIf/not/*[1]) = "isNull"] [name(./responseIf/not/isNull/*[1]) = "variable"] [name(./responseIf/*[2]) = "setOutcomeValue"] [name(./responseIf/setOutcomeValue/*[1]) = "sum"] [name(./responseIf/setOutcomeValue/sum/*[1]) = "variable"] [name(./responseIf/setOutcomeValue/sum/*[2]) = "mapResponsePoint"]';
$subPatternFeedbackOperatorIf = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] [contains(name(./responseIf/*[1]/*[1]), "map")] [name(./responseIf/*[1]/*[2]) = "baseValue" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] [name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ]';
$subPatternFeedbackElse = '[name(./*[2]) = "responseElse"] [count(./responseElse/*) = 1 ] [name(./responseElse/*[1]) = "setOutcomeValue"] [name(./responseElse/setOutcomeValue/*[1]) = "baseValue"]';
$subPatternFeedbackCorrect = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "match" ] [name(./responseIf/*[1]/*[1]) = "variable" ] [name(./responseIf/*[1]/*[2]) = "correct" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] [name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ]';
$subPatternFeedbackIncorrect = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "not" ] [count(./responseIf/not) = 1 ] [name(./responseIf/not/*[1]) = "match" ] [name(./responseIf/not/*[1]/*[1]) = "variable" ] [name(./responseIf/not/*[1]/*[2]) = "correct" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] [name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ]';
$subPatternFeedbackMatchChoices = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "match" ] [name(./responseIf/*[1]/*[2]) = "multiple" ] [name(./responseIf/*[1]/*[2]/*) = "baseValue" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] [name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ] ';
$subPatternFeedbackMatchChoicesEmpty = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "match" ] [name(./responseIf/*[1]/*[2]) = "multiple" ] [count(./responseIf/*[1]/*[2]/*) = 0 ] [name(./responseIf/*[2]) = "setOutcomeValue" ] [name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ] ';
$subPatternFeedbackMatchChoice = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "match" ] [name(./responseIf/*[1]/*[2]) = "baseValue" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] [name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ] ';
$patternFeedbackOperator = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackOperatorIf;
$patternFeedbackOperatorWithElse = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackOperatorIf . $subPatternFeedbackElse;
$patternFeedbackCorrect = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackCorrect;
$patternFeedbackCorrectWithElse = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackCorrect . $subPatternFeedbackElse;
$patternFeedbackIncorrect = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackIncorrect;
$patternFeedbackIncorrectWithElse = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackIncorrect . $subPatternFeedbackElse;
$patternFeedbackMatchChoices = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackMatchChoices;
$patternFeedbackMatchChoicesWithElse = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackMatchChoices . $subPatternFeedbackElse;
$patternFeedbackMatchChoice = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackMatchChoice;
$patternFeedbackMatchChoicesEmpty = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackMatchChoicesEmpty;
$patternFeedbackMatchChoicesEmptyWithElse = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackMatchChoicesEmpty . $subPatternFeedbackElse;
$patternFeedbackMatchChoice = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackMatchChoice;
$patternFeedbackMatchChoiceWithElse = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackMatchChoice . $subPatternFeedbackElse;
$rules = [];
$simpleFeedbackRules = [];
$data = simplexml_import_dom($data);
foreach ($data as $responseRule) {
$feedbackRule = null;
$subtree = new SimpleXMLElement($responseRule->asXML());
if (count($subtree->xpath($patternCorrectTAO)) > 0) {
$responseIdentifier = (string) $subtree->responseIf->match->variable['identifier'];
$rules[$responseIdentifier] = Template::MATCH_CORRECT;
} elseif (count($subtree->xpath($patternMappingTAO)) > 0) {
$responseIdentifier = (string) $subtree->responseIf->not->isNull->variable['identifier'];
$rules[$responseIdentifier] = Template::MAP_RESPONSE;
} elseif (count($subtree->xpath($patternMappingPointTAO)) > 0) {
$responseIdentifier = (string) $subtree->responseIf->not->isNull->variable['identifier'];
$rules[$responseIdentifier] = Template::MAP_RESPONSE_POINT;
} elseif (count($subtree->xpath($patternFeedbackCorrect)) > 0 || count($subtree->xpath($patternFeedbackCorrectWithElse)) > 0) {
$feedbackRule = $this->buildSimpleFeedbackRule($subtree, 'correct');
} elseif (count($subtree->xpath($patternFeedbackIncorrect)) > 0 || count($subtree->xpath($patternFeedbackIncorrectWithElse)) > 0) {
$responseIdentifier = (string) $subtree->responseIf->not->match->variable['identifier'];
$feedbackRule = $this->buildSimpleFeedbackRule($subtree, 'incorrect', null, $responseIdentifier);
} elseif (count($subtree->xpath($patternFeedbackOperator)) > 0 || count($subtree->xpath($patternFeedbackOperatorWithElse)) > 0) {
$operator = '';
$responseIdentifier = '';
$value = '';
foreach ($subtree->responseIf->children() as $child) {
$operator = $child->getName();
$map = null;
foreach ($child->children() as $granChild) {
$map = $granChild->getName();
$responseIdentifier = (string) $granChild['identifier'];
break;
}
$value = (string) $child->baseValue;
break;
}
$feedbackRule = $this->buildSimpleFeedbackRule($subtree, $operator, $value);
} elseif (
count($subtree->xpath($patternFeedbackMatchChoices)) > 0 || count($subtree->xpath($patternFeedbackMatchChoicesWithElse)) > 0 ||
count($subtree->xpath($patternFeedbackMatchChoicesEmpty)) > 0 || count($subtree->xpath($patternFeedbackMatchChoicesEmptyWithElse)) > 0
) {
$choices = [];
foreach ($subtree->responseIf->match->multiple->baseValue as $choice) {
$choices[] = (string)$choice;
}
$feedbackRule = $this->buildSimpleFeedbackRule($subtree, 'choices', $choices);
} elseif (count($subtree->xpath($patternFeedbackMatchChoice)) > 0 || count($subtree->xpath($patternFeedbackMatchChoiceWithElse)) > 0) {
$choices = [(string)$subtree->responseIf->match->baseValue];
$feedbackRule = $this->buildSimpleFeedbackRule($subtree, 'choices', $choices);
} else {
throw new UnexpectedResponseProcessing('Not template driven, unknown rule');
}
if (!is_null($feedbackRule)) {
$responseIdentifier = $feedbackRule->comparedOutcome()->getIdentifier();
if (!isset($simpleFeedbackRules[$responseIdentifier])) {
$simpleFeedbackRules[$responseIdentifier] = [];
}
$simpleFeedbackRules[$responseIdentifier][] = $feedbackRule;
}
}
$responseIdentifiers = [];
foreach ($interactions as $interaction) {
$interactionResponse = $interaction->getResponse();
$responseIdentifier = $interactionResponse->getIdentifier();
$responseIdentifiers[] = $responseIdentifier;
//create and set simple feedback rule here
if (isset($simpleFeedbackRules[$responseIdentifier])) {
foreach ($simpleFeedbackRules[$responseIdentifier] as $rule) {
$interactionResponse->addFeedbackRule($rule);
}
}
}
//all rules must have been previously identified as belonging to one interaction
if (count(array_diff(array_keys($rules), $responseIdentifiers)) > 0) {
throw new UnexpectedResponseProcessing('Not template driven, responseIdentifiers are ' . implode(',', $responseIdentifiers) . ' while rules are ' . implode(',', array_keys($rules)));
}
$templatesDrivenRP = new TemplatesDriven();
foreach ($interactions as $interaction) {
//if a rule has been found for an interaction, apply it. Default to the template NONE otherwise
$pattern = isset($rules[$interaction->getResponse()->getIdentifier()]) ? $rules[$interaction->getResponse()->getIdentifier()] : Template::NONE;
$templatesDrivenRP->setTemplate($interaction->getResponse(), $pattern);
}
$templatesDrivenRP->setRelatedItem($this->item);
$returnValue = $templatesDrivenRP;
return $returnValue;
}
private function buildSimpleFeedbackRule($subtree, $conditionName, $comparedValue = null, $responseId = '')
{
$responseIdentifier = empty($responseId) ? (string) $subtree->responseIf->match->variable['identifier'] : $responseId;
$feedbackOutcomeIdentifier = (string) $subtree->responseIf->setOutcomeValue['identifier'];
$feedbackIdentifier = (string) $subtree->responseIf->setOutcomeValue->baseValue;
try {
$response = $this->getResponse($responseIdentifier);
$outcome = $this->getOutcome($feedbackOutcomeIdentifier);
$feedbackThen = $this->getModalFeedback($feedbackIdentifier);
$feedbackElse = null;
if ($subtree->responseElse->getName()) {
$feedbackElseIdentifier = (string) $subtree->responseElse->setOutcomeValue->baseValue;
$feedbackElse = $this->getModalFeedback($feedbackElseIdentifier);
}
$feedbackRule = new SimpleFeedbackRule($outcome, $feedbackThen, $feedbackElse);
$feedbackRule->setCondition($response, $conditionName, $comparedValue);
} catch (ParsingException $e) {
throw new UnexpectedResponseProcessing('Feedback resources not found. Not template driven, unknown rule');
}
return $feedbackRule;
}
/**
* Short description of method buildObject
*
* @access private
* @author Joel Bout, <joel.bout@tudor.lu>
* @param DOMElement $data
* @return \oat\taoQtiItem\model\qti\QtiObject
*/
private function buildObject(DOMElement $data)
{
$attributes = $this->extractAttributes($data);
$returnValue = new QtiObject($attributes);
if ($data->hasChildNodes()) {
$nonEmptyChild = $this->getNonEmptyChildren($data);
if (count($nonEmptyChild) == 1 && reset($nonEmptyChild)->nodeName == 'object') {
$alt = $this->buildObject(reset($nonEmptyChild));
$returnValue->setAlt($alt);
} else {
//get the node xml content
$pattern = ["/^<{$data->nodeName}([^>]*)?>/i", "/<\/{$data->nodeName}([^>]*)?>$/i"];
$content = preg_replace($pattern, '', trim($this->saveXML($data)));
$returnValue->setAlt($content);
}
} else {
$alt = trim($data->nodeValue);
if (!empty($alt)) {
$returnValue->setAlt($alt);
}
}
return $returnValue;
}
private function buildImg(DOMElement $data)
{
$attributes = $this->extractAttributes($data);
$returnValue = new Img($attributes);
return $returnValue;
}
private function buildTooltip(DOMElement $data, DOMElement $context)
{
$tooltip = null;
$attributes = $this->extractAttributes($data);
// Look for tooltip content
$contentId = $attributes['aria-describedby'];
if (!empty($contentId)) {
$tooltipContentNodes = $this->queryXPath(".//*[@id='$contentId']", $context);
$tooltipContent = $tooltipContentNodes[0];
if (!is_null($tooltipContent)) {
$content = $this->getNodeContentAsHtml($this->data, $tooltipContent);
// Content has been found, we can build the tooltip
$tooltip = new Tooltip($attributes);
$tooltip->setContent($content);
// remove the tooltip content node so it does not pollute the markup
$tooltipContent->parentNode->removeChild($tooltipContent);
// Set the tooltip target
$this->parseContainerStatic($data, $tooltip->getBody());
}
}
return $tooltip;
}
private function getNodeContentAsHtml(DOMDocument $document, DOMElement $node)
{
$html = "";
$children = $node->childNodes;
foreach ($children as $childNode) {
$html .= $document->saveXML($childNode);
}
return $html;
}
private function buildTable(DOMElement $data)
{
$attributes = $this->extractAttributes($data);
$table = new Table($attributes);
$this->parseContainerStatic($data, $table->getBody());
return $table;
}
private function buildMath(DOMElement $data)
{
$ns = $this->getMathNamespace();
$annotationNodes = $this->queryXPath(".//*[name(.)='" . (empty($ns) ? '' : $ns . ':') . "annotation']", $data);
$annotations = [];
//need to extract the namespace, and clean it in the "bodydata"
foreach ($annotationNodes as $annotationNode) {
$attr = $this->extractAttributes($annotationNode);
$encoding = isset($attr['encoding']) ? strtolower(trim($attr['encoding'])) : '';
$str = $this->getBodyData($annotationNode);
if (!empty($encoding) && !empty($str)) {
$annotations[$encoding] = $str;
$this->deleteNode($annotationNode);
}
}
$math = new Math($this->extractAttributes($data));
$body = $this->getBodyData($data, true);
$math->setMathML($body);
$math->setAnnotations($annotations);
return $math;
}
private function buildXInclude(DOMElement $data)
{
return new XInclude($this->extractAttributes($data));
}
protected function getNonEmptyChildren(DOMElement $data)
{
$returnValue = [];
foreach ($data->childNodes as $childNode) {
if ($childNode->nodeName == '#text') {
if (trim($childNode->nodeValue) != '') {
$returnValue[] = $childNode;
}
} else {
$returnValue[] = $childNode;
}
}
return $returnValue;
}
private function buildStylesheet(DOMElement $data)
{
$returnValue = new Stylesheet([
'href' => (string) $data->getAttribute('href'),
'title' => $data->hasAttribute('title') ? (string) $data->getAttribute('title') : '',
'media' => $data->hasAttribute('media') ? (string) $data->getAttribute('media') : 'screen',
'type' => $data->hasAttribute('type') ? (string) $data->getAttribute('type') : 'text/css',
]);
return $returnValue;
}
private function buildRubricBlock(DOMElement $data)
{
$returnValue = new RubricBlock($this->extractAttributes($data));
$this->parseContainerStatic($data, $returnValue->getBody());
return $returnValue;
}
private function buildFeedback(DOMElement $data, $interactive = false)
{
$type = ucfirst($data->nodeName);
$feedbackClass = '\\oat\\taoQtiItem\\model\\qti\\feedback\\' . $type;
if (!class_exists($feedbackClass)) {
throw new ParsingException('The interaction class cannot be found: ' . $feedbackClass);
}
$attributes = $this->extractAttributes($data);
if ($data->nodeName == 'modalFeedback') {
$myFeedback = new $feedbackClass($attributes, $this->item);
$this->parseContainerStatic($data, $myFeedback->getBody());
} else {
throw new UnsupportedQtiElement($data);
}
return $myFeedback;
}
/**
* Return the list of registered php portable element subclasses
* @return array
*/
private function getPortableElementSubclasses($superClassName)
{
$subClasses = [];
foreach (PortableModelRegistry::getRegistry()->getModels() as $model) {
$portableElementClass = $model->getQtiElementClassName();
if (is_subclass_of($portableElementClass, $superClassName)) {
$subClasses[] = $portableElementClass;
}
}
return $subClasses;
}
/**
* Get the PCI class associated to a dom node based on its namespace
* Returns null if not a known PCI model
*
* @param DOMElement $data
* @return null
*/
private function getPortableElementClass(DOMElement $data, $superClassName, $portableElementNodeName)
{
$portableElementClasses = $this->getPortableElementSubclasses($superClassName);
//start searching from globally declared namespace
foreach ($this->item->getNamespaces() as $name => $uri) {
foreach ($portableElementClasses as $class) {
if (
$uri === $class::NS_URI
&& $this->queryXPathChildren([$portableElementNodeName], $data, $name)->length
) {
return $class;
}
}
}
//not found as a global namespace definition, try local namespace
if ($this->queryXPathChildren([$portableElementNodeName], $data)->length) {
$pciNode = $this->queryXPathChildren([$portableElementNodeName], $data)[0];
$xmlns = $pciNode->getAttribute('xmlns');
foreach ($portableElementClasses as $phpClass) {
if ($phpClass::NS_URI === $xmlns) {
return $phpClass;
}
}
}
//not a known portable element type
return null;
}
private function getPciClass(DOMElement $data)
{
return $this->getPortableElementClass($data, 'oat\\taoQtiItem\\model\\qti\\interaction\\CustomInteraction', 'portableCustomInteraction');
}
private function getPicClass(DOMElement $data)
{
return $this->getPortableElementClass($data, 'oat\\taoQtiItem\\model\\qti\\InfoControl', 'portableInfoControl');
}
/**
* Parse and build a custom interaction object
*
* @param DOMElement $data
* @return CustomInteraction
* @throws ParsingException
*/
private function buildCustomInteraction(DOMElement $data)
{
$interaction = null;
$pciClass = $this->getPciClass($data);
if (!empty($pciClass)) {
$ns = null;
foreach ($this->item->getNamespaces() as $name => $uri) {
if ($pciClass::NS_URI === $uri) {
$ns = new QtiNamespace($uri, $name);
}
}
if (is_null($ns)) {
$pciNodes = $this->queryXPathChildren(['portableCustomInteraction'], $data);
if ($pciNodes->length) {
$ns = new QtiNamespace($pciNodes->item(0)->getAttribute('xmlns'));
}
}
//use tao's implementation of portable custom interaction
$interaction = new $pciClass($this->extractAttributes($data), $this->item);
$interaction->feed($this, $data, $ns);
} else {
$ciClass = '';
$classes = $data->getAttribute('class');
$classeNames = preg_split('/\s+/', $classes);
foreach ($classeNames as $classeName) {
$ciClass = CustomInteractionRegistry::getCustomInteractionByName($classeName);
if ($ciClass) {
$interaction = new $ciClass($this->extractAttributes($data), $this->item);
$interaction->feed($this, $data);
break;
}
}
if (!$ciClass) {
throw new ParsingException('unknown custom interaction to be build');
}
}
return $interaction;
}
/**
* Parse and build a info control
*
* @param DOMElement $data
* @return InfoControl
* @throws ParsingException
*/
private function buildInfoControl(DOMElement $data)
{
$infoControl = null;
$picClass = $this->getPicClass($data);
if (!empty($picClass)) {
$ns = null;
foreach ($this->item->getNamespaces() as $name => $uri) {
if ($picClass::NS_URI === $uri) {
$ns = new QtiNamespace($uri, $name);
}
}
if (is_null($ns)) {
$pciNodes = $this->queryXPathChildren(['portableInfoControl'], $data);
if ($pciNodes->length) {
$ns = new QtiNamespace($pciNodes->item(0)->getAttribute('xmlns'));
}
}
//use tao's implementation of portable custom interaction
$infoControl = new PortableInfoControl($this->extractAttributes($data), $this->item);
$infoControl->feed($this, $data, $ns);
} else {
$ciClass = '';
$classes = $data->getAttribute('class');
$classeNames = preg_split('/\s+/', $classes);
foreach ($classeNames as $classeName) {
$ciClass = InfoControlRegistry::getInfoControlByName($classeName);
if ($ciClass) {
$infoControl = new $ciClass($this->extractAttributes($data), $this->item);
$infoControl->feed($this, $data);
break;
}
}
if (!$ciClass) {
throw new UnsupportedQtiElement($data);
}
}
return $infoControl;
}
}