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