Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
83.72% |
72 / 86 |
|
42.86% |
3 / 7 |
CRAP | |
0.00% |
0 / 1 |
AbstractQtiConverter | |
83.72% |
72 / 86 |
|
42.86% |
3 / 7 |
31.38 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
convertToQti2 | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
convertRootElementsRecursively | |
60.00% |
15 / 25 |
|
0.00% |
0 / 1 |
18.74 | |||
adjustAttributes | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
convertRootElement | |
96.43% |
27 / 28 |
|
0.00% |
0 / 1 |
2 | |||
getRootElement | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
isTagValid | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
3.05 | |||
canBeConverted | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 |
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) 2024 (original work) Open Assessment Technologies SA; |
19 | */ |
20 | |
21 | declare(strict_types=1); |
22 | |
23 | namespace oat\taoQtiItem\model\qti\converter; |
24 | |
25 | use common_Logger; |
26 | use DOMComment; |
27 | use DOMDocument; |
28 | use DOMElement; |
29 | use DOMText; |
30 | use DOMXPath; |
31 | use oat\taoQtiItem\model\ValidationService; |
32 | |
33 | abstract class AbstractQtiConverter |
34 | { |
35 | private const QTI_22_NS = 'http://www.imsglobal.org/xsd/imsqti_v2p2'; |
36 | private const QTI_3_NS = 'http://www.imsglobal.org/xsd/imsqtiasi_v3p0'; |
37 | private const XSI_NAMESPACE = 'http://www.w3.org/2001/XMLSchema-instance'; |
38 | private const XSI_SCHEMA_LOCATION = 'xsi:schemaLocation'; |
39 | private const SCHEMA_LOCATION = 'http://www.imsglobal.org/xsd/imsqti_v2p2 ' . |
40 | 'http://www.imsglobal.org/xsd/qti/qtiv2p2/imsqti_v2p2p1.xsd'; |
41 | private const XML_NS_NAMESPACE = 'http://www.w3.org/2000/xmlns/'; |
42 | private const QUALIFIED_NAME_NS = 'xmlns'; |
43 | private const QUALIFIED_NAME_XSI = self::QUALIFIED_NAME_NS . ':xsi'; |
44 | |
45 | private CaseConversionService $caseConversionService; |
46 | private ValidationService $validationService; |
47 | |
48 | public function __construct(CaseConversionService $caseConversionService, ValidationService $validationService) |
49 | { |
50 | $this->caseConversionService = $caseConversionService; |
51 | $this->validationService = $validationService; |
52 | } |
53 | |
54 | public function convertToQti2(string $filename): void |
55 | { |
56 | // Load the QTI XML document |
57 | $dom = new DOMDocument(); |
58 | $dom->preserveWhiteSpace = false; |
59 | $dom->formatOutput = true; |
60 | $dom->load($filename); |
61 | foreach (iterator_to_array($dom->childNodes) as $child) { |
62 | if ($this->canBeConverted($child)) { |
63 | $this->convertRootElementsRecursively(iterator_to_array($child->childNodes)); |
64 | $this->convertRootElement($child); |
65 | $dom->save($filename); |
66 | } |
67 | } |
68 | } |
69 | |
70 | private function convertRootElementsRecursively(array $children): void |
71 | { |
72 | foreach ($children as $child) { |
73 | if ($child instanceof DOMText || $child instanceof DOMComment) { |
74 | continue; |
75 | } |
76 | |
77 | if ($child instanceof DOMElement) { |
78 | $childNodes = null; |
79 | if ($child->hasChildNodes()) { |
80 | $this->convertRootElementsRecursively(iterator_to_array($child->childNodes)); |
81 | $childNodes = $child->childNodes; |
82 | } |
83 | } |
84 | |
85 | $tagName = $child->tagName; |
86 | // When elements has child we do not want to create literal value |
87 | $nodeValue = $child->childElementCount === 0 ? $child->nodeValue : ''; |
88 | |
89 | $convertedTag = $this->caseConversionService->kebabToCamelCase($tagName); |
90 | // Check if converted tag name is valid against defined QTI 2.2 namespace |
91 | if (!$this->isTagValid($convertedTag)) { |
92 | common_Logger::w(sprintf('Invalid tag name: %s, When importing', $convertedTag)); |
93 | $child->remove(); |
94 | continue; |
95 | } |
96 | |
97 | $newElement = $child->ownerDocument->createElement( |
98 | $convertedTag, |
99 | $nodeValue |
100 | ); |
101 | |
102 | if ($childNodes) { |
103 | foreach (iterator_to_array($childNodes) as $childNode) { |
104 | if ($childNode instanceof DOMElement) { |
105 | $newElement->appendChild($childNode); |
106 | } |
107 | } |
108 | } |
109 | |
110 | $this->adjustAttributes($newElement, $child); |
111 | $child->parentNode->replaceChild($newElement, $child); |
112 | } |
113 | } |
114 | |
115 | private function adjustAttributes(DOMElement $newElement, DOMElement $childNode) |
116 | { |
117 | foreach (iterator_to_array($childNode->attributes) as $attribute) { |
118 | //If attribute name is not equal to nodeName it's most probably namespace attribute |
119 | //This will be hardcoded below therefore we wish to ignore it. |
120 | if ($attribute->name !== $attribute->nodeName) { |
121 | continue; |
122 | } |
123 | |
124 | // Only replace attribute names from map |
125 | if ($attrReplacement = $this->caseConversionService->kebabToCamelCase($attribute->name)) { |
126 | $newElement->setAttribute($attrReplacement, $attribute->value); |
127 | continue; |
128 | } |
129 | |
130 | $newElement->setAttribute($attribute->name, $attribute->value); |
131 | } |
132 | } |
133 | |
134 | private function convertRootElement(DOMElement $rootElement): void |
135 | { |
136 | $newElement = $rootElement->ownerDocument->createElementNS( |
137 | self::QTI_22_NS, |
138 | $this->caseConversionService->kebabToCamelCase($this->getRootElement()) |
139 | ); |
140 | |
141 | $newElement->setAttributeNS( |
142 | self::XSI_NAMESPACE, |
143 | self::XSI_SCHEMA_LOCATION, |
144 | self::XSI_NAMESPACE |
145 | ); |
146 | |
147 | $newElement->setAttributeNS( |
148 | self::XSI_NAMESPACE, |
149 | self::XSI_SCHEMA_LOCATION, |
150 | self::SCHEMA_LOCATION |
151 | ); |
152 | |
153 | $newElement->setAttributeNS( |
154 | self::XSI_NAMESPACE, |
155 | self::XSI_SCHEMA_LOCATION, |
156 | self::SCHEMA_LOCATION |
157 | ); |
158 | |
159 | $newElement->setAttributeNS( |
160 | self::XML_NS_NAMESPACE, |
161 | self::QUALIFIED_NAME_XSI, |
162 | self::XSI_NAMESPACE |
163 | ); |
164 | |
165 | foreach (iterator_to_array($rootElement->childNodes) as $childNode) { |
166 | $newElement->appendChild($childNode); |
167 | } |
168 | |
169 | $this->adjustAttributes($newElement, $rootElement); |
170 | $rootElement->parentNode->replaceChild($newElement, $rootElement); |
171 | } |
172 | |
173 | abstract protected function getRootElement(): string; |
174 | |
175 | private function isTagValid(string $convertedTag): bool |
176 | { |
177 | $validationSchema = $this->validationService->getContentValidationSchema(self::QTI_22_NS); |
178 | foreach ($validationSchema as $schema) { |
179 | $xsdDom = new DOMDocument(); |
180 | $xsdDom->load($schema); |
181 | $xpath = new DOMXPath($xsdDom); |
182 | $xpath->registerNamespace('xs', 'http://www.w3.org/2001/XMLSchema'); |
183 | $elements = $xpath->query(sprintf("//xs:element[@name='%s']", $convertedTag)); |
184 | |
185 | if ($elements->count() === 0) { |
186 | return false; |
187 | } |
188 | |
189 | return true; |
190 | } |
191 | |
192 | return false; |
193 | } |
194 | |
195 | /** |
196 | * We can only apply conversion into root element equal to defined root element const |
197 | * We should ignore other dom object other DOMElement |
198 | * We should only apply if the namespace is equal to QTI 3.0 |
199 | */ |
200 | private function canBeConverted(mixed $child): bool |
201 | { |
202 | return $child instanceof DOMElement |
203 | && $child->tagName === $this->getRootElement() |
204 | && $child->getAttributeNode('xmlns') !== null |
205 | && $child->getAttributeNode('xmlns')->nodeValue === self::QTI_3_NS; |
206 | } |
207 | } |