Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
16.03% covered (danger)
16.03%
151 / 942
14.52% covered (danger)
14.52%
9 / 62
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParserFactory
16.03% covered (danger)
16.03%
151 / 942
14.52% covered (danger)
14.52%
9 / 62
59438.24
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setItem
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 load
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 saveXML
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getBodyData
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 replaceNode
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 deleteNode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 queryXPath
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 queryXPathChildren
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 loadContainerStatic
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseContainerStatic
44.74% covered (danger)
44.74%
34 / 76
0.00% covered (danger)
0.00%
0 / 1
140.09
 getAncestors
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 parseContainerInteractive
55.56% covered (warning)
55.56%
15 / 27
0.00% covered (danger)
0.00%
0 / 1
16.11
 setContainerElements
50.00% covered (danger)
50.00%
3 / 6
0.00% covered (danger)
0.00%
0 / 1
6.00
 setContainerAttributes
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 parseContainerItemBody
40.00% covered (danger)
40.00%
10 / 25
0.00% covered (danger)
0.00%
0 / 1
21.82
 parseContainerChoice
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 parseContainerGap
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseContainerHottext
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 extractAttributes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 findNamespace
62.50% covered (warning)
62.50%
10 / 16
0.00% covered (danger)
0.00%
0 / 1
9.58
 recursivelyFindNamespace
13.33% covered (danger)
13.33%
2 / 15
0.00% covered (danger)
0.00%
0 / 1
38.90
 getMathNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getXIncludeNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHTML5Namespace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 buildItem
