Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 20
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractQtiTestExporter
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 20
1056
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getItemExporter
n/a
0 / 0
n/a
0 / 0
0
 adjustTestXml
n/a
0 / 0
n/a
0 / 0
0
 setTestDocument
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTestDocument
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTestService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTestService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setItems
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getItems
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setManifest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getManifest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 preProcessing
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 export
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 exportItems
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 exportTest
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 referenceTest
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 referenceDependency
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 referenceAuxiliaryFile
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 setMetadataExporter
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getMetadataExporter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getServiceManager
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 genericLomOntologyExtractor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
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) 2014-2022 (original work) Open Assessment Technologies SA;
19 */
20
21declare(strict_types=1);
22
23namespace oat\taoQtiTest\models\export;
24
25use common_exception_Error;
26use common_exception_InconsistentData;
27use common_Logger;
28use core_kernel_classes_Resource as Resource;
29use core_kernel_persistence_Exception;
30use DOMDocument;
31use DOMException;
32use DOMXPath;
33use oat\oatbox\reporting\Report;
34use oat\oatbox\reporting\ReportInterface;
35use oat\tao\model\featureFlag\FeatureFlagChecker;
36use oat\taoQtiTest\models\classes\metadata\GenericLomOntologyExtractor;
37use oat\taoQtiTest\models\classes\metadata\MetadataLomService;
38use qtism\data\storage\xml\marshalling\MarshallingException;
39use qtism\data\storage\xml\XmlDocument;
40use oat\oatbox\filesystem\Directory;
41use oat\taoQtiItem\model\qti\metadata\exporter\MetadataExporter;
42use oat\taoQtiItem\model\qti\metadata\MetadataService;
43use oat\oatbox\service\ServiceManager;
44use oat\taoQtiTest\models\export\preprocessor\AssessmentItemRefPreProcessor;
45use qtism\data\storage\xml\XmlStorageException;
46use tao_helpers_Uri;
47use taoItems_models_classes_ItemExporter as ItemExporter;
48use taoQtiTest_models_classes_QtiTestService as QtiTestService;
49use taoQtiTest_models_classes_QtiTestServiceException;
50use ZipArchive;
51
52abstract class AbstractQtiTestExporter extends ItemExporter implements QtiTestExporterInterface
53{
54    /** The QTISM XmlDocument representing the Test to be exported. */
55    private XmlDocument $testDocument;
56
57    /** A reference to the QTI Test Service. */
58    private QtiTestService $testService;
59
60    /**
61     * An array of items related to the current Test Export. The array is associative.
62     * Its keys are actually the assessmentItemRef identifiers.
63     */
64    private array $items;
65
66    /** A DOMDocument representing the IMS Manifest to be populated while exporting the Test. */
67    private DOMDocument $manifest;
68
69    /** Service to export metadata to IMSManifest */
70    protected MetadataExporter $metadataExporter;
71
72    /**
73     * Create a new instance of QtiTestExport.
74     * @param Resource $test The Resource in the ontology representing the QTI Test to be exported.
75     * @param ZipArchive $zip An instance of ZipArchive were components of the QTI Test will be stored into.
76     * @param DOMDocument $manifest A DOMDocument representing the IMS Manifest to be populated during the Test Export.
77     * @throws taoQtiTest_models_classes_QtiTestServiceException
78     */
79    public function __construct(Resource $test, ZipArchive $zip, DOMDocument $manifest)
80    {
81        parent::__construct($test, $zip);
82
83        /** @noinspection PhpParamsInspection */
84        $this->setTestService(QtiTestService::singleton());
85        $this->setTestDocument($this->getTestService()->getDoc($test));
86        $this->setItems($this->getTestService()->getItems($test));
87        $this->setManifest($manifest);
88    }
89
90    abstract protected function getItemExporter(Resource $item): QtiItemExporterInterface;
91
92    abstract protected function adjustTestXml(string $xml): string;
93
94    /** Set the QTISM XmlDocument which holds the QTI Test definition to be exported. */
95    protected function setTestDocument(XmlDocument $testDocument): void
96    {
97        $this->testDocument = $testDocument;
98    }
99
100    /** Get the QTISM XmlDocument which holds the QTI Test definition to be exported. */
101    protected function getTestDocument(): XmlDocument
102    {
103        return $this->testDocument;
104    }
105
106    /** Set a reference on the QTI Test Service. */
107    protected function setTestService(QtiTestService $service): void
108    {
109        $this->testService = $service;
110    }
111
112    /** Get a reference on the QTI Test Service. */
113    protected function getTestService(): QtiTestService
114    {
115        return $this->testService;
116    }
117
118    /**
119     * Set the array of items that are involved in the QTI Test Definition to be exported.
120     * @param array $items An associative array where keys are assessmentItemRef identifiers and values are
121     *                     core_kernel_classes_Resource objects representing the items in the knowledge base.
122     */
123    protected function setItems(array $items): void
124    {
125        $this->items = $items;
126    }
127
128    /**
129     * Get the array of items that are involved in the QTI Test Definition to be exported.
130     * @return array An associative array where keys are assessmentItemRef identifiers and values are
131     *               core_kernel_classes_Resource objects representing the items in the knowledge base.
132     */
133    protected function getItems(): array
134    {
135        return $this->items;
136    }
137
138    /** Set the DOMDocument representing the IMS Manifest to be populated during Test Export. */
139    protected function setManifest(DOMDocument $manifest): void
140    {
141        $this->manifest = $manifest;
142    }
143
144    /** Get the DOMDocument representing the IMS Manifest to be populated during Test Export. */
145    protected function getManifest(): DOMDocument
146    {
147        return $this->manifest;
148    }
149
150    protected function preProcessing(): void
151    {
152        if (!$this->getServiceManager()->has(AssessmentItemRefPreProcessor::SERVICE_ID)) {
153            return;
154        }
155
156        /** @var AssessmentItemRefPreProcessor $preprocessor */
157        $preprocessor = $this->getServiceManager()->get(AssessmentItemRefPreProcessor::SERVICE_ID);
158
159        $this->setItems($preprocessor->process($this->testDocument));
160    }
161
162    /** Export the test definition and all its dependencies (media, items, ...) into the related ZIP archive. */
163    public function export(array $options = []): Report
164    {
165        if ($options['manifest']) {
166            $this->setManifest($options['manifest']);
167        }
168
169        $this->preProcessing();
170
171        // 1. Export the items bound to the test.
172        $report = $this->exportItems();
173
174        // 2. Export the test definition itself.
175        $this->exportTest($report->getData());
176
177        // 3. Export test metadata to manifest
178        $this->getMetadataExporter()->export($this->getItem(), $this->getManifest());
179
180        $this->genericLomOntologyExtractor()->extract(
181            [$this->getItem()],
182            $this->getManifest()
183        );
184
185        // 4. Persist manifest in archive.
186        $this->getZip()->addFromString('imsmanifest.xml', $this->getManifest()->saveXML());
187
188        return $report;
189    }
190
191    /**
192     * Export the dependent items into the ZIP archive.
193     *
194     * @return Report that contains An array of identifiers that were assigned to exported items into the IMS Manifest.
195     * @throws common_exception_Error
196     */
197    protected function exportItems(): Report
198    {
199        $report = Report::createSuccess(__('Export successful for the test "%s"', $this->getItem()->getLabel()));
200
201        $identifiers = [];
202
203        foreach ($this->getItems() as $refIdentifier => $item) {
204            $itemExporter = $this->getItemExporter($item);
205            if (!in_array($itemExporter->buildIdentifier(), $identifiers)) {
206                $identifiers[] = $itemExporter->buildIdentifier();
207                $subReport = $itemExporter->export();
208            }
209
210            // Modify the reference to the item in the test definition.
211            $newQtiItemXmlPath = '../../items/' . tao_helpers_Uri::getUniqueId($item->getUri()) . '/qti.xml';
212            $itemRef = $this->getTestDocument()->getDocumentComponent()->getComponentByIdentifier($refIdentifier);
213            $itemRef->setHref($newQtiItemXmlPath);
214
215            if (
216                $report->getType() !== ReportInterface::TYPE_ERROR &&
217                isset($subReport) &&
218                ($subReport->containsError() || $subReport->getType() === ReportInterface::TYPE_ERROR)
219            ) {
220                // only report errors otherwise the list of report can be very long
221                $report->setType(ReportInterface::TYPE_ERROR);
222                $report->setMessage(__('Export failed for the test "%s"', $this->getItem()->getLabel()));
223                $report->add($subReport);
224            }
225        }
226
227        return $report->setData($identifiers);
228    }
229
230    /**
231     * Export the Test definition itself and its related media.
232     * @param array $itemIdentifiers An array of identifiers that were assigned to exported items into the IMS Manifest.
233     * @throws DOMException
234     * @throws common_exception_InconsistentData
235     * @throws core_kernel_persistence_Exception
236     * @throws XmlStorageException
237     * @throws MarshallingException
238     * @throws taoQtiTest_models_classes_QtiTestServiceException
239     */
240    protected function exportTest(array $itemIdentifiers): void
241    {
242        $testXmlDocument = $this->adjustTestXml($this->getTestDocument()->saveToString());
243
244        $newTestDir = 'tests/' . tao_helpers_Uri::getUniqueId($this->getItem()->getUri()) . '/';
245        $testRootDir = $this->getTestService()->getQtiTestDir($this->getItem());
246        $testHref = $newTestDir . 'test.xml';
247
248        common_Logger::t('TEST DEFINITION AT: ' . $testHref);
249
250        $this->getZip()->addFromString($testHref, $testXmlDocument);
251        $this->referenceTest($testHref, $itemIdentifiers);
252
253        $iterator = $testRootDir->getFlyIterator(Directory::ITERATOR_RECURSIVE | Directory::ITERATOR_FILE);
254        $indexFile = pathinfo(QtiTestService::QTI_TEST_DEFINITION_INDEX, PATHINFO_BASENAME);
255        foreach ($iterator as $f) {
256            // Only add dependency files...
257            if ($f->getBasename() !== QtiTestService::TAOQTITEST_FILENAME && $f->getBasename() !== $indexFile) {
258                // Add the file to the archive.
259                $fileHref = $newTestDir . $f->getBaseName();
260                common_Logger::t('AUXILIARY FILE AT: ' . $fileHref);
261                $this->getZip()->addFromString($fileHref, $f->read());
262                $this->referenceAuxiliaryFile($fileHref);
263            }
264        }
265    }
266
267    /**
268     * Reference the test into the IMS Manifest.
269     *
270     * @param string $href The path (base path is the ZIP archive) to the test definition.
271     * @param array $itemIdentifiers An array of identifiers that were assigned to exported items into the IMS Manifest.
272     * @throws DOMException
273     */
274    protected function referenceTest(string $href, array $itemIdentifiers): void
275    {
276        $identifier = tao_helpers_Uri::getUniqueId($this->getItem()->getUri());
277        $manifest = $this->getManifest();
278
279        // Identify the target node.
280        $resources = $manifest->getElementsByTagName('resources');
281        $targetElt = $resources->item(0);
282
283        // Create the IMS Manifest <resource> element.
284        $resourceElt = $manifest->createElement('resource');
285        $resourceElt->setAttribute('identifier', $identifier);
286        $resourceElt->setAttribute('type', static::TEST_RESOURCE_TYPE);
287        $resourceElt->setAttribute('href', $href);
288        $targetElt->appendChild($resourceElt);
289
290        // Append an IMS Manifest <file> element referencing the test definition.
291        $fileElt = $manifest->createElement('file');
292        $fileElt->setAttribute('href', $href);
293        $resourceElt->appendChild($fileElt);
294
295        foreach ($itemIdentifiers as $itemIdentifier) {
296            $this->referenceDependency($itemIdentifier);
297        }
298    }
299
300    /**
301     * Reference a test dependency (i.e. Items related to the test) in the IMS Manifest.
302     * @param string $identifierRef The identifier of the item resource in the IMS Manifest.
303     * @throws DOMException
304     */
305    protected function referenceDependency(string $identifierRef): void
306    {
307        $manifest = $this->getManifest();
308        $xpath = new DOMXpath($manifest);
309
310        $identifier = tao_helpers_Uri::getUniqueId($this->getItem()->getUri());
311
312        $search = $xpath->query("//resource[@identifier='${identifier}']");
313        $resourceElement = $search->item(0);
314
315        // Append IMS Manifest <dependency> elements referencing $identifierRef.
316        $dependencyElement = $manifest->createElement('dependency');
317        $dependencyElement->setAttribute('identifierref', $identifierRef);
318
319        $resourceElement->appendChild($dependencyElement);
320    }
321
322    /**
323     * Reference a Test Auxiliary File (e.g. media, stylesheet, ...) in the IMS Manifest.
324     * @param string $href The path (base path is the ZIP archive) to the auxiliary file in the ZIP archive.
325     * @throws DOMException
326     */
327    protected function referenceAuxiliaryFile(string $href): void
328    {
329        $manifest = $this->getManifest();
330        $xpath = new DOMXPath($manifest);
331
332        $testIdentifier = tao_helpers_Uri::getUniqueId($this->getItem()->getUri());
333
334        // Find the first <dependency> element.
335        $dependencies = $xpath->query("//resource[@identifier='${testIdentifier}']/dependency");
336        $firstDependencyElement = $dependencies->item(0);
337
338        // Create an IMS Manifest <file> element.
339        $fileElement = $manifest->createElement('file');
340        $fileElement->setAttribute('href', ltrim($href, '/'));
341
342        $firstDependencyElement->parentNode->insertBefore($fileElement, $firstDependencyElement);
343    }
344
345    protected function setMetadataExporter(): MetadataExporter
346    {
347        $this->metadataExporter = $this->getServiceManager()->get(MetadataService::SERVICE_ID)->getExporter();
348
349        return $this->metadataExporter;
350    }
351
352    protected function getMetadataExporter(): MetadataExporter
353    {
354        return $this->metadataExporter ?? $this->setMetadataExporter();
355    }
356
357    protected function getServiceManager(): ServiceManager
358    {
359        return ServiceManager::getServiceManager();
360    }
361
362    private function genericLomOntologyExtractor(): GenericLomOntologyExtractor
363    {
364        return $this->getServiceManager()->getContainer()->get(GenericLomOntologyExtractor::class);
365    }
366}