Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
1.86% |
8 / 431 |
|
4.00% |
1 / 25 |
CRAP | |
0.00% |
0 / 1 |
| ImportService | |
1.86% |
8 / 431 |
|
4.00% |
1 / 25 |
14895.98 | |
0.00% |
0 / 1 |
| singleton | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| importQTIFile | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
| createRdfItem | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
| createQtiItemModel | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
56 | |||
| createQtiManifest | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
| importQTIPACKFile | |
0.00% |
0 / 100 |
|
0.00% |
0 / 1 |
380 | |||
| checkImportLockTime | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| importQtiItem | |
0.00% |
0 / 193 |
|
0.00% |
0 / 1 |
2652 | |||
| validResponseProcessing | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
| getOutcomesIds | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
| getSetOutcomeValueIds | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
| getResponseProcessingRules | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
| importResourceMetadata | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
| rollback | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
| getMetadataImporter | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| getMetaMetadataExtractor | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getTargetClassForAssets | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
| getItemEventDispatcher | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getMappedMetadataInjector | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getMetaMetadataImportMapper | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getUniqueNumericQtiIdentifierReplacer | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| replaceUniqueNumericQtiIdentifier | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| convertToQti2 | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getItemConverter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| checkMissingClassProperties | |
0.00% |
0 / 5 |
|
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) 2016-2024 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); |
| 19 | * |
| 20 | */ |
| 21 | |
| 22 | namespace oat\taoQtiItem\model\qti; |
| 23 | |
| 24 | use common_exception_Error; |
| 25 | use common_exception_UserReadableException; |
| 26 | use common_Logger; |
| 27 | use core_kernel_classes_Class; |
| 28 | use core_kernel_classes_Resource; |
| 29 | use DOMDocument; |
| 30 | use Exception; |
| 31 | use helpers_File; |
| 32 | use oat\generis\model\OntologyAwareTrait; |
| 33 | use oat\oatbox\reporting\Report; |
| 34 | use oat\tao\model\TaoOntology; |
| 35 | use oat\oatbox\mutex\LockTrait; |
| 36 | use oat\taoItems\model\media\ItemMediaResolver; |
| 37 | use oat\taoItems\model\media\LocalItemSource; |
| 38 | use oat\taoQtiItem\helpers\Authoring; |
| 39 | use oat\taoQtiItem\model\ItemModel; |
| 40 | use oat\taoQtiItem\model\portableElement\exception\PortableElementException; |
| 41 | use oat\taoQtiItem\model\portableElement\exception\PortableElementInvalidModelException; |
| 42 | use oat\taoQtiItem\model\qti\asset\AssetManager; |
| 43 | use oat\taoQtiItem\model\qti\asset\handler\LocalAssetHandler; |
| 44 | use oat\taoQtiItem\model\qti\asset\handler\PortableAssetHandler; |
| 45 | use oat\taoQtiItem\model\qti\asset\handler\SharedStimulusAssetHandler; |
| 46 | use oat\taoQtiItem\model\qti\asset\handler\StimulusHandler; |
| 47 | use oat\taoQtiItem\model\qti\converter\ItemConverter; |
| 48 | use oat\taoQtiItem\model\qti\event\UpdatedItemEventDispatcher; |
| 49 | use oat\taoQtiItem\model\qti\exception\ExtractException; |
| 50 | use oat\taoQtiItem\model\qti\exception\ParsingException; |
| 51 | use oat\taoQtiItem\model\qti\exception\TemplateException; |
| 52 | use oat\taoQtiItem\model\qti\metadata\importer\MetadataImporter; |
| 53 | use oat\taoQtiItem\model\qti\metadata\importer\MetaMetadataImportMapper; |
| 54 | use oat\taoQtiItem\model\qti\metadata\imsManifest\MetaMetadataExtractor; |
| 55 | use oat\taoQtiItem\model\qti\metadata\MetadataGuardianResource; |
| 56 | use oat\taoQtiItem\model\qti\metadata\MetadataService; |
| 57 | use oat\taoQtiItem\model\qti\metadata\ontology\MappedMetadataInjector; |
| 58 | use oat\taoQtiItem\model\qti\parser\UniqueNumericQtiIdentifierReplacer; |
| 59 | use oat\taoQtiItem\model\qti\parser\ValidationException; |
| 60 | use oat\taoQtiItem\model\event\ItemImported; |
| 61 | use qtism\data\QtiComponentCollection; |
| 62 | use qtism\data\rules\SetOutcomeValue; |
| 63 | use qtism\data\storage\xml\XmlDocument; |
| 64 | use qtism\data\storage\xml\XmlStorageException; |
| 65 | use qtism\runtime\processing\ResponseProcessingEngine; |
| 66 | use qtism\runtime\tests\AssessmentItemSession; |
| 67 | use qtism\runtime\tests\SessionManager; |
| 68 | use tao_helpers_File; |
| 69 | use taoItems_models_classes_ItemsService; |
| 70 | use oat\oatbox\event\EventManager; |
| 71 | use oat\oatbox\service\ServiceManager; |
| 72 | use oat\oatbox\service\ConfigurableService; |
| 73 | |
| 74 | /** |
| 75 | * Short description of class oat\taoQtiItem\model\qti\ImportService |
| 76 | * |
| 77 | * @access public |
| 78 | * @author Joel Bout, <joel.bout@tudor.lu> |
| 79 | * @package taoQTI |
| 80 | */ |
| 81 | class ImportService extends ConfigurableService |
| 82 | { |
| 83 | use OntologyAwareTrait; |
| 84 | |
| 85 | use LockTrait; |
| 86 | |
| 87 | public const SERVICE_ID = 'taoQtiItem/ImportService'; |
| 88 | |
| 89 | /** |
| 90 | * Checks that setOutcomeValue declared in the outcomeDeclaration |
| 91 | */ |
| 92 | public const CONFIG_VALIDATE_RESPONSE_PROCESSING = 'validateResponseProcessing'; |
| 93 | |
| 94 | /** |
| 95 | * TTL of the item importing process |
| 96 | * How long item will be locked while lock service automatically release the lock |
| 97 | */ |
| 98 | public const OPTION_IMPORT_LOCK_TTL = 'importLockTtl'; |
| 99 | |
| 100 | public const PROPERTY_QTI_ITEM_IDENTIFIER = 'http://www.tao.lu/Ontologies/TAOItem.rdf#QtiItemIdentifier'; |
| 101 | |
| 102 | /** |
| 103 | * @return ImportService |
| 104 | */ |
| 105 | public static function singleton() |
| 106 | { |
| 107 | return ServiceManager::getServiceManager()->get(self::SERVICE_ID); |
| 108 | } |
| 109 | |
| 110 | /** |
| 111 | * @var MetadataImporter Service to manage Lom metadata during package import |
| 112 | */ |
| 113 | protected $metadataImporter; |
| 114 | |
| 115 | /** |
| 116 | * Short description of method importQTIFile |
| 117 | * |
| 118 | * @access public |
| 119 | * @param $qtiFile |
| 120 | * @param core_kernel_classes_Class $itemClass |
| 121 | * @param bool $validate |
| 122 | * @return Report |
| 123 | * @throws \common_ext_ExtensionException |
| 124 | * @throws common_exception_Error |
| 125 | * @throws \common_Exception |
| 126 | * @author Joel Bout, <joel.bout@tudor.lu> |
| 127 | */ |
| 128 | public function importQTIFile($qtiFile, core_kernel_classes_Class $itemClass, $validate = true) |
| 129 | { |
| 130 | try { |
| 131 | $qtiModel = $this->createQtiItemModel($qtiFile, $validate); |
| 132 | $rdfItem = $this->createRdfItem($itemClass, $qtiModel); |
| 133 | |
| 134 | $report = Report::createSuccess(__('The IMS QTI Item was successfully imported.'), $rdfItem); |
| 135 | } catch (ValidationException $ve) { |
| 136 | $report = Report::createFailure(__('The IMS QTI Item could not be imported.')); |
| 137 | $report->add($ve->getReport()); |
| 138 | } |
| 139 | |
| 140 | return $report; |
| 141 | } |
| 142 | |
| 143 | /** |
| 144 | * |
| 145 | * @param core_kernel_classes_Class $itemClass |
| 146 | * @param Item $qtiModel |
| 147 | * @param Resource $qtiItemResource |
| 148 | * @return core_kernel_classes_Resource |
| 149 | * @throws \common_Exception |
| 150 | * @throws common_exception_Error |
| 151 | */ |
| 152 | protected function createRdfItem(core_kernel_classes_Class $itemClass, Item $qtiModel) |
| 153 | { |
| 154 | $itemService = taoItems_models_classes_ItemsService::singleton(); |
| 155 | $qtiService = Service::singleton(); |
| 156 | |
| 157 | if (!$itemService->isItemClass($itemClass)) { |
| 158 | throw new common_exception_Error('provided non Itemclass for ' . __FUNCTION__); |
| 159 | } |
| 160 | |
| 161 | $rdfItem = $itemService->createInstance($itemClass); |
| 162 | |
| 163 | //set the QTI type |
| 164 | $itemService->setItemModel($rdfItem, new core_kernel_classes_Resource(ItemModel::MODEL_URI)); |
| 165 | |
| 166 | //set the label |
| 167 | $label = ''; |
| 168 | if ($qtiModel->hasAttribute('label')) { |
| 169 | $label = $qtiModel->getAttributeValue('label'); |
| 170 | } |
| 171 | |
| 172 | if (empty($label)) { |
| 173 | $label = $qtiModel->getAttributeValue('title'); |
| 174 | } |
| 175 | $rdfItem->setLabel($label); |
| 176 | |
| 177 | //save itemcontent |
| 178 | if (!$qtiService->saveDataItemToRdfItem($qtiModel, $rdfItem)) { |
| 179 | throw new \common_Exception('Unable to save item'); |
| 180 | } |
| 181 | |
| 182 | |
| 183 | return $rdfItem; |
| 184 | } |
| 185 | |
| 186 | protected function createQtiItemModel($qtiFile, $validate = true) |
| 187 | { |
| 188 | $qtiXml = Authoring::sanitizeQtiXml($qtiFile); |
| 189 | $qtiXml = $this->replaceUniqueNumericQtiIdentifier($qtiXml); |
| 190 | //validate the file to import |
| 191 | $qtiParser = new Parser($qtiXml); |
| 192 | |
| 193 | if ($validate) { |
| 194 | $qtiParser->validate(); |
| 195 | |
| 196 | if (!$qtiParser->isValid()) { |
| 197 | $eStrs = []; |
| 198 | foreach ($qtiParser->getErrors() as $libXmlError) { |
| 199 | // phpcs:disable Generic.Files.LineLength |
| 200 | $eStrs[] = __('QTI-XML error at line %1$d "%2$s".', $libXmlError['line'], str_replace('[LibXMLError] ', '', trim($libXmlError['message']))); |
| 201 | // phpcs:enable Generic.Files.LineLength |
| 202 | } |
| 203 | |
| 204 | // Make sure there are no duplicate... |
| 205 | $eStrs = array_unique($eStrs); |
| 206 | |
| 207 | // Add sub-report. |
| 208 | throw new ValidationException($qtiFile, $eStrs); |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | $qtiItem = $qtiParser->load(); |
| 213 | if (!$qtiItem && count($qtiParser->getErrors())) { |
| 214 | $errors = []; |
| 215 | foreach ($qtiParser->getErrors() as $error) { |
| 216 | $errors[] = $error['message']; |
| 217 | } |
| 218 | |
| 219 | throw new ValidationException($qtiFile, $errors); |
| 220 | } |
| 221 | |
| 222 | return $qtiItem; |
| 223 | } |
| 224 | |
| 225 | protected function createQtiManifest($manifestFile, $validate = true) |
| 226 | { |
| 227 | //load and validate the manifest |
| 228 | $qtiManifestParser = new ManifestParser($manifestFile); |
| 229 | |
| 230 | if ($validate) { |
| 231 | $qtiManifestParser->validate(); |
| 232 | |
| 233 | if (!$qtiManifestParser->isValid()) { |
| 234 | $eStrs = []; |
| 235 | foreach ($qtiManifestParser->getErrors() as $libXmlError) { |
| 236 | if (isset($libXmlError['line'])) { |
| 237 | // phpcs:disable Generic.Files.LineLength |
| 238 | $error = __('XML error at line %1$d "%2$s".', $libXmlError['line'], str_replace('[LibXMLError] ', '', trim($libXmlError['message']))); |
| 239 | // phpcs:enable Generic.Files.LineLength |
| 240 | } else { |
| 241 | // phpcs:disable Generic.Files.LineLength |
| 242 | $error = __('XML error "%1$s".', str_replace('[LibXMLError] ', '', trim($libXmlError['message']))); |
| 243 | // phpcs:enable Generic.Files.LineLength |
| 244 | } |
| 245 | $eStrs[] = $error; |
| 246 | } |
| 247 | |
| 248 | // Add sub-report. |
| 249 | throw new ValidationException($manifestFile, $eStrs); |
| 250 | } |
| 251 | } |
| 252 | |
| 253 | return $qtiManifestParser->load(); |
| 254 | } |
| 255 | |
| 256 | /** |
| 257 | * imports a qti package and |
| 258 | * returns the number of items imported |
| 259 | * |
| 260 | * @access public |
| 261 | * @param $file |
| 262 | * @param core_kernel_classes_Class $itemClass |
| 263 | * @param bool $validate |
| 264 | * @param bool $rollbackOnError |
| 265 | * @param bool $rollbackOnWarning |
| 266 | * @param bool $enableMetadataGuardians |
| 267 | * @param bool $enableMetadataValidators |
| 268 | * @param bool $itemMustExist |
| 269 | * @param bool $itemMustBeOverwritten |
| 270 | * @return Report |
| 271 | * @throws Exception |
| 272 | * @throws ExtractException |
| 273 | * @throws ParsingException |
| 274 | * @throws \common_Exception |
| 275 | * @throws \common_ext_ExtensionException |
| 276 | * @throws common_exception_Error |
| 277 | * @author Joel Bout, <joel.bout@tudor.lu> |
| 278 | */ |
| 279 | public function importQTIPACKFile( |
| 280 | $file, |
| 281 | core_kernel_classes_Class $itemClass, |
| 282 | $validate = true, |
| 283 | $rollbackOnError = false, |
| 284 | $rollbackOnWarning = false, |
| 285 | $enableMetadataGuardians = true, |
| 286 | $enableMetadataValidators = true, |
| 287 | $itemMustExist = false, |
| 288 | $itemMustBeOverwritten = false, |
| 289 | $importMetadataEnabled = false |
| 290 | ) { |
| 291 | $initialLogMsg = "Importing QTI Package with the following options:\n"; |
| 292 | $initialLogMsg .= '- Rollback On Warning: ' . json_encode($rollbackOnWarning) . "\n"; |
| 293 | $initialLogMsg .= '- Rollback On Error: ' . json_encode($rollbackOnError) . "\n"; |
| 294 | $initialLogMsg .= '- Enable Metadata Guardians: ' . json_encode($enableMetadataGuardians) . "\n"; |
| 295 | $initialLogMsg .= '- Enable Metadata Validators: ' . json_encode($enableMetadataValidators) . "\n"; |
| 296 | $initialLogMsg .= '- Item Must Exist: ' . json_encode($itemMustExist) . "\n"; |
| 297 | $initialLogMsg .= '- Item Must Be Overwritten: ' . json_encode($itemMustBeOverwritten) . "\n"; |
| 298 | $initialLogMsg .= '- Import Metadata Enabled: ' . json_encode($importMetadataEnabled) . "\n"; |
| 299 | \common_Logger::d($initialLogMsg); |
| 300 | |
| 301 | //load and validate the package |
| 302 | $qtiPackageParser = new PackageParser($file); |
| 303 | |
| 304 | if ($validate) { |
| 305 | $qtiPackageParser->validate(); |
| 306 | if (!$qtiPackageParser->isValid()) { |
| 307 | throw new ParsingException('Invalid QTI package format'); |
| 308 | } |
| 309 | } |
| 310 | |
| 311 | //extract the package |
| 312 | $folder = $qtiPackageParser->extract(); |
| 313 | if (!is_dir($folder)) { |
| 314 | throw new ExtractException(); |
| 315 | } |
| 316 | |
| 317 | $report = new Report(Report::TYPE_SUCCESS, ''); |
| 318 | $successItems = []; |
| 319 | $allCreatedClasses = []; |
| 320 | $overwrittenItems = []; |
| 321 | $itemCount = 0; |
| 322 | |
| 323 | try { |
| 324 | // The metadata import feature needs a DOM representation of the manifest. |
| 325 | $domManifest = new DOMDocument('1.0', 'UTF-8'); |
| 326 | $domManifest->load($folder . 'imsmanifest.xml'); |
| 327 | |
| 328 | /** @var Resource[] $qtiItemResources */ |
| 329 | $qtiItemResources = $this->createQtiManifest($folder . 'imsmanifest.xml'); |
| 330 | |
| 331 | if ($importMetadataEnabled) { |
| 332 | $metaMetadataValues = $this->getMetaMetadataExtractor()->extract($domManifest); |
| 333 | $mappedMetadataValues = $this->getMetaMetadataImportMapper()->mapMetaMetadataToProperties( |
| 334 | $metaMetadataValues, |
| 335 | $itemClass |
| 336 | ); |
| 337 | $metadataValues = $this->getMetadataImporter()->extract($domManifest); |
| 338 | $notMatchingProperties = $this->checkMissingClassProperties( |
| 339 | $metadataValues, |
| 340 | $mappedMetadataValues['itemProperties'] |
| 341 | ); |
| 342 | if (!empty($notMatchingProperties)) { |
| 343 | return Report::createError( |
| 344 | sprintf( |
| 345 | __('Target class is missing the following metadata properties: %s'), |
| 346 | implode(', ', $notMatchingProperties) |
| 347 | ) |
| 348 | ); |
| 349 | } |
| 350 | if (empty($mappedMetadataValues)) { |
| 351 | $mappedMetadataValues = $this->getMetaMetadataImportMapper()->mapMetadataToProperties( |
| 352 | $metadataValues, |
| 353 | $itemClass |
| 354 | ); |
| 355 | } |
| 356 | } |
| 357 | |
| 358 | $sharedFiles = []; |
| 359 | $createdClasses = []; |
| 360 | foreach ($qtiItemResources as $qtiItemResource) { |
| 361 | $itemCount++; |
| 362 | $itemReport = $this->importQtiItem( |
| 363 | $folder, |
| 364 | $qtiItemResource, |
| 365 | $itemClass, |
| 366 | $sharedFiles, |
| 367 | [], |
| 368 | $metadataValues ?? [], |
| 369 | $createdClasses, |
| 370 | $enableMetadataGuardians, |
| 371 | $enableMetadataValidators, |
| 372 | $itemMustExist, |
| 373 | $itemMustBeOverwritten, |
| 374 | $overwrittenItems, |
| 375 | isset($mappedMetadataValues['itemProperties']) ? $mappedMetadataValues['itemProperties'] : [], |
| 376 | $importMetadataEnabled |
| 377 | ); |
| 378 | |
| 379 | $allCreatedClasses = array_merge($allCreatedClasses, $createdClasses); |
| 380 | |
| 381 | $rdfItem = $itemReport->getData(); |
| 382 | |
| 383 | if ($rdfItem) { |
| 384 | $successItems[$qtiItemResource->getIdentifier()] = $rdfItem; |
| 385 | } |
| 386 | |
| 387 | $report->add($itemReport); |
| 388 | } |
| 389 | } catch (ValidationException $ve) { |
| 390 | $validationReport = Report::createFailure("The IMS Manifest file could not be validated"); |
| 391 | $validationReport->add($ve->getReport()); |
| 392 | $report->setMessage(__("No Items could be imported from the given IMS QTI package.")); |
| 393 | $report->setType(Report::TYPE_ERROR); |
| 394 | $report->add($validationReport); |
| 395 | } catch (common_exception_UserReadableException $e) { |
| 396 | $report = new Report(Report::TYPE_ERROR, $e->getUserMessage()); |
| 397 | $report->add($e); |
| 398 | } |
| 399 | |
| 400 | if (!empty($successItems)) { |
| 401 | // Some items were imported from the package. |
| 402 | $report->setMessage( |
| 403 | __('%d Item(s) of %d imported from the given IMS QTI Package.', count($successItems), $itemCount) |
| 404 | ); |
| 405 | |
| 406 | if (count($successItems) !== $itemCount) { |
| 407 | $report->setType(Report::TYPE_WARNING); |
| 408 | } |
| 409 | } else { |
| 410 | $report->setMessage(__('No Items could be imported from the given IMS QTI package.')); |
| 411 | $report->setType(Report::TYPE_ERROR); |
| 412 | } |
| 413 | |
| 414 | if ($rollbackOnError === true) { |
| 415 | if ( |
| 416 | $report->getType() === Report::TYPE_ERROR || $report->contains( |
| 417 | Report::TYPE_ERROR |
| 418 | ) |
| 419 | ) { |
| 420 | $this->rollback($successItems, $report, $allCreatedClasses, $overwrittenItems); |
| 421 | } |
| 422 | } elseif ($rollbackOnWarning === true) { |
| 423 | if ($report->contains(Report::TYPE_WARNING)) { |
| 424 | $this->rollback($successItems, $report, $allCreatedClasses, $overwrittenItems); |
| 425 | } |
| 426 | } |
| 427 | |
| 428 | // cleanup |
| 429 | tao_helpers_File::delTree($folder); |
| 430 | |
| 431 | return $report; |
| 432 | } |
| 433 | |
| 434 | /** |
| 435 | * Log events when items lock released after the configured item import ttl |
| 436 | * It is possible that somehow 2 item import processes were run at once, |
| 437 | * in this case we can get a situation when process 1 started import of the |
| 438 | * item with ID item1, then process 2 started import of the same item item1, |
| 439 | * process 2 saw that item with this ID already exists and pass information that |
| 440 | * item exists, but as we know item import is in progress and if someone try to |
| 441 | * work with items files he will see an error that files (any resources of the item) |
| 442 | * are not found and show error |
| 443 | * @param float $startImportTime |
| 444 | * @param string $itemId |
| 445 | */ |
| 446 | private function checkImportLockTime(float $startImportTime, string $itemId = ''): void |
| 447 | { |
| 448 | $timeElapsedSecs = microtime(true) - $startImportTime; |
| 449 | if ($timeElapsedSecs > $this->getOption(self::OPTION_IMPORT_LOCK_TTL)) { |
| 450 | common_Logger::w('Items lock was released before item ' . $itemId . ' import finished.'); |
| 451 | } |
| 452 | } |
| 453 | |
| 454 | /** |
| 455 | * @param $tmpFolder |
| 456 | * @param \oat\taoQtiItem\model\qti\Resource $qtiItemResource |
| 457 | * @param $itemClass |
| 458 | * @param array $sharedFiles |
| 459 | * @param array $dependencies |
| 460 | * @param array $metadataValues |
| 461 | * @param array $createdClasses |
| 462 | * @param boolean $enableMetadataGuardians |
| 463 | * @param boolean $enableMetadataValidators |
| 464 | * @param bool $itemMustExist |
| 465 | * @param bool $itemMustBeOverwritten |
| 466 | * @param array $overwrittenItems |
| 467 | * @return Report |
| 468 | * @throws common_exception_Error |
| 469 | */ |
| 470 | public function importQtiItem( |
| 471 | $tmpFolder, |
| 472 | Resource $qtiItemResource, |
| 473 | $itemClass, |
| 474 | array &$sharedFiles, |
| 475 | array $dependencies = [], |
| 476 | array $metadataValues = [], |
| 477 | &$createdClasses = [], |
| 478 | $enableMetadataGuardians = true, |
| 479 | $enableMetadataValidators = true, |
| 480 | $itemMustExist = false, |
| 481 | $itemMustBeOverwritten = false, |
| 482 | &$overwrittenItems = [], |
| 483 | $metaMedataValues = [], |
| 484 | $importMetadataEnabled = false |
| 485 | ) { |
| 486 | // if report can't be finished |
| 487 | $report = Report::createError( |
| 488 | __('IMS QTI Item referenced as "%s" cannot be imported.', $qtiItemResource->getIdentifier()) |
| 489 | ); |
| 490 | |
| 491 | $startImportTime = microtime(true); |
| 492 | |
| 493 | $lock = $this->createLock( |
| 494 | __CLASS__ . '/' . __METHOD__ . '/' . $qtiItemResource->getIdentifier(), |
| 495 | $this->getOption(self::OPTION_IMPORT_LOCK_TTL) |
| 496 | ); |
| 497 | $lock->acquire(true); |
| 498 | try { |
| 499 | $qtiService = Service::singleton(); |
| 500 | $overWriting = false; |
| 501 | |
| 502 | //load the information about resources in the manifest |
| 503 | try { |
| 504 | $resourceIdentifier = $qtiItemResource->getIdentifier(); |
| 505 | $guardian = false; |
| 506 | |
| 507 | if ($enableMetadataGuardians === true) { |
| 508 | $guardian = $this->getMetadataImporter()->guard($resourceIdentifier); |
| 509 | if ($guardian !== false) { |
| 510 | // Item found by guardians. |
| 511 | if ($itemMustBeOverwritten === true) { |
| 512 | \common_Logger::i( |
| 513 | 'Resource "' . $resourceIdentifier |
| 514 | . '" is already stored in the database and will be overwritten.' |
| 515 | ); |
| 516 | $overWriting = true; |
| 517 | } else { |
| 518 | \common_Logger::i( |
| 519 | 'Resource "' . $resourceIdentifier |
| 520 | . '" is already stored in the database and will not be imported.' |
| 521 | ); |
| 522 | |
| 523 | return Report::createInfo( |
| 524 | // phpcs:disable Generic.Files.LineLength |
| 525 | __('The IMS QTI Item referenced as "%s" in the IMS Manifest file was already stored in the Item Bank.', $resourceIdentifier), |
| 526 | // phpcs:enable Generic.Files.LineLength |
| 527 | new MetadataGuardianResource($guardian) |
| 528 | ); |
| 529 | } |
| 530 | } elseif ($itemMustExist === true) { // Item not found by guardians. |
| 531 | \common_Logger::i( |
| 532 | 'Resource "' . $resourceIdentifier |
| 533 | . '" must be already stored in the database in order to proceed.' |
| 534 | ); |
| 535 | |
| 536 | return new Report( |
| 537 | Report::TYPE_ERROR, |
| 538 | // phpcs:disable Generic.Files.LineLength |
| 539 | __('The IMS QTI Item referenced as "%s" in the IMS Manifest file should have been found the Item Bank. Item not found.', $resourceIdentifier) |
| 540 | // phpcs:enable Generic.Files.LineLength |
| 541 | ); |
| 542 | } |
| 543 | } |
| 544 | |
| 545 | if ($enableMetadataValidators === true) { |
| 546 | $validationReport = $this->getMetadataImporter()->validate($resourceIdentifier); |
| 547 | |
| 548 | if ($validationReport->getType() !== Report::TYPE_SUCCESS) { |
| 549 | $validationReport->setMessage( |
| 550 | // phpcs:disable Generic.Files.LineLength |
| 551 | __('Item metadata with identifier "%s" is not valid: ', $resourceIdentifier) . $validationReport->getMessage() |
| 552 | // phpcs:enable Generic.Files.LineLength |
| 553 | ); |
| 554 | \common_Logger::i('Item metadata is not valid: ' . $validationReport->getMessage()); |
| 555 | |
| 556 | return $validationReport; |
| 557 | } |
| 558 | } |
| 559 | |
| 560 | $targetClass = $this->getMetadataImporter()->classLookUp($resourceIdentifier, $createdClasses); |
| 561 | $tmpQtiFile = $tmpFolder . helpers_File::urlToPath($qtiItemResource->getFile()); |
| 562 | common_Logger::i('file :: ' . $qtiItemResource->getFile()); |
| 563 | $this->convertToQti2($tmpQtiFile); |
| 564 | $qtiModel = $this->createQtiItemModel($tmpQtiFile); |
| 565 | |
| 566 | if ( |
| 567 | $this->getOption(self::CONFIG_VALIDATE_RESPONSE_PROCESSING) && !$this->validResponseProcessing( |
| 568 | $qtiModel |
| 569 | ) |
| 570 | ) { |
| 571 | return Report::createError( |
| 572 | // phpcs:disable Generic.Files.LineLength |
| 573 | __('The IMS QTI Item referenced as "%s" in the IMS Manifest file has incorrect Response Processing and outcomeDeclaration definitions.', $resourceIdentifier) |
| 574 | // phpcs:enable Generic.Files.LineLength |
| 575 | ); |
| 576 | } |
| 577 | |
| 578 | if ($guardian !== false && $itemMustBeOverwritten) { |
| 579 | \common_Logger::d( |
| 580 | 'Resource "' . $resourceIdentifier . '" will overwrite item with URI ' . $guardian->getUri() |
| 581 | ); |
| 582 | $rdfItem = $guardian; |
| 583 | $overwrittenItems[$guardian->getUri()] = $qtiService->backupContentByRdfItem($rdfItem); |
| 584 | $qtiService->saveDataItemToRdfItem($qtiModel, $rdfItem); |
| 585 | } else { |
| 586 | $rdfItem = $this->createRdfItem((($targetClass !== false) ? $targetClass : $itemClass), $qtiModel); |
| 587 | } |
| 588 | |
| 589 | // Setting qtiIdentifier property |
| 590 | $qtiIdentifierProperty = new \core_kernel_classes_Property(self::PROPERTY_QTI_ITEM_IDENTIFIER); |
| 591 | $rdfItem->editPropertyValues($qtiIdentifierProperty, $resourceIdentifier); |
| 592 | |
| 593 | $itemAssetManager = new AssetManager(); |
| 594 | $itemAssetManager->setItemContent($qtiModel->toXML()); |
| 595 | $itemAssetManager->setSource($tmpFolder); |
| 596 | |
| 597 | /** |
| 598 | * Load asset handler following priority handler defined by you |
| 599 | * The first applicable will be used to import assets |
| 600 | */ |
| 601 | |
| 602 | /** Portable element handler */ |
| 603 | $peHandler = new PortableAssetHandler($qtiModel, $tmpFolder, dirname($tmpQtiFile)); |
| 604 | $itemAssetManager->loadAssetHandler($peHandler); |
| 605 | |
| 606 | if ( |
| 607 | $this |
| 608 | ->getServiceLocator() |
| 609 | ->get(\common_ext_ExtensionsManager::SERVICE_ID) |
| 610 | ->isInstalled('taoMediaManager') |
| 611 | ) { |
| 612 | $mediaClassPath = $this->getTargetClassForAssets($itemClass, $rdfItem); |
| 613 | /** Shared stimulus handler */ |
| 614 | $sharedStimulusHandler = new SharedStimulusAssetHandler(); |
| 615 | $sharedStimulusHandler->setServiceLocator($this->getServiceLocator()); |
| 616 | $sharedStimulusHandler |
| 617 | ->setQtiModel($qtiModel) |
| 618 | ->setItemSource(new ItemMediaResolver($rdfItem, '')) |
| 619 | ->setSharedFiles($sharedFiles) |
| 620 | ->setTargetClassPath($mediaClassPath); |
| 621 | $itemAssetManager->loadAssetHandler($sharedStimulusHandler); |
| 622 | } else { |
| 623 | $handler = new StimulusHandler(); |
| 624 | $handler->setQtiItem($qtiModel); |
| 625 | $handler->setItemSource(new LocalItemSource(['item' => $rdfItem])); |
| 626 | $itemAssetManager->loadAssetHandler($handler); |
| 627 | } |
| 628 | |
| 629 | /** Local storage handler */ |
| 630 | $localHandler = new LocalAssetHandler(); |
| 631 | $localHandler->setItemSource(new LocalItemSource(['item' => $rdfItem])); |
| 632 | $itemAssetManager->loadAssetHandler($localHandler); |
| 633 | |
| 634 | /** Copy external files to the item directory (preparation before import) */ |
| 635 | $itemAssetManager->copyDependencyFiles($qtiItemResource, $dependencies); |
| 636 | |
| 637 | $itemAssetManager |
| 638 | ->importAuxiliaryFiles($qtiItemResource) |
| 639 | ->importDependencyFiles($qtiItemResource, $dependencies); |
| 640 | |
| 641 | $itemAssetManager->finalize(); |
| 642 | |
| 643 | if (isset($sharedStimulusHandler) && $sharedStimulusHandler instanceof SharedStimulusAssetHandler) { |
| 644 | $sharedFiles = $sharedStimulusHandler->getSharedFiles(); |
| 645 | } |
| 646 | |
| 647 | $qtiModel = $this->createQtiItemModel($itemAssetManager->getItemContent(), false); |
| 648 | $qtiService->saveDataItemToRdfItem($qtiModel, $rdfItem); |
| 649 | |
| 650 | if ($importMetadataEnabled && isset($metadataValues[$resourceIdentifier])) { |
| 651 | $this->getMappedMetadataInjector()->inject( |
| 652 | $metaMedataValues, |
| 653 | $metadataValues[$resourceIdentifier], |
| 654 | $rdfItem |
| 655 | ); |
| 656 | } |
| 657 | |
| 658 | |
| 659 | $eventManager = ServiceManager::getServiceManager()->get(EventManager::CONFIG_ID); |
| 660 | $eventManager->trigger(new ItemImported($rdfItem, $qtiModel)); |
| 661 | |
| 662 | // Build report message. |
| 663 | if ($guardian !== false) { |
| 664 | // phpcs:disable Generic.Files.LineLength |
| 665 | $msg = __('The IMS QTI Item referenced as "%s" in the IMS Manifest file was successfully overwritten.', $qtiItemResource->getIdentifier()); |
| 666 | // phpcs:enable Generic.Files.LineLength |
| 667 | } else { |
| 668 | // phpcs:disable Generic.Files.LineLength |
| 669 | $msg = __('The IMS QTI Item referenced as "%s" in the IMS Manifest file was successfully imported.', $qtiItemResource->getIdentifier()); |
| 670 | // phpcs:enable Generic.Files.LineLength |
| 671 | } |
| 672 | |
| 673 | $this->getItemEventDispatcher()->dispatch($qtiModel, $rdfItem); |
| 674 | |
| 675 | $report = Report::createSuccess($msg, $rdfItem); |
| 676 | } catch (ParsingException $e) { |
| 677 | $message = __('Resource "' . $resourceIdentifier . 'has an error. ') . $e->getUserMessage(); |
| 678 | |
| 679 | $report = new Report(Report::TYPE_ERROR, $message); |
| 680 | } catch (ValidationException $ve) { |
| 681 | $report = Report::createFailure( |
| 682 | // phpcs:disable Generic.Files.LineLength |
| 683 | __('IMS QTI Item referenced as "%s" in the IMS Manifest file could not be imported.', $resourceIdentifier) |
| 684 | // phpcs:enable Generic.Files.LineLength |
| 685 | ); |
| 686 | $report->add($ve->getReport()); |
| 687 | } catch (XmlStorageException $e) { |
| 688 | $files = []; |
| 689 | $message = __('There are errors in the following shared stimulus : ') . PHP_EOL; |
| 690 | /** @var \LibXMLError $error */ |
| 691 | foreach ($e->getErrors() as $error) { |
| 692 | if (!in_array($error->file, $files)) { |
| 693 | $files[] = $error->file; |
| 694 | $message .= '- ' . basename($error->file) . ' :' . PHP_EOL; |
| 695 | } |
| 696 | $message .= $error->message . ' at line : ' . $error->line . PHP_EOL; |
| 697 | } |
| 698 | $message .= __(' For Resource "' . $resourceIdentifier); |
| 699 | |
| 700 | $report = new Report( |
| 701 | Report::TYPE_ERROR, |
| 702 | $message |
| 703 | ); |
| 704 | } catch (PortableElementInvalidModelException $pe) { |
| 705 | $report = Report::createFailure( |
| 706 | // phpcs:disable Generic.Files.LineLength |
| 707 | __('IMS QTI Item referenced as "%s" contains a portable element and cannot be imported.', $resourceIdentifier) |
| 708 | // phpcs:enable Generic.Files.LineLength |
| 709 | ); |
| 710 | $report->add($pe->getReport()); |
| 711 | if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) { |
| 712 | $rdfItem->delete(); |
| 713 | } |
| 714 | } catch (PortableElementException $e) { |
| 715 | // an error occurred during a specific item |
| 716 | if ($e instanceof common_exception_UserReadableException) { |
| 717 | $msg = __('Error on item %1$s : %2$s', $resourceIdentifier, $e->getUserMessage()); |
| 718 | } else { |
| 719 | $msg = __('Error on item %s', $resourceIdentifier); |
| 720 | common_Logger::d($e->getMessage()); |
| 721 | } |
| 722 | $report = new Report(Report::TYPE_ERROR, $msg); |
| 723 | if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) { |
| 724 | $rdfItem->delete(); |
| 725 | } |
| 726 | } catch (TemplateException $e) { |
| 727 | $report = new Report( |
| 728 | Report::TYPE_ERROR, |
| 729 | // phpcs:disable Generic.Files.LineLength |
| 730 | __('The IMS QTI Item referenced as "%s" in the IMS Manifest file failed: %s', $resourceIdentifier, $e->getMessage()) |
| 731 | // phpcs:enable Generic.Files.LineLength |
| 732 | ); |
| 733 | if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) { |
| 734 | $rdfItem->delete(); |
| 735 | } |
| 736 | } catch (MetaMetadataException $e) { |
| 737 | $error = Reporter::createError( |
| 738 | sprintf('Import failed at validating metametadata with message: "%s"', $e->getMessage()) |
| 739 | ); |
| 740 | $report->add($error); |
| 741 | common_Logger::e($e->getMessage()); |
| 742 | if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) { |
| 743 | $rdfItem->delete(); |
| 744 | } |
| 745 | } catch (Exception $e) { |
| 746 | // an error occurred during a specific item |
| 747 | $report = new Report( |
| 748 | Report::TYPE_ERROR, |
| 749 | // phpcs:disable Generic.Files.LineLength |
| 750 | __("An unknown error occured while importing the IMS QTI Package with identifier: " . $resourceIdentifier) |
| 751 | // phpcs:enable Generic.Files.LineLength |
| 752 | ); |
| 753 | if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) { |
| 754 | $rdfItem->delete(); |
| 755 | } |
| 756 | common_Logger::e($e->getMessage()); |
| 757 | } |
| 758 | } catch (ValidationException $ve) { |
| 759 | $validationReport = Report::createFailure("The IMS Manifest file could not be validated"); |
| 760 | $validationReport->add($ve->getReport()); |
| 761 | $report->setMessage(__("No Items could be imported from the given IMS QTI package.")); |
| 762 | $report->setType(Report::TYPE_ERROR); |
| 763 | $report->add($validationReport); |
| 764 | } catch (common_exception_UserReadableException $e) { |
| 765 | $report = new Report(Report::TYPE_ERROR, __($e->getUserMessage())); |
| 766 | $report->add($e); |
| 767 | } finally { |
| 768 | $this->checkImportLockTime($startImportTime, $qtiItemResource->getIdentifier()); |
| 769 | $lock->release(); |
| 770 | } |
| 771 | |
| 772 | return $report; |
| 773 | } |
| 774 | |
| 775 | protected function validResponseProcessing(Item $qtiModel) |
| 776 | { |
| 777 | // <outcomeDeclaration> from the items qti |
| 778 | $outcomes = $this->getOutcomesIds($qtiModel); |
| 779 | // <setOutcomeValue> from the responseProcessing (template or body) also items qti |
| 780 | $rules = $this->getSetOutcomeValueIds($qtiModel); |
| 781 | |
| 782 | return count(array_diff($rules, $outcomes)) === 0; |
| 783 | } |
| 784 | |
| 785 | protected function getOutcomesIds(Item $qtiModel) |
| 786 | { |
| 787 | $declaredIds = []; |
| 788 | /** @var OutcomeDeclaration $outcomeDeclaration */ |
| 789 | foreach ($qtiModel->getOutcomes() as $outcomeDeclaration) { |
| 790 | $declaredIds[] = $outcomeDeclaration->getIdentifier(); |
| 791 | } |
| 792 | |
| 793 | return $declaredIds; |
| 794 | } |
| 795 | |
| 796 | protected function getSetOutcomeValueIds($qtiModel) |
| 797 | { |
| 798 | $rules = $this->getResponseProcessingRules($qtiModel); |
| 799 | $ids = []; |
| 800 | foreach ($rules as $rule) { |
| 801 | /** @var QtiComponentCollection $collection */ |
| 802 | $collection = $rule->getComponentsByClassName(SetOutcomeValue::CLASS_NAME, true); |
| 803 | while ($collection->valid()) { |
| 804 | /** @var SetOutcomeValue $setOutcomeValue */ |
| 805 | $setOutcomeValue = $collection->current(); |
| 806 | $ids[] = $setOutcomeValue->getIdentifier(); |
| 807 | $collection->next(); |
| 808 | } |
| 809 | } |
| 810 | |
| 811 | return array_unique($ids); |
| 812 | } |
| 813 | |
| 814 | protected function getResponseProcessingRules(Item $qtiModel) |
| 815 | { |
| 816 | $rules = []; |
| 817 | $qti = $qtiModel->toQTI(); |
| 818 | $qtiXmlDoc = new XmlDocument(); |
| 819 | $qtiXmlDoc->loadFromString($qti); |
| 820 | $itemSession = new AssessmentItemSession($qtiXmlDoc->getDocumentComponent(), new SessionManager()); |
| 821 | $responseProcessing = $itemSession->getAssessmentItem()->getResponseProcessing(); |
| 822 | |
| 823 | // Some items (especially to collect information) have no response processing! |
| 824 | if ( |
| 825 | $responseProcessing !== null && ($responseProcessing->hasTemplate( |
| 826 | ) === true || $responseProcessing->hasTemplateLocation() === true || count( |
| 827 | $responseProcessing->getResponseRules() |
| 828 | ) > 0) |
| 829 | ) { |
| 830 | $engine = new ResponseProcessingEngine($responseProcessing, $itemSession); |
| 831 | $rules = $engine->getResponseProcessingRules(); |
| 832 | } |
| 833 | |
| 834 | return $rules; |
| 835 | } |
| 836 | |
| 837 | /** |
| 838 | * Import metadata to a given QTI Item. |
| 839 | * |
| 840 | * @param MetadataValue[] $metadataValues An array of MetadataValue objects. |
| 841 | * @param Resource $qtiResource The object representing the QTI Resource, from an IMS Manifest perspective. |
| 842 | * @param core_kernel_classes_Resource $resource The object representing the target QTI Item in the Ontology. |
| 843 | * @param MetadataInjector[] $ontologyInjectors Implementations of MetadataInjector that will take care to inject |
| 844 | * the metadata values in the appropriate Ontology Resource Properties. |
| 845 | * @throws MetadataInjectionException If an error occurs while importing the metadata. |
| 846 | * @deprecated use MetadataService::getImporter::inject() |
| 847 | * |
| 848 | */ |
| 849 | public function importResourceMetadata( |
| 850 | array $metadataValues, |
| 851 | Resource $qtiResource, |
| 852 | core_kernel_classes_Resource $resource, |
| 853 | array $ontologyInjectors = [] |
| 854 | ) { |
| 855 | // Filter metadata values for this given item. |
| 856 | $identifier = $qtiResource->getIdentifier(); |
| 857 | if (isset($metadataValues[$identifier]) === true) { |
| 858 | \common_Logger::i("Preparing Metadata Values for resource '${identifier}'..."); |
| 859 | $values = $metadataValues[$identifier]; |
| 860 | |
| 861 | foreach ($ontologyInjectors as $injector) { |
| 862 | $valuesCount = count($values); |
| 863 | $injectorClass = get_class($injector); |
| 864 | \common_Logger::i( |
| 865 | "Attempting to inject ${valuesCount} Metadata Values in database for resource " |
| 866 | . "'${identifier}' with Metadata Injector '${injectorClass}'." |
| 867 | ); |
| 868 | $injector->inject($resource, [$identifier => $values]); |
| 869 | } |
| 870 | } |
| 871 | } |
| 872 | |
| 873 | /** |
| 874 | * @param array $items |
| 875 | * @param Report $report |
| 876 | * @param array $createdClasses (optional) |
| 877 | * @throws common_exception_Error |
| 878 | */ |
| 879 | protected function rollback( |
| 880 | array $items, |
| 881 | Report $report, |
| 882 | array $createdClasses = [], |
| 883 | array $overwrittenItems = [] |
| 884 | ) { |
| 885 | $overwrittenItemsIds = array_keys($overwrittenItems); |
| 886 | $qtiService = Service::singleton(); |
| 887 | |
| 888 | // 1. Simply delete items that were not involved in overwriting. |
| 889 | foreach ($items as $id => $item) { |
| 890 | if (!$item instanceof MetadataGuardianResource && !in_array($item->getUri(), $overwrittenItemsIds)) { |
| 891 | \common_Logger::d("Deleting item '${id}'..."); |
| 892 | @taoItems_models_classes_ItemsService::singleton()->deleteResource($item); |
| 893 | |
| 894 | $report->add( |
| 895 | new Report( |
| 896 | Report::TYPE_WARNING, |
| 897 | __('The IMS QTI Item referenced as "%s" in the IMS Manifest was successfully rolled back.', $id) |
| 898 | ) |
| 899 | ); |
| 900 | } |
| 901 | } |
| 902 | |
| 903 | // 2. Restore overwritten item contents. |
| 904 | foreach ($overwrittenItems as $overwrittenItemId => $backupName) { |
| 905 | common_Logger::d("Restoring content for item '${overwrittenItemId}'..."); |
| 906 | @$qtiService->restoreContentByRdfItem(new core_kernel_classes_Resource($overwrittenItemId), $backupName); |
| 907 | } |
| 908 | |
| 909 | foreach ($createdClasses as $createdClass) { |
| 910 | @$createdClass->delete(); |
| 911 | } |
| 912 | } |
| 913 | |
| 914 | /** |
| 915 | * Get the lom metadata importer |
| 916 | * |
| 917 | * @return MetadataImporter |
| 918 | */ |
| 919 | protected function getMetadataImporter() |
| 920 | { |
| 921 | if (!$this->metadataImporter) { |
| 922 | $this->metadataImporter = $this->getServiceLocator()->get(MetadataService::SERVICE_ID)->getImporter(); |
| 923 | } |
| 924 | return $this->metadataImporter; |
| 925 | } |
| 926 | |
| 927 | protected function getMetaMetadataExtractor(): MetaMetadataExtractor |
| 928 | { |
| 929 | return $this->getServiceManager()->getContainer()->get(MetaMetadataExtractor::class); |
| 930 | } |
| 931 | |
| 932 | /** |
| 933 | * Retrieve the labels of all parent classes up to base item class. |
| 934 | */ |
| 935 | public function getTargetClassForAssets( |
| 936 | core_kernel_classes_Class $itemClass, |
| 937 | core_kernel_classes_Resource $itemResource |
| 938 | ): array { |
| 939 | // Collecting labels path from item root to the class where the item resource is stored |
| 940 | $labels = []; |
| 941 | while ($itemClass->getUri() !== TaoOntology::CLASS_URI_ITEM) { |
| 942 | $labels [] = $itemClass->getLabel(); |
| 943 | $parentClasses = $itemClass->getParentClasses(); |
| 944 | $itemClass = reset($parentClasses); |
| 945 | } |
| 946 | |
| 947 | $path = array_reverse($labels); |
| 948 | |
| 949 | // Adding item's label as the leaf class. |
| 950 | $path[] = $itemResource->getLabel(); |
| 951 | |
| 952 | return $path; |
| 953 | } |
| 954 | |
| 955 | private function getItemEventDispatcher(): UpdatedItemEventDispatcher |
| 956 | { |
| 957 | return $this->getServiceLocator()->get(UpdatedItemEventDispatcher::class); |
| 958 | } |
| 959 | |
| 960 | private function getMappedMetadataInjector(): MappedMetadataInjector |
| 961 | { |
| 962 | return $this->getServiceManager()->getContainer()->get(MappedMetadataInjector::class); |
| 963 | } |
| 964 | |
| 965 | private function getMetaMetadataImportMapper(): MetaMetadataImportMapper |
| 966 | { |
| 967 | return $this->getServiceManager()->getContainer()->get(MetaMetadataImportMapper::class); |
| 968 | } |
| 969 | |
| 970 | private function getUniqueNumericQtiIdentifierReplacer(): UniqueNumericQtiIdentifierReplacer |
| 971 | { |
| 972 | return $this->getServiceManager()->getContainer()->get(UniqueNumericQtiIdentifierReplacer::class); |
| 973 | } |
| 974 | |
| 975 | private function replaceUniqueNumericQtiIdentifier(string $qtiXml): string |
| 976 | { |
| 977 | return $this->getUniqueNumericQtiIdentifierReplacer()->replace($qtiXml); |
| 978 | } |
| 979 | |
| 980 | private function convertToQti2(string $tmpQtiFile): void |
| 981 | { |
| 982 | $this->getItemConverter()->convertToQti2($tmpQtiFile); |
| 983 | } |
| 984 | |
| 985 | private function getItemConverter(): ItemConverter |
| 986 | { |
| 987 | return $this->getServiceManager()->getContainer()->get(ItemConverter::class); |
| 988 | } |
| 989 | |
| 990 | /** |
| 991 | * Checks if target class has all the properties needed to import the metadata. |
| 992 | * @param array $metadataValues |
| 993 | * @param $itemProperties |
| 994 | * @return array |
| 995 | */ |
| 996 | private function checkMissingClassProperties(array $metadataValues, $itemProperties): array |
| 997 | { |
| 998 | $metadataValueUris = $this->getMetadataImporter()->metadataValueUris($metadataValues); |
| 999 | return array_diff( |
| 1000 | $metadataValueUris, |
| 1001 | array_keys($itemProperties) |
| 1002 | ); |
| 1003 | } |
| 1004 | } |