52.83% covered (warning)
52.83%
28 / 53
0.00% covered (danger)
0.00%
0 / 1
47.33
 loadNamespaces
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 loadSchemaLocations
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
3.01
 buildApipAccessibility
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
3.19
 buildInteraction
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
600
 buildChoice
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 buildResponseDeclaration
0.00% covered (danger)
0.00%
0 / 58
0.00% covered (danger)
0.00%
0 / 1
342
 buildOutcomeDeclaration
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 buildTemplateResponseProcessing
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
90
 buildResponseProcessing
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 buildCompositeResponseProcessing
0.00% covered (danger)
0.00%
0 / 89
0.00% covered (danger)
0.00%
0 / 1
380
 buildCustomResponseProcessing
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 buildExpression
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getModalFeedback
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getOutcome
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getResponse
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 buildTemplatedrivenResponse
0.00% covered (danger)
0.00%
0 / 142
0.00% covered (danger)
0.00%
0 / 1
812
 buildSimpleFeedbackRule
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 buildObject
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 buildImg
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 buildFigCaption
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 buildTooltip
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 getNodeContentAsHtml
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 buildTable
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 buildFigure
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 buildMath
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 buildXInclude
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNonEmptyChildren
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 buildStylesheet
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 buildRubricBlock
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 buildFeedback
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getPortableElementSubclasses
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getPortableElementClass
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
 getPciClass
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getPicClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 buildCustomInteraction
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
 buildInfoControl
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2013-2022 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 */
20
21namespace oat\taoQtiItem\model\qti;
22
23use oat\taoQtiItem\model\qti\Element;
24use oat\taoQtiItem\model\qti\container\Container;
25use oat\taoQtiItem\model\qti\exception\UnsupportedQtiElement;
26use oat\taoQtiItem\model\qti\exception\ParsingException;
27use oat\taoQtiItem\model\qti\container\ContainerInteractive;
28use oat\taoQtiItem\model\qti\container\ContainerItemBody;
29use oat\taoQtiItem\model\qti\container\ContainerGap;
30use oat\taoQtiItem\model\qti\container\ContainerHottext;
31use oat\taoQtiItem\model\qti\Item;
32use oat\taoQtiItem\model\qti\response\Custom;
33use oat\taoQtiItem\model\qti\interaction\BlockInteraction;
34use oat\taoQtiItem\model\qti\interaction\ObjectInteraction;
35use oat\taoQtiItem\model\qti\interaction\CustomInteraction;
36use oat\taoQtiItem\model\qti\interaction\PortableCustomInteraction;
37use oat\taoQtiItem\model\CustomInteractionRegistry;
38use oat\taoQtiItem\model\qti\InfoControl;
39use oat\taoQtiItem\model\qti\PortableInfoControl;
40use oat\taoQtiItem\model\InfoControlRegistry;
41use oat\taoQtiItem\model\qti\choice\ContainerChoice;
42use oat\taoQtiItem\model\qti\choice\TextVariableChoice;
43use oat\taoQtiItem\model\qti\choice\GapImg;
44use oat\taoQtiItem\model\qti\response\interactionResponseProcessing\None;
45use oat\taoQtiItem\model\qti\ResponseDeclaration;
46use oat\taoQtiItem\model\qti\OutcomeDeclaration;
47use oat\taoQtiItem\model\qti\response\Template;
48use oat\taoQtiItem\model\qti\exception\UnexpectedResponseProcessing;
49use oat\taoQtiItem\model\qti\response\TemplatesDriven;
50use oat\taoQtiItem\model\qti\response\TakeoverFailedException;
51use oat\taoQtiItem\model\qti\response\Summation;
52use oat\taoQtiItem\model\qti\expression\ExpressionParserFactory;
53use oat\taoQtiItem\model\qti\response\SimpleFeedbackRule;
54use oat\taoQtiItem\model\qti\QtiObject;
55use oat\taoQtiItem\model\qti\Img;
56use oat\taoQtiItem\model\qti\Math;
57use oat\taoQtiItem\model\qti\XInclude;
58use oat\taoQtiItem\model\qti\Stylesheet;
59use oat\taoQtiItem\model\qti\RubricBlock;
60use oat\taoQtiItem\model\qti\container\ContainerFeedbackInteractive;
61use oat\taoQtiItem\model\qti\container\ContainerStatic;
62use DOMDocument;
63use DOMXPath;
64use DOMElement;
65use common_Logger;
66use SimpleXMLElement;
67use oat\oatbox\service\ServiceManager;
68use oat\taoQtiItem\model\portableElement\model\PortableModelRegistry;
69use oat\oatbox\log\LoggerAwareTrait;
70
71/**
72 * The ParserFactory provides some methods to build the QTI_Data objects from an
73 * element.
74 * SimpleXML is used as source to build the model.
75 *
76 * @access public
77 * @author Joel Bout, <joel.bout@tudor.lu>
78 * @package taoQTI
79 */
80class ParserFactory
81{
82    use LoggerAwareTrait;
83
84    protected $data = null;
85    /** @var \oat\taoQtiItem\model\qti\Item */
86    protected $item = null;
87    protected $attributeMap = ['lang' => 'xml:lang'];
88
89    public function __construct(DOMDocument $data)
90    {
91        $this->data = $data;
92        $this->xpath = new DOMXPath($data);
93    }
94
95    /**
96     * @param \oat\taoQtiItem\model\qti\Item $item
97     */
98    public function setItem(Item $item)
99    {
100        $this->item = $item;
101    }
102
103    public function load()
104    {
105
106        $item = null;
107
108        if (!is_null($this->data)) {
109            $item = $this->buildItem($this->data->documentElement);
110        }
111
112        return $item;
113    }
114
115    protected function saveXML(DOMElement $data)
116    {
117        return $data->ownerDocument->saveXML($data);
118    }
119
120
121    /**
122     * Get the body data (markups) of an element.
123     * @param \DOMElement $data the element
124     * @param boolean $removeNamespace if XML namespaces should be removed
125     * @param boolean $keepEmptyTags if true, the empty tags are kept expanded (useful when tags are HTML)
126     * @return string the body data (XML markup)
127     */
128    public function getBodyData(DOMElement $data, $removeNamespace = false, $keepEmptyTags = false)
129    {
130
131        //prepare the data string
132        $bodyData = '';
133        $saveOptions = $keepEmptyTags ? LIBXML_NOEMPTYTAG : 0;
134
135        $children  = $data->childNodes;
136
137        foreach ($children as $child) {
138            $bodyData .= $data->ownerDocument->saveXML($child, $saveOptions);
139        }
140
141        if ($removeNamespace) {
142            $bodyData = preg_replace('/<(\/)?(\w*):/i', '<$1', $bodyData);
143        }
144
145        return $bodyData;
146    }
147
148    protected function replaceNode(DOMElement $node, Element $element)
149    {
150        $placeholder = $this->data->createTextNode($element->getPlaceholder());
151        $node->parentNode->replaceChild($placeholder, $node);
152    }
153
154    protected function deleteNode(DOMElement $node)
155    {
156        $node->parentNode->removeChild($node);
157    }
158
159    public function queryXPath($query, DOMElement $contextNode = null)
160    {
161        if (is_null($contextNode)) {
162            return $this->xpath->query($query);
163        } else {
164            return $this->xpath->query($query, $contextNode);
165        }
166    }
167
168    public function queryXPathChildren($paths = [], DOMElement $contextNode = null, $ns = '')
169    {
170        $query = '.';
171        $ns = empty($ns) ? '' : $ns . ':';
172        foreach ($paths as $path) {
173            $query .= "/*[name(.)='" . $ns . $path . "']";
174        }
175
176        return $this->queryXPath($query, $contextNode);
177    }
178
179    public function loadContainerStatic(DOMElement $data, Container $container)
180    {
181        $this->parseContainerStatic($data, $container);
182    }
183
184    protected function parseContainerStatic(DOMElement $data, Container $container)
185    {
186        //initialize elements array to collect all QTI elements
187        $bodyElements = [];
188
189        //parse for feedback elements
190        //warning: parse feedback elements before any other because feedback may contain them!
191        $feedbackNodes = $this->queryXPath(
192            ".//*[not(ancestor::feedbackBlock) and not(ancestor::feedbackInline) and contains(name(.), 'feedback')]",
193            $data
194        );
195
196        foreach ($feedbackNodes as $feedbackNode) {
197            $feedback = $this->buildFeedback($feedbackNode);
198            if (!is_null($feedback)) {
199                $bodyElements[$feedback->getSerial()] = $feedback;
200                $this->replaceNode($feedbackNode, $feedback);
201            }
202        }
203
204        // parse for QTI elements within item body
205
206        // parse the remaining tables, those that does not contain any interaction.
207        //warning: parse table elements before any other because table may contain them!
208        $tableNodes = $this->queryXPath(".//*[not(ancestor::*[name()='table']) and name()='table']", $data);
209        foreach ($tableNodes as $tableNode) {
210            $table = $this->buildTable($tableNode);
211            if (!is_null($table)) {
212                $bodyElements[$table->getSerial()] = $table;
213                $this->replaceNode($tableNode, $table);
214            }
215        }
216
217        $figureNodes = $this->queryXPath(".//*[name(.)='" . $this->getHTML5Namespace() . "figure']", $data);
218        foreach ($figureNodes as $figureNode) {
219            $figure = $this->buildFigure($figureNode);
220            if (!is_null($figure)) {
221                $bodyElements[$figure->getSerial()] = $figure;
222                $this->replaceNode($figureNode, $figure);
223            }
224        }
225
226        $tooltipNodes = $this->queryXPath(".//*[@data-role='tooltip-target']", $data);
227        foreach ($tooltipNodes as $tooltipNode) {
228            $tooltip = $this->buildTooltip($tooltipNode, $data);
229            if (!is_null($tooltip)) {
230                $bodyElements[$tooltip->getSerial()] = $tooltip;
231
232                $this->replaceNode($tooltipNode, $tooltip);
233            }
234        }
235
236        $objectNodes = $this->queryXPath(".//*[name(.)='object']", $data);
237        foreach ($objectNodes as $objectNode) {
238            if (!in_array('object', $this->getAncestors($objectNode))) {
239                $object = $this->buildObject($objectNode);
240                if (!is_null($object)) {
241                    $bodyElements[$object->getSerial()] = $object;
242
243                    $this->replaceNode($objectNode, $object);
244                }
245            }
246        }
247
248        $imgNodes = $this->queryXPath(".//*[name(.)='img']", $data);
249        foreach ($imgNodes as $imgNode) {
250            $img = $this->buildImg($imgNode);
251            if (!is_null($img)) {
252                $bodyElements[$img->getSerial()] = $img;
253
254                $this->replaceNode($imgNode, $img);
255            }
256        }
257
258        $figCaptionNodes = $this->queryXPath(".//*[name(.)='" . $this->getHTML5Namespace() . "figcaption']", $data);
259        foreach ($figCaptionNodes as $figCaptionNode) {
260            $figCaption = $this->buildFigCaption($figCaptionNode);
261            if (!is_null($figCaption)) {
262                $bodyElements[$figCaption->getSerial()] = $figCaption;
263
264                $this->replaceNode($figCaptionNode, $figCaption);
265            }
266            //should be no more then one
267            break;
268        }
269
270        $ns = $this->getMathNamespace();
271        $ns = empty($ns) ? '' : $ns . ':';
272        $mathNodes = $this->queryXPath(".//*[name(.)='" . $ns . "math']", $data);
273        foreach ($mathNodes as $mathNode) {
274            $math = $this->buildMath($mathNode);
275            if (!is_null($math)) {
276                $bodyElements[$math->getSerial()] = $math;
277                $this->replaceNode($mathNode, $math);
278            }
279        }
280
281        $ns = $this->getXIncludeNamespace();
282        $ns = empty($ns) ? '' : $ns . ':';
283        $xincludeNodes = $this->queryXPath(".//*[name(.)='" . $ns . "include']", $data);
284        foreach ($xincludeNodes as $xincludeNode) {
285            $include = $this->buildXInclude($xincludeNode);
286            if (!is_null($include)) {
287                $bodyElements[$include->getSerial()] = $include;
288                $this->replaceNode($xincludeNode, $include);
289            }
290        }
291
292        $printedVariableNodes = $this->queryXPath(".//*[name(.)='printedVariable']", $data);
293        foreach ($printedVariableNodes as $printedVariableNode) {
294            throw new UnsupportedQtiElement($printedVariableNode);
295        }
296
297        $templateNodes = $this->queryXPath(".//*[name(.)='templateBlock'] | *[name(.)='templateInline']", $data);
298        foreach ($templateNodes as $templateNode) {
299            throw new UnsupportedQtiElement($templateNode);
300        }
301
302        //finally, add all body elements to the body
303        $bodyData = $this->getBodyData($data);
304        //there use to be $bodyData = ItemAuthoring::cleanHTML($bodyData); there
305
306        if (empty($bodyElements)) {
307            $container->edit($bodyData);
308        } elseif (!$container->setElements($bodyElements, $bodyData)) {
309            throw new ParsingException('Cannot set elements to the static container');
310        }
311
312        return $data;
313    }
314
315    protected function getAncestors(DOMElement $data, $topNode = 'itemBody')
316    {
317        $ancestors = [];
318        $parentNodeName = '';
319        $currentNode = $data;
320        $i = 0;
321        while (!is_null($currentNode->parentNode) && $parentNodeName != $topNode) {
322            if ($i > 100) {
323                throw new ParsingException('maximum recursion of 100 reached');
324            }
325
326            $parentNodeName = $currentNode->parentNode->nodeName;
327            $ancestors[] = $parentNodeName;
328            $currentNode = $currentNode->parentNode;
329            $i++;
330        }
331        return $ancestors;
332    }
333
334    protected function parseContainerInteractive(DOMElement $data, ContainerInteractive $container)
335    {
336
337        $bodyElements = [];
338
339        //parse the xml to find the interaction nodes
340        $interactionNodes = $this->queryXPath(
341            ".//*[not(ancestor::feedbackBlock) and not(ancestor::feedbackInline) and contains(name(.), 'Interaction')]",
342            $data
343        );
344
345        foreach ($interactionNodes as $k => $interactionNode) {
346            if (strpos($interactionNode->nodeName, 'portableCustomInteraction') === false) {
347                //build an interaction instance
348                $interaction = $this->buildInteraction($interactionNode);
349                if (!is_null($interaction)) {
350                    $bodyElements[$interaction->getSerial()] = $interaction;
351                    $this->replaceNode($interactionNode, $interaction);
352                }
353            }
354        }
355
356        //parse for feedback elements interactive!
357        $feedbackNodes = $this->queryXPath(
358            ".//*[not(ancestor::feedbackBlock) and not(ancestor::feedbackInline) and contains(name(.), 'feedback')]",
359            $data
360        );
361
362        foreach ($feedbackNodes as $feedbackNode) {
363            $feedback = $this->buildFeedback($feedbackNode, true);
364            if (!is_null($feedback)) {
365                $bodyElements[$feedback->getSerial()] = $feedback;
366                $this->replaceNode($feedbackNode, $feedback);
367            }
368        }
369
370        $bodyData = $this->getBodyData($data);
371        foreach ($bodyElements as $bodyElement) {
372            if (strpos($bodyData, $bodyElement->getPlaceholder()) === false) {
373                unset($bodyElements[$bodyElement->getSerial()]);
374            }
375        }
376        if (!$container->setElements($bodyElements, $bodyData)) {
377            throw new ParsingException('Cannot set elements to the interactive container');
378        }
379
380        return $this->parseContainerStatic($data, $container);
381    }
382
383    protected function setContainerElements(Container $container, DOMElement $data, $bodyElements = [])
384    {
385        $bodyData = $this->getBodyData($data);
386        foreach ($bodyElements as $bodyElement) {
387            if (strpos($bodyData, $bodyElement->getPlaceholder()) === false) {
388                unset($bodyElements[$bodyElement->getSerial()]);
389            }
390        }
391        if (!$container->setElements($bodyElements, $bodyData)) {
392            throw new ParsingException('Cannot set elements to the interactive container');
393        }
394    }
395
396    private function setContainerAttributes(Container $container, DOMElement $data): void
397    {
398        $languageAttribute = $data->getAttribute('dir');
399        if (!empty($languageAttribute)) {
400            $container->setAttribute('dir', $languageAttribute);
401        }
402    }
403
404    protected function parseContainerItemBody(DOMElement $data, ContainerItemBody $container)
405    {
406
407        $bodyElements = [];
408
409        //parse for rubricBlocks: rubricBlock only allowed in item body !
410        $rubricNodes = $this->queryXPath(".//*[name(.)='rubricBlock']", $data);
411        foreach ($rubricNodes as $rubricNode) {
412            $rubricBlock = $this->buildRubricBlock($rubricNode);
413            if (!is_null($rubricBlock)) {
414                $bodyElements[$rubricBlock->getSerial()] = $rubricBlock;
415                $this->replaceNode($rubricNode, $rubricBlock);
416            }
417        }
418
419        //parse for infoControls: infoControl only allowed in item body !
420        $infoControlNodes = $this->queryXPath(".//*[name(.)='infoControl']", $data);
421        foreach ($infoControlNodes as $infoControlNode) {
422            $infoControl = $this->buildInfoControl($infoControlNode);
423            if (!is_null($infoControl)) {
424                $bodyElements[$infoControl->getSerial()] = $infoControl;
425                $this->replaceNode($infoControlNode, $infoControl);
426            }
427        }
428
429        // parse for tables, but only the ones containing interactions
430        $tableNodes = $this->queryXPath(".//*[name(.)='table']", $data);
431        foreach ($tableNodes as $tableNode) {
432            $interactionsNodes = $this->queryXPath(".//*[contains(name(.), 'Interaction')]", $tableNode);
433            if ($interactionsNodes->length > 0) {
434                $table = $this->buildTable($tableNode);
435                if (!is_null($table)) {
436                    $bodyElements[$table->getSerial()] = $table;
437                    $this->replaceNode($tableNode, $table);
438                    $this->parseContainerInteractive($tableNode, $table->getBody());
439                }
440            }
441        }
442
443        $this->setContainerAttributes($container, $data);
444        $this->setContainerElements($container, $data, $bodyElements);
445
446        return $this->parseContainerInteractive($data, $container);
447    }
448
449    private function parseContainerChoice(DOMElement $data, Container $container, $tag)
450    {
451
452        $choices = [];
453        $gapNodes = $this->queryXPath(".//*[name(.)='" . $tag . "']", $data);
454        foreach ($gapNodes as $gapNode) {
455            $gap = $this->buildChoice($gapNode);
456            if (!is_null($gap)) {
457                $choices[$gap->getSerial()] = $gap;
458                $this->replaceNode($gapNode, $gap);
459            }
460        }
461        $bodyData = $this->getBodyData($data);
462        $container->setElements($choices, $bodyData);
463
464        $data = $this->parseContainerStatic($data, $container);
465
466        return $data;
467    }
468
469    protected function parseContainerGap(DOMElement $data, ContainerGap $container)
470    {
471        return $this->parseContainerChoice($data, $container, 'gap');
472    }
473
474    protected function parseContainerHottext(DOMElement $data, ContainerHottext $container)
475    {
476        return $this->parseContainerChoice($data, $container, 'hottext');
477    }
478
479    protected function extractAttributes(DOMElement $data)
480    {
481        $options = [];
482        foreach ($data->attributes as $attr) {
483            if ($attr->nodeName === 'xsi:schemaLocation') {
484                continue;
485            }
486
487            $options[$this->attributeMap[$attr->nodeName] ?? $attr->nodeName] = (string) $attr->nodeValue;
488        }
489        return $options;
490    }
491
492    public function findNamespace($nsFragment)
493    {
494        $returnValue = '';
495
496        if (is_null($this->item)) {
497            foreach ($this->queryXPath('namespace::*') as $node) {
498                $name = preg_replace('/xmlns(:)?/', '', $node->nodeName);
499                $uri = $node->nodeValue;
500                if (strpos($uri, $nsFragment) > 0) {
501                    $returnValue = $name;
502                    break;
503                }
504            }
505        } else {
506            $namespaces = $this->item->getNamespaces();
507
508            foreach ($namespaces as $name => $uri) {
509                if (strpos($uri, $nsFragment) > 0) {
510                    $returnValue = $name;
511                    break;
512                }
513            }
514            if ($returnValue === '') {
515                $returnValue = $this->recursivelyFindNamespace($this->data, $nsFragment);
516            }
517        }
518        return $returnValue;
519    }
520
521    private function recursivelyFindNamespace($element, $nsFragment)
522    {
523        if (strpos($this->data->saveXML(), $nsFragment) === false) {
524            return '';
525        }
526
527        $returnValue = '';
528
529        foreach ($element->childNodes as $child) {
530            if ($child->nodeType === XML_ELEMENT_NODE) {
531                foreach ($this->queryXPath('namespace::*', $child) as $node) {
532                    $name = preg_replace('/xmlns(:)?/', '', $node->nodeName);
533                    $uri = $node->nodeValue;
534                    if (strpos($uri, $nsFragment) > 0) {
535                        $returnValue = $name;
536                        break;
537                    }
538                }
539                $value = $this->recursivelyFindNamespace($child, $nsFragment);
540                if ($value !== '') {
541                    $returnValue = $value;
542                }
543            }
544        }
545
546        return $returnValue;
547    }
548
549    protected function getMathNamespace()
550    {
551        return $this->findNamespace('MathML');
552    }
553
554    protected function getXIncludeNamespace()
555    {
556        return $this->findNamespace('XInclude');
557    }
558
559    protected function getHTML5Namespace(): string
560    {
561        // qh5
562        $ns = $this->findNamespace('html5');
563
564        return empty($ns) ? '' : $ns . ':';
565    }
566
567    /**
568     * Build a QTI_Item from a DOMElement, the root tag of which is root assessmentItem
569     *
570     * @param DOMElement $data
571     * @return \oat\taoQtiItem\model\qti\Item
572     * @throws InvalidArgumentException
573     * @throws ParsingException
574     * @throws UnsupportedQtiElement
575     */
576    protected function buildItem(DOMElement $data)
577    {
578        //check on the root tag.
579        $itemId = (string) $data->getAttribute('identifier');
580
581        $this->logDebug('Started parsing of QTI item' . (isset($itemId) ? ' ' . $itemId : ''), ['TAOITEMS']);
582
583        //create the item instance
584        $this->item = new Item($this->extractAttributes($data));
585
586        //load xml ns and schema locations
587        $this->loadNamespaces();
588        $this->loadSchemaLocations($data);
589
590        //load stylesheets
591        $styleSheetNodes = $this->queryXPath("*[name(.) = 'stylesheet']", $data);
592        foreach ($styleSheetNodes as $styleSheetNode) {
593            $styleSheet = $this->buildStylesheet($styleSheetNode);
594            $this->item->addStylesheet($styleSheet);
595        }
596
597        //extract the responses
598        $responseNodes = $this->queryXPath("*[name(.) = 'responseDeclaration']", $data);
599        foreach ($responseNodes as $responseNode) {
600            $response = $this->buildResponseDeclaration($responseNode);
601            if (!is_null($response)) {
602                $this->item->addResponse($response);
603            }
604        }
605
606        //extract outcome variables
607        $outcomes = [];
608        $outComeNodes = $this->queryXPath("*[name(.) = 'outcomeDeclaration']", $data);
609        foreach ($outComeNodes as $outComeNode) {
610            $outcome = $this->buildOutcomeDeclaration($outComeNode);
611            if (!is_null($outcome)) {
612                $outcomes[] = $outcome;
613            }
614        }
615        if (count($outcomes) > 0) {
616            $this->item->setOutcomes($outcomes);
617        }
618
619        //extract modal feedbacks
620        $feedbackNodes = $this->queryXPath("*[name(.) = 'modalFeedback']", $data);
621        foreach ($feedbackNodes as $feedbackNode) {
622            $modalFeedback = $this->buildFeedback($feedbackNode);
623            if (!is_null($modalFeedback)) {
624                $this->item->addModalFeedback($modalFeedback);
625            }
626        }
627
628        //extract the item structure to separate the structural/style content to the item content
629        $itemBodies = $this->queryXPath("*[name(.) = 'itemBody']", $data); // array with 1 or zero bodies
630        if ($itemBodies === false) {
631            $errors = libxml_get_errors();
632            if (count($errors) > 0) {
633                $error = array_shift($errors);
634                $errormsg = $error->message;
635            } else {
636                $errormsg = "without errormessage";
637            }
638            throw new ParsingException(
639                'XML error(' . $errormsg . ') on itemBody read' . (isset($itemId) ? ' for item ' . $itemId : '')
640            );
641        } elseif ($itemBodies->length) {
642            $this->parseContainerItemBody($itemBodies->item(0), $this->item->getBody());
643            $this->item->addClass($itemBodies->item(0)->getAttribute('class'));
644        }
645
646
647        // warning: extract the response processing at the latest to make
648        // oat\taoQtiItem\model\qti\response\TemplatesDriven::takeOverFrom() work
649        $rpNodes = $this->queryXPath("*[name(.) = 'responseProcessing']", $data);
650        if ($rpNodes->length === 0) {
651            //no response processing node found: the template for an empty response processing is simply "NONE"
652            $rProcessing = new TemplatesDriven();
653            $rProcessing->setRelatedItem($this->item);
654            foreach ($this->item->getInteractions() as $interaction) {
655                $rProcessing->setTemplate($interaction->getResponse(), Template::NONE);
656            }
657            $this->item->setResponseProcessing($rProcessing);
658        } else {
659            //if there is a response processing node, try parsing it
660            $rpNode = $rpNodes->item(0);
661            $rProcessing = $this->buildResponseProcessing($rpNode, $this->item);
662            if (!is_null($rProcessing)) {
663                $this->item->setResponseProcessing($rProcessing);
664            }
665        }
666
667        $this->buildApipAccessibility($data);
668
669        return $this->item;
670    }
671
672    /**
673     * Load xml namespaces into the item model
674     */
675    protected function loadNamespaces()
676    {
677        $namespaces = [];
678        foreach ($this->queryXPath('namespace::*') as $node) {
679            $name = preg_replace('/xmlns(:)?/', '', $node->nodeName);
680            if ($name !== 'xml') {//always removed the implicit xml namespace
681                $namespaces[$name] = $node->nodeValue;
682            }
683        }
684        ksort($namespaces);
685        foreach ($namespaces as $name => $uri) {
686            $this->item->addNamespace($name, $uri);
687        }
688    }
689
690    /**
691     * Load xml schema locations into the item model
692     *
693     * @param DOMElement $itemData
694     * @throws ParsingException
695     */
696    protected function loadSchemaLocations(DOMElement $itemData)
697    {
698        $schemaLoc = preg_replace(
699            '/\s+/',
700            ' ',
701            trim($itemData->getAttributeNS($itemData->lookupNamespaceURI('xsi'), 'schemaLocation'))
702        );
703        $schemaLocToken = explode(' ', $schemaLoc);
704        $schemaCount = count($schemaLocToken);
705        if ($schemaCount % 2) {
706            throw new ParsingException('invalid schema location');
707        }
708        for ($i = 0; $i < $schemaCount; $i = $i + 2) {
709            $this->item->addSchemaLocation($schemaLocToken[$i], $schemaLocToken[$i + 1]);
710        }
711    }
712
713    protected function buildApipAccessibility(DOMElement $data)
714    {
715        $ApipNodes = $this->queryXPath("*[name(.) = 'apipAccessibility']|*[name(.) = 'apip:apipAccessibility']", $data);
716        if ($ApipNodes->length > 0) {
717            common_Logger::i('is APIP item', ['QTI', 'TAOITEMS']);
718            $apipNode = $ApipNodes->item(0);
719            $apipXml = $apipNode->ownerDocument->saveXML($apipNode);
720            $this->item->setApipAccessibility($apipXml);
721        }
722    }
723
724    /**
725     * Build a QTI_Interaction from a DOMElement (the root tag of this is an 'interaction' node)
726     *
727     * @access public
728     * @author Joel Bout, <joel.bout@tudor.lu>
729     * @param DOMElement $data
730     * @return \oat\taoQtiItem\model\qti\interaction\Interaction
731     * @throws ParsingException
732     * @throws UnsupportedQtiElement
733     * @throws interaction\InvalidArgumentException
734     * @see http://www.imsglobal.org/question/qti_v2p0/imsqti_infov2p0.html#element10247
735     */
736    protected function buildInteraction(DOMElement $data)
737    {
738
739        $returnValue = null;
740
741        if ($data->nodeName === 'customInteraction') {
742            $returnValue = $this->buildCustomInteraction($data);
743        } else {
744            //build one of the standard interaction
745
746            try {
747                $type = ucfirst($data->nodeName);
748
749                $interactionClass = '\\oat\\taoQtiItem\\model\\qti\\interaction\\' . $type;
750                if (!class_exists($interactionClass)) {
751                    throw new ParsingException('The interaction class cannot be found: ' . $interactionClass);
752                }
753
754                $myInteraction = new $interactionClass($this->extractAttributes($data), $this->item);
755
756                if ($myInteraction instanceof BlockInteraction) {
757                    //extract prompt:
758                    $promptNodes = $this->queryXPath("*[name(.) = 'prompt']", $data); //prompt
759
760                    foreach ($promptNodes as $promptNode) {
761                        //only block interactions have prompt
762                        $this->parseContainerStatic($promptNode, $myInteraction->getPrompt());
763                        $this->deleteNode($promptNode);
764                    }
765                }
766
767                //build the interaction's choices regarding it's type
768                switch (strtolower($type)) {
769                    case 'matchinteraction':
770                        //extract simpleMatchSet choices
771                        $matchSetNodes = $this->queryXPath("*[name(.) = 'simpleMatchSet']", $data); //simpleMatchSet
772                        $matchSetNumber = 0;
773                        foreach ($matchSetNodes as $matchSetNode) {
774                            //simpleAssociableChoice
775                            $choiceNodes = $this->queryXPath(
776                                "*[name(.) = 'simpleAssociableChoice']",
777                                $matchSetNode
778                            );
779
780                            foreach ($choiceNodes as $choiceNode) {
781                                $choice = $this->buildChoice($choiceNode);
782                                if (!is_null($choice)) {
783                                    $myInteraction->addChoice($choice, $matchSetNumber);
784                                }
785                            }
786                            if (++$matchSetNumber === 2) {
787                                //matchSet is limited to 2 maximum
788                                break;
789                            }
790                        }
791                        break;
792
793                    case 'gapmatchinteraction':
794                        //create choices with the gapText nodes
795                        $choiceNodes = $this->queryXPath("*[name(.)='gapText']", $data); //or gapImg!!
796                        $choices = [];
797                        foreach ($choiceNodes as $choiceNode) {
798                            $choice = $this->buildChoice($choiceNode);
799                            if (!is_null($choice)) {
800                                $myInteraction->addChoice($choice);
801                                $this->deleteNode($choiceNode);
802                            }
803
804                            //remove node so it does not pollute subsequent parsing data
805                            unset($choiceNode);
806                        }
807
808                        $this->parseContainerGap($data, $myInteraction->getBody());
809                        break;
810
811                    case 'hottextinteraction':
812                        $this->parseContainerHottext($data, $myInteraction->getBody());
813                        break;
814
815                    case 'graphicgapmatchinteraction':
816                        //create choices with the gapImg nodes
817                        $choiceNodes = $this->queryXPath("*[name(.)='gapImg']", $data);
818                        $choices = [];
819                        foreach ($choiceNodes as $choiceNode) {
820                            $choice = $this->buildChoice($choiceNode);
821                            if (!is_null($choice)) {
822                                $myInteraction->addGapImg($choice);
823                            }
824                        }
825                        // no break
826                    default:
827                        //parse, extract and build the choice nodes contained in the interaction
828                        $exp = "*[contains(name(.),'Choice')] | *[name(.)='associableHotspot']";
829                        $choiceNodes = $this->queryXPath($exp, $data);
830                        foreach ($choiceNodes as $choiceNode) {
831                            $choice = $this->buildChoice($choiceNode);
832                            if (!is_null($choice)) {
833                                $myInteraction->addChoice($choice);
834                            }
835                            unset($choiceNode);
836                        }
837                        break;
838                }
839
840                if ($myInteraction instanceof ObjectInteraction) {
841                    $objectNodes = $this->queryXPath("*[name(.)='object']", $data); //object
842                    foreach ($objectNodes as $objectNode) {
843                        $object = $this->buildObject($objectNode);
844                        if (!is_null($object)) {
845                            $myInteraction->setObject($object);
846                        }
847                    }
848                }
849
850                $returnValue = $myInteraction;
851            } catch (InvalidArgumentException $iae) {
852                throw new ParsingException($iae);
853            }
854        }
855
856        return $returnValue;
857    }
858
859    /**
860     * Build a QTI_Choice from a DOMElement (the root tag of this element
861     * an 'choice' node)
862     *
863     * @access public
864     * @author Joel Bout, <joel.bout@tudor.lu>
865     * @param  DOMElement $data
866     * @return \oat\taoQtiItem\model\qti\choice\Choice
867     * @throws ParsingException
868     * @throws UnsupportedQtiElement
869     * @throws choice\InvalidArgumentException
870     * @see http://www.imsglobal.org/question/qti_v2p0/imsqti_infov2p0.html#element10254
871     */
872    protected function buildChoice(DOMElement $data)
873    {
874
875        $className = '\\oat\\taoQtiItem\\model\\qti\\choice\\' . ucfirst($data->nodeName);
876        if (!class_exists($className)) {
877            throw new ParsingException("The choice class does not exist " . $className);
878        }
879
880        $myChoice = new $className($this->extractAttributes($data));
881
882        if ($myChoice instanceof ContainerChoice) {
883            $this->parseContainerStatic($data, $myChoice->getBody());
884        } elseif ($myChoice instanceof TextVariableChoice) {
885            //use getBodyData() instead of $data->nodeValue() to preserve xml entities
886            $myChoice->setContent($this->getBodyData($data));
887        } elseif ($myChoice instanceof GapImg) {
888            //extract the media object tag
889            $objectNodes = $this->queryXPath("*[name(.)='object']", $data);
890            foreach ($objectNodes as $objectNode) {
891                $object = $this->buildObject($objectNode);
892                $myChoice->setContent($object);
893                break;
894            }
895        }
896
897        return $myChoice;
898    }
899
900    /**
901     * Short description of method buildResponseDeclaration
902     *
903     * @access public
904     * @author Joel Bout, <joel.bout@tudor.lu>
905     * @param  DOMElement $data
906     * @return \oat\taoQtiItem\model\qti\ResponseDeclaration
907     * @see http://www.imsglobal.org/question/qti_v2p0/imsqti_infov2p0.html#element10074
908     */
909    protected function buildResponseDeclaration(DOMElement $data)
910    {
911
912        $myResponse = new ResponseDeclaration($this->extractAttributes($data), $this->item);
913
914        $data = simplexml_import_dom($data);
915        //set the correct responses
916        $correctResponseNodes = $data->xpath("*[name(.) = 'correctResponse']");
917        $responses = [];
918        foreach ($correctResponseNodes as $correctResponseNode) {
919            foreach ($correctResponseNode->value as $value) {
920                $correct = (string) $value;
921                $response = new Value();
922                foreach ($value->attributes() as $attrName => $attrValue) {
923                    $response->setAttribute($attrName, strval($attrValue));
924                }
925                $response->setValue($correct);
926                $responses[] = $response;
927            }
928            break;
929        }
930        $myResponse->setCorrectResponses($responses);
931
932        //set the correct responses
933        $defaultValueNodes = $data->xpath("*[name(.) = 'defaultValue']");
934        $defaultValues = [];
935        foreach ($defaultValueNodes as $defaultValueNode) {
936            foreach ($defaultValueNode->value as $value) {
937                $default = (string) $value;
938                $defaultValue = new Value();
939                foreach ($value->attributes() as $attrName => $attrValue) {
940                    $defaultValue->setAttribute($attrName, strval($attrValue));
941                }
942                $defaultValue->setValue($default);
943                $defaultValues[] = $defaultValue;
944            }
945            break;
946        }
947        $myResponse->setDefaultValue($defaultValues);
948
949        //set the mapping if defined
950        $mappingNodes = $data->xpath("*[name(.) = 'mapping']");
951        foreach ($mappingNodes as $mappingNode) {
952            if (isset($mappingNode['defaultValue'])) {
953                $myResponse->setMappingDefaultValue(floatval((string) $mappingNode['defaultValue']));
954            }
955            $mappingOptions = [];
956            foreach ($mappingNode->attributes() as $key => $value) {
957                if ($key != 'defaultValue') {
958                    $mappingOptions[$key] = (string) $value;
959                }
960            }
961            $myResponse->setAttribute('mapping', $mappingOptions);
962
963            $mapping = [];
964            foreach ($mappingNode->mapEntry as $mapEntry) {
965                $mapping[(string) htmlspecialchars($mapEntry['mapKey'])] = (string) $mapEntry['mappedValue'];
966            }
967            $myResponse->setMapping($mapping);
968
969            break;
970        }
971
972        //set the areaMapping if defined
973        $mappingNodes = $data->xpath("*[name(.) = 'areaMapping']");
974        foreach ($mappingNodes as $mappingNode) {
975            if (isset($mappingNode['defaultValue'])) {
976                $myResponse->setMappingDefaultValue(floatval((string) $mappingNode['defaultValue']));
977            }
978            $mappingOptions = [];
979            foreach ($mappingNode->attributes() as $key => $value) {
980                if ($key != 'defaultValue') {
981                    $mappingOptions[$key] = (string) $value;
982                }
983            }
984            $myResponse->setAttribute('areaMapping', $mappingOptions);
985
986            $mapping = [];
987            foreach ($mappingNode->areaMapEntry as $mapEntry) {
988                $mappingAttributes = [];
989                foreach ($mapEntry->attributes() as $key => $value) {
990                    $mappingAttributes[(string) $key] = (string) $value;
991                }
992                $mapping[] = $mappingAttributes;
993            }
994            $myResponse->setMapping($mapping, 'area');
995
996            break;
997        }
998
999        return $myResponse;
1000    }
1001
1002    /**
1003     * Short description of method buildOutcomeDeclaration
1004     *
1005     * @access public
1006     * @author Joel Bout, <joel.bout@tudor.lu>
1007     * @param  DOMElement data
1008     * @return oat\taoQtiItem\model\qti\OutcomeDeclaration
1009     */
1010    protected function buildOutcomeDeclaration(DOMElement $data)
1011    {
1012
1013        $outcome = new OutcomeDeclaration($this->extractAttributes($data));
1014        $data = simplexml_import_dom($data);
1015
1016        if (isset($data->defaultValue)) {
1017            if (!is_null($data->defaultValue->value)) {
1018                $outcome->setDefaultValue((string) $data->defaultValue->value);
1019            }
1020        }
1021
1022        return $outcome;
1023    }
1024
1025    /**
1026     * Short description of method buildTemplateResponseProcessing
1027     *
1028     * @access public
1029     * @author Joel Bout, <joel.bout@tudor.lu>
1030     * @param  DOMElement data
1031     * @return oat\taoQtiItem\model\qti\response\ResponseProcessing
1032     */
1033    protected function buildTemplateResponseProcessing(DOMElement $data)
1034    {
1035        $returnValue = null;
1036
1037        if ($data->hasAttribute('template') && $data->childNodes->length === 0) {
1038            $templateUri = (string) $data->getAttribute('template');
1039            $returnValue = new Template($templateUri);
1040        } elseif ($data->childNodes->length === 1) {
1041            //check response declaration identifier, which must be RESPONSE in standard rp
1042            $responses = $this->item->getResponses();
1043            if (count($responses) == 1) {
1044                $response = reset($responses);
1045                if ($response->getIdentifier() !== 'RESPONSE') {
1046                    throw new UnexpectedResponseProcessing('the response declaration identifier must be RESPONSE');
1047                }
1048            } else {
1049                //invalid number of response declaration
1050                throw new UnexpectedResponseProcessing('the item must have exactly one response declaration');
1051            }
1052
1053            $patternCorrectIMS = 'responseCondition [count(./*) = 2 ] [name(./*[1]) = "responseIf" ] '
1054                . '[count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "match" ] '
1055                . '[name(./responseIf/match/*[1]) = "variable" ] [name(./responseIf/match/*[2]) = "correct" ] '
1056                . '[name(./responseIf/*[2]) = "setOutcomeValue" ] '
1057                . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ] [name(./*[2]) = "responseElse" ] '
1058                . '[count(./responseElse/*) = 1 ] [name(./responseElse/*[1]) = "setOutcomeValue" ] '
1059                . '[name(./responseElse/setOutcomeValue/*[1]) = "baseValue"]';
1060            $patternMappingIMS = 'responseCondition [count(./*) = 2] [name(./*[1]) = "responseIf"] '
1061                . '[count(./responseIf/*) = 2] [name(./responseIf/*[1]) = "isNull"] '
1062                . '[name(./responseIf/isNull/*[1]) = "variable"] [name(./responseIf/*[2]) = "setOutcomeValue"] '
1063                . '[name(./responseIf/setOutcomeValue/*[1]) = "variable"] [name(./*[2]) = "responseElse"] '
1064                . '[count(./responseElse/*) = 1] [name(./responseElse/*[1]) = "setOutcomeValue"] '
1065                . '[name(./responseElse/setOutcomeValue/*[1]) = "mapResponse"]';
1066            $patternMappingPointIMS = 'responseCondition [count(./*) = 2] [name(./*[1]) = "responseIf"] '
1067                . '[count(./responseIf/*) = 2] [name(./responseIf/*[1]) = "isNull"] '
1068                . '[name(./responseIf/isNull/*[1]) = "variable"] [name(./responseIf/*[2]) = "setOutcomeValue"] '
1069                . '[name(./responseIf/setOutcomeValue/*[1]) = "variable"] [name(./*[2]) = "responseElse"] '
1070                . '[count(./responseElse/*) = 1] [name(./responseElse/*[1]) = "setOutcomeValue"] '
1071                . '[name(./responseElse/setOutcomeValue/*[1]) = "mapResponsePoint"]';
1072            if (count($this->queryXPath($patternCorrectIMS)) == 1) {
1073                $returnValue = new Template(Template::MATCH_CORRECT);
1074            } elseif (count($this->queryXPath($patternMappingIMS)) == 1) {
1075                $returnValue = new Template(Template::MAP_RESPONSE);
1076            } elseif (count($this->queryXPath($patternMappingPointIMS)) == 1) {
1077                $returnValue = new Template(Template::MAP_RESPONSE_POINT);
1078            } else {
1079                throw new UnexpectedResponseProcessing('not Template, wrong rule');
1080            }
1081            $returnValue->setRelatedItem($this->item);
1082        } else {
1083            throw new UnexpectedResponseProcessing('not Template');
1084        }
1085
1086        return $returnValue;
1087    }
1088
1089    /**
1090     * Short description of method buildResponseProcessing
1091     *
1092     * @access public
1093     * @author Joel Bout, <joel.bout@tudor.lu>
1094     * @param  DOMElement data
1095     * @param  Item item
1096     * @return oat\taoQtiItem\model\qti\response\ResponseProcessing
1097     */
1098    protected function buildResponseProcessing(DOMElement $data, Item $item)
1099    {
1100        $returnValue = null;
1101
1102        // try template
1103        try {
1104            $returnValue = $this->buildTemplateResponseProcessing($data);
1105
1106            try {
1107                //warning: require to add interactions to the item to make it work
1108                $returnValue = TemplatesDriven::takeOverFrom($returnValue, $item);
1109            } catch (TakeoverFailedException $e) {
1110            }
1111        } catch (UnexpectedResponseProcessing $e) {
1112        }
1113
1114        //try templatedriven
1115        if (is_null($returnValue)) {
1116            try {
1117                $returnValue = $this->buildTemplatedrivenResponse($data, $item->getInteractions());
1118            } catch (UnexpectedResponseProcessing $e) {
1119            }
1120        }
1121
1122        // build custom
1123        if (is_null($returnValue)) {
1124            try {
1125                $returnValue = $this->buildCustomResponseProcessing($data);
1126            } catch (UnexpectedResponseProcessing $e) {
1127                // not a Template
1128                common_Logger::e('custom response processing failed', ['TAOITEMS', 'QTI']);
1129            }
1130        }
1131
1132        if (is_null($returnValue)) {
1133            common_Logger::w('failed to determine ResponseProcessing');
1134        }
1135
1136        return $returnValue;
1137    }
1138
1139    /**
1140     * Short description of method buildCompositeResponseProcessing
1141     *
1142     * @access public
1143     * @author Joel Bout, <joel.bout@tudor.lu>
1144     * @param  DOMElement data
1145     * @param  Item item
1146     * @return oat\taoQtiItem\model\qti\response\ResponseProcessing
1147     */
1148    protected function buildCompositeResponseProcessing(DOMElement $data, Item $item)
1149    {
1150        $returnValue = null;
1151
1152        // STRONGLY simplified summation detection
1153        $patternCorrectTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] '
1154            . '[count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "match" ] '
1155            . '[name(./responseIf/match/*[1]) = "variable" ] [name(./responseIf/match/*[2]) = "correct" ] '
1156            . '[name(./responseIf/*[2]) = "setOutcomeValue" ] [count(./responseIf/setOutcomeValue/*) = 1 ] '
1157            . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue"]';
1158        $patternMapTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] '
1159            . '[count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "not" ] [count(./responseIf/not/*) = 1 ] '
1160            . '[name(./responseIf/not/*[1]) = "isNull" ] [count(./responseIf/not/isNull/*) = 1 ] '
1161            . '[name(./responseIf/not/isNull/*[1]) = "variable" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1162            . '[count(./responseIf/setOutcomeValue/*) = 1 ] [name(./responseIf/setOutcomeValue/*[1]) = "mapResponse"]';
1163        $patternMapPointTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] '
1164            . '[count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "not" ] [count(./responseIf/not/*) = 1 ] '
1165            . '[name(./responseIf/not/*[1]) = "isNull" ] [count(./responseIf/not/isNull/*) = 1 ] '
1166            . '[name(./responseIf/not/isNull/*[1]) = "variable" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1167            . '[count(./responseIf/setOutcomeValue/*) = 1 ] '
1168            . '[name(./responseIf/setOutcomeValue/*[1]) = "mapResponsePoint"]';
1169        $patternNoneTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] '
1170            . '[count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "isNull" ] [count(./responseIf/isNull/*) = 1 ] '
1171            . '[name(./responseIf/isNull/*[1]) = "variable" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1172            . '[count(./responseIf/setOutcomeValue/*) = 1 ] [name(./responseIf/setOutcomeValue/*[1]) = "baseValue"]';
1173        $possibleSummation = '/setOutcomeValue [count(./*) = 1 ] [name(./*[1]) = "sum" ]';
1174
1175        $irps = [];
1176        $composition = null;
1177        $data = simplexml_import_dom($data);
1178        foreach ($data as $responseRule) {
1179            if (!is_null($composition)) {
1180                throw new UnexpectedResponseProcessing('Not composite, rules after composition');
1181            }
1182
1183            $subtree = new SimpleXMLElement($responseRule->asXML());
1184
1185            if (count($subtree->xpath($patternCorrectTAO)) > 0) {
1186                $responseIdentifier = (string) $subtree->responseIf->match->variable[0]['identifier'];
1187                $irps[$responseIdentifier] = [
1188                    'class' => 'MatchCorrectTemplate',
1189                    'outcome' => (string) $subtree->responseIf->setOutcomeValue[0]['identifier']
1190                ];
1191            } elseif (count($subtree->xpath($patternMapTAO)) > 0) {
1192                $responseIdentifier = (string) $subtree->responseIf->not->isNull->variable[0]['identifier'];
1193                $irps[$responseIdentifier] = [
1194                    'class' => 'MapResponseTemplate',
1195                    'outcome' => (string) $subtree->responseIf->setOutcomeValue[0]['identifier']
1196                ];
1197            } elseif (count($subtree->xpath($patternMapPointTAO)) > 0) {
1198                $responseIdentifier = (string) $subtree->responseIf->not->isNull->variable[0]['identifier'];
1199                $irps[$responseIdentifier] = [
1200                    'class' => 'MapResponsePointTemplate',
1201                    'outcome' => (string) $subtree->responseIf->setOutcomeValue[0]['identifier']
1202                ];
1203            } elseif (count($subtree->xpath($patternNoneTAO)) > 0) {
1204                $responseIdentifier = (string) $subtree->responseIf->isNull->variable[0]['identifier'];
1205                $irps[$responseIdentifier] = [
1206                    'class' => 'None',
1207                    'outcome' => (string) $subtree->responseIf->setOutcomeValue[0]['identifier'],
1208                    'default' => (string) $subtree->responseIf->setOutcomeValue[0]->baseValue[0]
1209                ];
1210            } elseif (count($subtree->xpath($possibleSummation)) > 0) {
1211                $composition = 'Summation';
1212                $outcomesUsed = [];
1213                foreach ($subtree->xpath('/setOutcomeValue/sum/variable') as $var) {
1214                    $outcomesUsed[] = (string) $var[0]['identifier'];
1215                }
1216            } else {
1217                throw new UnexpectedResponseProcessing('Not composite, unknown rule');
1218            }
1219        }
1220
1221        if (is_null($composition)) {
1222            throw new UnexpectedResponseProcessing('Not composit, Composition rule missing');
1223        }
1224
1225        $responses = [];
1226        foreach ($item->getInteractions() as $interaction) {
1227            $responses[$interaction->getResponse()->getIdentifier()] = $interaction->getResponse();
1228        }
1229
1230        if (count(array_diff(array_keys($irps), array_keys($responses))) > 0) {
1231            throw new UnexpectedResponseProcessing(
1232                'Not composite, no responses for rules: '
1233                    . implode(',', array_diff(array_keys($irps), array_keys($responses)))
1234            );
1235        }
1236        if (count(array_diff(array_keys($responses), array_keys($irps))) > 0) {
1237            throw new UnexpectedResponseProcessing('Not composite, no support for unmatched variables yet');
1238        }
1239
1240        //assuming sum is correct
1241
1242        $compositonRP = new Summation($item);
1243        foreach ($responses as $id => $response) {
1244            $outcome = null;
1245            foreach ($item->getOutcomes() as $possibleOutcome) {
1246                if ($possibleOutcome->getIdentifier() == $irps[$id]['outcome']) {
1247                    $outcome = $possibleOutcome;
1248                    break;
1249                }
1250            }
1251            if (is_null($outcome)) {
1252                throw new ParsingException('Undeclared Outcome in ResponseProcessing');
1253            }
1254            $classname = '\\oat\\taoQtiItem\\model\\qti\\response\\interactionResponseProcessing\\'
1255                . $irps[$id]['class'];
1256            $irp = new $classname($response, $outcome);
1257
1258            if ($irp instanceof None && isset($irps[$id]['default'])) {
1259                $irp->setDefaultValue($irps[$id]['default']);
1260            }
1261            $compositonRP->add($irp);
1262        }
1263        $returnValue = $compositonRP;
1264
1265        return $returnValue;
1266    }
1267
1268    /**
1269     * Short description of method buildCustomResponseProcessing
1270     *
1271     * @access public
1272     * @author Joel Bout, <joel.bout@tudor.lu>
1273     * @param  DOMElement data
1274     * @return oat\taoQtiItem\model\qti\response\ResponseProcessing
1275     */
1276    protected function buildCustomResponseProcessing(DOMElement $data)
1277    {
1278
1279        // Parse to find the different response rules
1280        $responseRules = [];
1281
1282        $data = simplexml_import_dom($data);
1283
1284        $returnValue = new Custom($responseRules, $data->asXml());
1285
1286        return $returnValue;
1287    }
1288
1289    /**
1290     * Short description of method buildExpression
1291     *
1292     * @access public
1293     * @author Joel Bout, <joel.bout@tudor.lu>
1294     * @param  DOMElement data
1295     * @return oat\taoQtiItem\model\qti\response\Rule
1296     */
1297    protected function buildExpression(DOMElement $data)
1298    {
1299        $data = simplexml_import_dom($data);
1300        return ExpressionParserFactory::build($data);
1301    }
1302
1303    protected function getModalFeedback($identifier)
1304    {
1305        foreach ($this->item->getModalFeedbacks() as $feedback) {
1306            if ($feedback->getIdentifier() == $identifier) {
1307                return $feedback;
1308            }
1309        }
1310        throw new ParsingException('cannot found the modal feedback with identifier ' . $identifier);
1311    }
1312
1313    protected function getOutcome($identifier)
1314    {
1315        foreach ($this->item->getOutcomes() as $outcome) {
1316            if ($outcome->getIdentifier() == $identifier) {
1317                return $outcome;
1318            }
1319        }
1320        throw new ParsingException('cannot found the outcome with identifier ' . $identifier);
1321    }
1322
1323    protected function getResponse($identifier)
1324    {
1325        foreach ($this->item->getResponses() as $response) {
1326            if ($response->getIdentifier() == $identifier) {
1327                return $response;
1328            }
1329        }
1330        throw new ParsingException('cannot found the response with identifier ' . $identifier);
1331    }
1332
1333    /**
1334     * Short description of method buildTemplatedrivenResponse
1335     *
1336     * @access public
1337     * @author Joel Bout, <joel.bout@tudor.lu>
1338     * @param DOMElement $data
1339     * @param $interactions
1340     * @return TemplatesDriven
1341     * @throws UnexpectedResponseProcessing
1342     * @throws exception\QtiModelException
1343     * @throws response\InvalidArgumentException
1344     */
1345    protected function buildTemplatedrivenResponse(DOMElement $data, $interactions)
1346    {
1347
1348        $patternCorrectTAO = '/responseCondition [count(./*) = 1 ] [name(./*[1]) = "responseIf" ] '
1349            . '[count(./responseIf/*) = 2 ] [name(./responseIf/*[1]) = "match" ] '
1350            . '[name(./responseIf/match/*[1]) = "variable" ] [name(./responseIf/match/*[2]) = "correct" ] '
1351            . '[name(./responseIf/*[2]) = "setOutcomeValue" ] [name(./responseIf/setOutcomeValue/*[1]) = "sum" ] '
1352            . '[name(./responseIf/setOutcomeValue/sum/*[1]) = "variable" ] '
1353            . '[name(./responseIf/setOutcomeValue/sum/*[2]) = "baseValue"]';
1354        $patternMappingTAO = '/responseCondition [count(./*) = 1] [name(./*[1]) = "responseIf"] '
1355            . '[count(./responseIf/*) = 2] [name(./responseIf/*[1]) = "not"] [name(./responseIf/not/*[1]) = "isNull"] '
1356            . '[name(./responseIf/not/isNull/*[1]) = "variable"] [name(./responseIf/*[2]) = "setOutcomeValue"] '
1357            . '[name(./responseIf/setOutcomeValue/*[1]) = "sum"] '
1358            . '[name(./responseIf/setOutcomeValue/sum/*[1]) = "variable"] '
1359            . '[name(./responseIf/setOutcomeValue/sum/*[2]) = "mapResponse"]';
1360        $patternMappingPointTAO = '/responseCondition [count(./*) = 1] [name(./*[1]) = "responseIf"] '
1361            . '[count(./responseIf/*) = 2] [name(./responseIf/*[1]) = "not"] [name(./responseIf/not/*[1]) = "isNull"] '
1362            . '[name(./responseIf/not/isNull/*[1]) = "variable"] [name(./responseIf/*[2]) = "setOutcomeValue"] '
1363            . '[name(./responseIf/setOutcomeValue/*[1]) = "sum"] '
1364            . '[name(./responseIf/setOutcomeValue/sum/*[1]) = "variable"] '
1365            . '[name(./responseIf/setOutcomeValue/sum/*[2]) = "mapResponsePoint"]';
1366
1367        $subPatternFeedbackOperatorIf = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] '
1368            . '[contains(name(./responseIf/*[1]/*[1]), "map")] [name(./responseIf/*[1]/*[2]) = "baseValue" ] '
1369            . '[name(./responseIf/*[2]) = "setOutcomeValue" ] [name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ]';
1370        $subPatternFeedbackElse = '[name(./*[2]) = "responseElse"] [count(./responseElse/*) = 1 ] '
1371            . '[name(./responseElse/*[1]) = "setOutcomeValue"] '
1372            . '[name(./responseElse/setOutcomeValue/*[1]) = "baseValue"]';
1373        $subPatternFeedbackCorrect = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] '
1374            . '[name(./responseIf/*[1]) = "match" ] [name(./responseIf/*[1]/*[1]) = "variable" ] '
1375            . '[name(./responseIf/*[1]/*[2]) = "correct" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1376            . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ]';
1377        $subPatternFeedbackIncorrect = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] '
1378            . '[name(./responseIf/*[1]) = "not" ] [count(./responseIf/not) = 1 ] '
1379            . '[name(./responseIf/not/*[1]) = "match" ] [name(./responseIf/not/*[1]/*[1]) = "variable" ] '
1380            . '[name(./responseIf/not/*[1]/*[2]) = "correct" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1381            . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ]';
1382        $subPatternFeedbackMatchChoices = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] '
1383            . '[name(./responseIf/*[1]) = "match" ] [name(./responseIf/*[1]/*[2]) = "multiple" ] '
1384            . '[name(./responseIf/*[1]/*[2]/*) = "baseValue" ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1385            . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ] ';
1386        $subPatternFeedbackMatchChoicesEmpty = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] '
1387            . '[name(./responseIf/*[1]) = "match" ] [name(./responseIf/*[1]/*[2]) = "multiple" ] '
1388            . '[count(./responseIf/*[1]/*[2]/*) = 0 ] [name(./responseIf/*[2]) = "setOutcomeValue" ] '
1389            . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ] ';
1390        $subPatternFeedbackMatchChoice = '[name(./*[1]) = "responseIf" ] [count(./responseIf/*) = 2 ] '
1391            . '[name(./responseIf/*[1]) = "match" ] [name(./responseIf/*[1]/*[2]) = "baseValue" ] '
1392            . '[name(./responseIf/*[2]) = "setOutcomeValue" ] '
1393            . '[name(./responseIf/setOutcomeValue/*[1]) = "baseValue" ] ';
1394        $patternFeedbackOperator = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackOperatorIf;
1395        $patternFeedbackOperatorWithElse = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackOperatorIf
1396            . $subPatternFeedbackElse;
1397        $patternFeedbackCorrect = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackCorrect;
1398        $patternFeedbackCorrectWithElse = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackCorrect
1399            . $subPatternFeedbackElse;
1400        $patternFeedbackIncorrect = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackIncorrect;
1401        $patternFeedbackIncorrectWithElse = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackIncorrect
1402            . $subPatternFeedbackElse;
1403        $patternFeedbackMatchChoices = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackMatchChoices;
1404        $patternFeedbackMatchChoicesWithElse  = '/responseCondition [count(./*) = 2 ]'
1405            . $subPatternFeedbackMatchChoices . $subPatternFeedbackElse;
1406        $patternFeedbackMatchChoice = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackMatchChoice;
1407        $patternFeedbackMatchChoicesEmpty = '/responseCondition [count(./*) = 1 ]'
1408            . $subPatternFeedbackMatchChoicesEmpty;
1409        $patternFeedbackMatchChoicesEmptyWithElse  = '/responseCondition [count(./*) = 2 ]'
1410            . $subPatternFeedbackMatchChoicesEmpty . $subPatternFeedbackElse;
1411        $patternFeedbackMatchChoice = '/responseCondition [count(./*) = 1 ]' . $subPatternFeedbackMatchChoice;
1412        $patternFeedbackMatchChoiceWithElse  = '/responseCondition [count(./*) = 2 ]' . $subPatternFeedbackMatchChoice
1413            . $subPatternFeedbackElse;
1414
1415        $rules = [];
1416        $simpleFeedbackRules = [];
1417        $data = simplexml_import_dom($data);
1418
1419        foreach ($data as $responseRule) {
1420            $feedbackRule = null;
1421            $subtree = new SimpleXMLElement($responseRule->asXML());
1422
1423            if (count($subtree->xpath($patternCorrectTAO)) > 0) {
1424                $responseIdentifier = (string) $subtree->responseIf->match->variable['identifier'];
1425                $rules[$responseIdentifier] = Template::MATCH_CORRECT;
1426            } elseif (count($subtree->xpath($patternMappingTAO)) > 0) {
1427                $responseIdentifier = (string) $subtree->responseIf->not->isNull->variable['identifier'];
1428                $rules[$responseIdentifier] = Template::MAP_RESPONSE;
1429            } elseif (count($subtree->xpath($patternMappingPointTAO)) > 0) {
1430                $responseIdentifier = (string) $subtree->responseIf->not->isNull->variable['identifier'];
1431                $rules[$responseIdentifier] = Template::MAP_RESPONSE_POINT;
1432            } elseif (
1433                count($subtree->xpath($patternFeedbackCorrect)) > 0
1434                || count($subtree->xpath($patternFeedbackCorrectWithElse)) > 0
1435            ) {
1436                $feedbackRule = $this->buildSimpleFeedbackRule($subtree, 'correct');
1437            } elseif (
1438                count($subtree->xpath($patternFeedbackIncorrect)) > 0
1439                || count($subtree->xpath($patternFeedbackIncorrectWithElse)) > 0
1440            ) {
1441                $responseIdentifier = (string) $subtree->responseIf->not->match->variable['identifier'];
1442                $feedbackRule = $this->buildSimpleFeedbackRule($subtree, 'incorrect', null, $responseIdentifier);
1443            } elseif (
1444                count($subtree->xpath($patternFeedbackOperator)) > 0
1445                || count($subtree->xpath($patternFeedbackOperatorWithElse)) > 0
1446            ) {
1447                $operator = '';
1448                $responseIdentifier = '';
1449                $value = '';
1450                foreach ($subtree->responseIf->children() as $child) {
1451                    $operator = $child->getName();
1452                    $map = null;
1453                    foreach ($child->children() as $granChild) {
1454                        $map = $granChild->getName();
1455                        $responseIdentifier = (string) $granChild['identifier'];
1456                        break;
1457                    }
1458                    $value = (string) $child->baseValue;
1459                    break;
1460                }
1461                $feedbackRule = $this->buildSimpleFeedbackRule($subtree, $operator, $value);
1462            } elseif (
1463                count($subtree->xpath($patternFeedbackMatchChoices)) > 0
1464                || count($subtree->xpath($patternFeedbackMatchChoicesWithElse)) > 0
1465                || count($subtree->xpath($patternFeedbackMatchChoicesEmpty)) > 0
1466                || count($subtree->xpath($patternFeedbackMatchChoicesEmptyWithElse)) > 0
1467            ) {
1468                $choices = [];
1469                foreach ($subtree->responseIf->match->multiple->baseValue as $choice) {
1470                    $choices[] = (string)$choice;
1471                }
1472                $feedbackRule = $this->buildSimpleFeedbackRule($subtree, 'choices', $choices);
1473            } elseif (
1474                count($subtree->xpath($patternFeedbackMatchChoice)) > 0
1475                || count($subtree->xpath($patternFeedbackMatchChoiceWithElse)) > 0
1476            ) {
1477                $choices = [(string)$subtree->responseIf->match->baseValue];
1478                $feedbackRule = $this->buildSimpleFeedbackRule($subtree, 'choices', $choices);
1479            } else {
1480                throw new UnexpectedResponseProcessing('Not template driven, unknown rule');
1481            }
1482
1483            if (!is_null($feedbackRule)) {
1484                $responseIdentifier = $feedbackRule->comparedOutcome()->getIdentifier();
1485                if (!isset($simpleFeedbackRules[$responseIdentifier])) {
1486                    $simpleFeedbackRules[$responseIdentifier] = [];
1487                }
1488                $simpleFeedbackRules[$responseIdentifier][] = $feedbackRule;
1489            }
1490        }
1491
1492        $responseIdentifiers = [];
1493        foreach ($interactions as $interaction) {
1494            $interactionResponse = $interaction->getResponse();
1495            $responseIdentifier = $interactionResponse->getIdentifier();
1496            $responseIdentifiers[] = $responseIdentifier;
1497
1498            //create and set simple feedback rule here
1499            if (isset($simpleFeedbackRules[$responseIdentifier])) {
1500                foreach ($simpleFeedbackRules[$responseIdentifier] as $rule) {
1501                    $interactionResponse->addFeedbackRule($rule);
1502                }
1503            }
1504        }
1505
1506        //all rules must have been previously identified as belonging to one interaction
1507        if (count(array_diff(array_keys($rules), $responseIdentifiers)) > 0) {
1508            throw new UnexpectedResponseProcessing(
1509                'Not template driven, responseIdentifiers are ' . implode(',', $responseIdentifiers)
1510                    . ' while rules are ' . implode(',', array_keys($rules))
1511            );
1512        }
1513
1514        $templatesDrivenRP = new TemplatesDriven();
1515        foreach ($interactions as $interaction) {
1516            //if a rule has been found for an interaction, apply it. Default to the template NONE otherwise
1517            $pattern = isset($rules[$interaction->getResponse()->getIdentifier()])
1518                ? $rules[$interaction->getResponse()->getIdentifier()]
1519                : Template::NONE;
1520            $templatesDrivenRP->setTemplate($interaction->getResponse(), $pattern);
1521        }
1522        $templatesDrivenRP->setRelatedItem($this->item);
1523        $returnValue = $templatesDrivenRP;
1524
1525        return $returnValue;
1526    }
1527
1528    private function buildSimpleFeedbackRule($subtree, $conditionName, $comparedValue = null, $responseId = '')
1529    {
1530
1531        $responseIdentifier = empty($responseId)
1532            ? (string) $subtree->responseIf->match->variable['identifier']
1533            : $responseId;
1534        $feedbackOutcomeIdentifier = (string) $subtree->responseIf->setOutcomeValue['identifier'];
1535        $feedbackIdentifier = (string) $subtree->responseIf->setOutcomeValue->baseValue;
1536
1537        try {
1538            $response = $this->getResponse($responseIdentifier);
1539            $outcome = $this->getOutcome($feedbackOutcomeIdentifier);
1540            $feedbackThen = $this->getModalFeedback($feedbackIdentifier);
1541
1542            $feedbackElse = null;
1543            if ($subtree->responseElse->getName()) {
1544                $feedbackElseIdentifier = (string) $subtree->responseElse->setOutcomeValue->baseValue;
1545                $feedbackElse = $this->getModalFeedback($feedbackElseIdentifier);
1546            }
1547
1548            $feedbackRule = new SimpleFeedbackRule($outcome, $feedbackThen, $feedbackElse);
1549            $feedbackRule->setCondition($response, $conditionName, $comparedValue);
1550        } catch (ParsingException $e) {
1551            throw new UnexpectedResponseProcessing('Feedback resources not found. Not template driven, unknown rule');
1552        }
1553        return $feedbackRule;
1554    }
1555    /**
1556     * Short description of method buildObject
1557     *
1558     * @access private
1559     * @author Joel Bout, <joel.bout@tudor.lu>
1560     * @param  DOMElement $data
1561     * @return \oat\taoQtiItem\model\qti\QtiObject
1562     */
1563    private function buildObject(DOMElement $data)
1564    {
1565
1566        $attributes = $this->extractAttributes($data);
1567        $returnValue = new QtiObject($attributes);
1568
1569        if ($data->hasChildNodes()) {
1570            $nonEmptyChild = $this->getNonEmptyChildren($data);
1571            if (count($nonEmptyChild) == 1 && reset($nonEmptyChild)->nodeName == 'object') {
1572                $alt = $this->buildObject(reset($nonEmptyChild));
1573                $returnValue->setAlt($alt);
1574            } else {
1575                //get the node xml content
1576                $pattern = ["/^<{$data->nodeName}([^>]*)?>/i", "/<\/{$data->nodeName}([^>]*)?>$/i"];
1577                $content = preg_replace($pattern, '', trim($this->saveXML($data)));
1578                $returnValue->setAlt($content);
1579            }
1580        } else {
1581            $alt = trim($data->nodeValue);
1582            if (!empty($alt)) {
1583                $returnValue->setAlt($alt);
1584            }
1585        }
1586
1587        return $returnValue;
1588    }
1589
1590    private function buildImg(DOMElement $data)
1591    {
1592
1593        $attributes = $this->extractAttributes($data);
1594        $returnValue = new Img($attributes);
1595
1596        return $returnValue;
1597    }
1598
1599    private function buildFigCaption(DOMElement $data): FigCaption
1600    {
1601        $attributes = $this->extractAttributes($data);
1602        $figCaption = new FigCaption($attributes);
1603        $this->parseContainerStatic($data, $figCaption->getBody());
1604
1605        return $figCaption;
1606    }
1607
1608    private function buildTooltip(DOMElement $data, DOMElement $context)
1609    {
1610
1611        $tooltip = null;
1612        $attributes = $this->extractAttributes($data);
1613
1614        // Look for tooltip content
1615        $contentId = $attributes['aria-describedby'];
1616        if (!empty($contentId)) {
1617            $tooltipContentNodes = $this->queryXPath(".//*[@id='$contentId']", $context);
1618            $tooltipContent = $tooltipContentNodes[0];
1619
1620            if (!is_null($tooltipContent)) {
1621                $content = $this->getNodeContentAsHtml($this->data, $tooltipContent);
1622
1623                // Content has been found, we can build the tooltip
1624                $tooltip = new Tooltip($attributes);
1625                $tooltip->setContent($content);
1626
1627                // remove the tooltip content node so it does not pollute the markup
1628                $tooltipContent->parentNode->removeChild($tooltipContent);
1629
1630                // Set the tooltip target
1631                $this->parseContainerStatic($data, $tooltip->getBody());
1632            }
1633        }
1634        return $tooltip;
1635    }
1636
1637    private function getNodeContentAsHtml(DOMDocument $document, DOMElement $node)
1638    {
1639        $html = "";
1640        $children = $node->childNodes;
1641        foreach ($children as $childNode) {
1642            $html .= $document->saveXML($childNode);
1643        }
1644        return $html;
1645    }
1646
1647    private function buildTable(DOMElement $data)
1648    {
1649
1650        $attributes = $this->extractAttributes($data);
1651        $table = new Table($attributes);
1652        $this->parseContainerStatic($data, $table->getBody());
1653
1654        return $table;
1655    }
1656
1657    private function buildFigure(DOMElement $data): Figure
1658    {
1659
1660        $attributes = $this->extractAttributes($data);
1661        $figure = new Figure($attributes);
1662        $this->parseContainerStatic($data, $figure->getBody());
1663
1664        return $figure;
1665    }
1666
1667    private function buildMath(DOMElement $data)
1668    {
1669
1670        $ns = $this->getMathNamespace();
1671        $annotationNodes = $this->queryXPath(".//*[name(.)='" . (empty($ns) ? '' : $ns . ':') . "annotation']", $data);
1672        $annotations = [];
1673        //need to extract the namespace, and clean it in the "bodydata"
1674        foreach ($annotationNodes as $annotationNode) {
1675            $attr = $this->extractAttributes($annotationNode);
1676            $encoding = isset($attr['encoding']) ? strtolower(trim($attr['encoding'])) : '';
1677            $str = $this->getBodyData($annotationNode);
1678            if (!empty($encoding) && !empty($str)) {
1679                $annotations[$encoding] = $str;
1680                $this->deleteNode($annotationNode);
1681            }
1682        }
1683
1684        $math = new Math($this->extractAttributes($data));
1685        $body = $this->getBodyData($data, true);
1686        $math->setMathML($body);
1687        $math->setAnnotations($annotations);
1688
1689        return $math;
1690    }
1691
1692    private function buildXInclude(DOMElement $data)
1693    {
1694
1695        return  new XInclude($this->extractAttributes($data));
1696    }
1697
1698    protected function getNonEmptyChildren(DOMElement $data)
1699    {
1700        $returnValue = [];
1701        foreach ($data->childNodes as $childNode) {
1702            if ($childNode->nodeName == '#text') {
1703                if (trim($childNode->nodeValue) != '') {
1704                    $returnValue[] = $childNode;
1705                }
1706            } else {
1707                $returnValue[] = $childNode;
1708            }
1709        }
1710        return $returnValue;
1711    }
1712
1713    private function buildStylesheet(DOMElement $data)
1714    {
1715        $returnValue = new Stylesheet([
1716            'href' => (string) $data->getAttribute('href'),
1717            'title' => $data->hasAttribute('title') ? (string) $data->getAttribute('title') : '',
1718            'media' => $data->hasAttribute('media') ? (string) $data->getAttribute('media') : 'screen',
1719            'type' => $data->hasAttribute('type') ? (string) $data->getAttribute('type') : 'text/css',
1720        ]);
1721
1722        return $returnValue;
1723    }
1724
1725    private function buildRubricBlock(DOMElement $data)
1726    {
1727
1728        $returnValue = new RubricBlock($this->extractAttributes($data));
1729        $this->parseContainerStatic($data, $returnValue->getBody());
1730
1731        return $returnValue;
1732    }
1733
1734    private function buildFeedback(DOMElement $data, $interactive = false)
1735    {
1736
1737        $type = ucfirst($data->nodeName);
1738        $feedbackClass = '\\oat\\taoQtiItem\\model\\qti\\feedback\\' . $type;
1739        if (!class_exists($feedbackClass)) {
1740            throw new ParsingException('The interaction class cannot be found: ' . $feedbackClass);
1741        }
1742
1743        $attributes = $this->extractAttributes($data);
1744
1745        if ($data->nodeName == 'modalFeedback') {
1746            $myFeedback = new $feedbackClass($attributes, $this->item);
1747            $this->parseContainerStatic($data, $myFeedback->getBody());
1748        } else {
1749            throw new UnsupportedQtiElement($data);
1750        }
1751
1752        return $myFeedback;
1753    }
1754
1755    /**
1756     * Return the list of registered php portable element subclasses
1757     * @return array
1758     */
1759    private function getPortableElementSubclasses($superClassName)
1760    {
1761        $subClasses = [];
1762        foreach (PortableModelRegistry::getRegistry()->getModels() as $model) {
1763            $portableElementClass = $model->getQtiElementClassName();
1764            if (is_subclass_of($portableElementClass, $superClassName)) {
1765                $subClasses[] = $portableElementClass;
1766            }
1767        }
1768        return $subClasses;
1769    }
1770
1771    /**
1772     * Get the PCI class associated to a dom node based on its namespace
1773     * Returns null if not a known PCI model
1774     *
1775     * @param DOMElement $data
1776     * @return null
1777     */
1778    private function getPortableElementClass(DOMElement $data, $superClassName, $portableElementNodeName)
1779    {
1780
1781        $portableElementClasses = $this->getPortableElementSubclasses($superClassName);
1782
1783        //start searching from globally declared namespace
1784        foreach ($this->item->getNamespaces() as $name => $uri) {
1785            foreach ($portableElementClasses as $class) {
1786                if (
1787                    $uri === $class::NS_URI
1788                    && $this->queryXPathChildren([$portableElementNodeName], $data, $name)->length
1789                ) {
1790                    return $class;
1791                }
1792            }
1793        }
1794
1795        //not found as a global namespace definition, try local namespace
1796        if ($this->queryXPathChildren([$portableElementNodeName], $data)->length) {
1797            $pciNode = $this->queryXPathChildren([$portableElementNodeName], $data)[0];
1798            $xmlns = $pciNode->getAttribute('xmlns');
1799            foreach ($portableElementClasses as $phpClass) {
1800                if ($phpClass::NS_URI === $xmlns) {
1801                    return $phpClass;
1802                }
1803            }
1804        }
1805
1806        //not a known portable element type
1807        return null;
1808    }
1809
1810    private function getPciClass(DOMElement $data)
1811    {
1812        return $this->getPortableElementClass(
1813            $data,
1814            'oat\\taoQtiItem\\model\\qti\\interaction\\CustomInteraction',
1815            'portableCustomInteraction'
1816        );
1817    }
1818
1819    private function getPicClass(DOMElement $data)
1820    {
1821        return $this->getPortableElementClass($data, 'oat\\taoQtiItem\\model\\qti\\InfoControl', 'portableInfoControl');
1822    }
1823
1824    /**
1825     * Parse and build a custom interaction object
1826     *
1827     * @param DOMElement $data
1828     * @return CustomInteraction
1829     * @throws ParsingException
1830     */
1831    private function buildCustomInteraction(DOMElement $data)
1832    {
1833
1834        $interaction = null;
1835
1836        $pciClass = $this->getPciClass($data);
1837
1838        if (!empty($pciClass)) {
1839            $ns = null;
1840            foreach ($this->item->getNamespaces() as $name => $uri) {
1841                if ($pciClass::NS_URI === $uri) {
1842                    $ns = new QtiNamespace($uri, $name);
1843                }
1844            }
1845            if (is_null($ns)) {
1846                $pciNodes = $this->queryXPathChildren(['portableCustomInteraction'], $data);
1847                if ($pciNodes->length) {
1848                    $ns = new QtiNamespace($pciNodes->item(0)->getAttribute('xmlns'));
1849                }
1850            }
1851
1852            //use tao's implementation of portable custom interaction
1853            $interaction = new $pciClass($this->extractAttributes($data), $this->item);
1854            $interaction->feed($this, $data, $ns);
1855        } else {
1856            $ciClass = '';
1857            $classes = $data->getAttribute('class');
1858            $classeNames = preg_split('/\s+/', $classes);
1859            foreach ($classeNames as $classeName) {
1860                $ciClass = CustomInteractionRegistry::getCustomInteractionByName($classeName);
1861                if ($ciClass) {
1862                    $interaction = new $ciClass($this->extractAttributes($data), $this->item);
1863                    $interaction->feed($this, $data);
1864                    break;
1865                }
1866            }
1867
1868            if (!$ciClass) {
1869                throw new ParsingException('unknown custom interaction to be build');
1870            }
1871        }
1872
1873        return $interaction;
1874    }
1875
1876    /**
1877     * Parse and build a info control
1878     *
1879     * @param DOMElement $data
1880     * @return InfoControl
1881     * @throws ParsingException
1882     */
1883    private function buildInfoControl(DOMElement $data)
1884    {
1885
1886        $infoControl = null;
1887
1888        $picClass = $this->getPicClass($data);
1889
1890        if (!empty($picClass)) {
1891            $ns = null;
1892            foreach ($this->item->getNamespaces() as $name => $uri) {
1893                if ($picClass::NS_URI === $uri) {
1894                    $ns = new QtiNamespace($uri, $name);
1895                }
1896            }
1897            if (is_null($ns)) {
1898                $pciNodes = $this->queryXPathChildren(['portableInfoControl'], $data);
1899                if ($pciNodes->length) {
1900                    $ns = new QtiNamespace($pciNodes->item(0)->getAttribute('xmlns'));
1901                }
1902            }
1903
1904            //use tao's implementation of portable custom interaction
1905            $infoControl = new PortableInfoControl($this->extractAttributes($data), $this->item);
1906            $infoControl->feed($this, $data, $ns);
1907        } else {
1908            $ciClass = '';
1909            $classes = $data->getAttribute('class');
1910            $classeNames = preg_split('/\s+/', $classes);
1911            foreach ($classeNames as $classeName) {
1912                $ciClass = InfoControlRegistry::getInfoControlByName($classeName);
1913                if ($ciClass) {
1914                    $infoControl = new $ciClass($this->extractAttributes($data), $this->item);
1915                    $infoControl->feed($this, $data);
1916                    break;
1917                }
1918            }
1919
1920            if (!$ciClass) {
1921                throw new UnsupportedQtiElement($data);
1922            }
1923        }
1924
1925        return $infoControl;
1926    }
1927}