Skip import of none mappable objects (#100)

The schema is very flexible and some values are not validated upfront.
This will result in many objects which we currently can not map.
This resulted in an exception breaking the rest of the import.
We now handle the broken mapping and skip those objects with proper
logging.
This allows to continue with import and report and debug those objects
in order to improve the mapping step by step.

Relates: #10198
This commit is contained in:
Daniel Siepmann 2022-12-15 12:41:35 +01:00 committed by GitHub
parent 61ae59c127
commit 723ea3b512
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 743 additions and 124 deletions

View file

@ -48,4 +48,9 @@ class MappingException extends \Exception
$this->jsonLD = $jsonLD;
$this->targetClassName = $targetClassName;
}
public function getUrl(): string
{
return $this->jsonLD['@id'] ?? 'unknown';
}
}

View file

@ -27,6 +27,7 @@ use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Log\Logger;
use WerkraumMedia\ThueCat\Domain\Import\EntityMapper\EntityRegistry;
use WerkraumMedia\ThueCat\Domain\Import\EntityMapper\JsonDecode;
use WerkraumMedia\ThueCat\Domain\Import\EntityMapper\MappingException;
use WerkraumMedia\ThueCat\Domain\Import\Entity\MapsToType;
use WerkraumMedia\ThueCat\Domain\Import\Importer\Converter;
use WerkraumMedia\ThueCat\Domain\Import\Importer\FetchData;
@ -36,6 +37,7 @@ use WerkraumMedia\ThueCat\Domain\Import\Model\EntityCollection;
use WerkraumMedia\ThueCat\Domain\Import\UrlProvider\Registry as UrlProviderRegistry;
use WerkraumMedia\ThueCat\Domain\Import\UrlProvider\UrlProvider;
use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLog;
use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\MappingError;
use WerkraumMedia\ThueCat\Domain\Repository\Backend\ImportLogRepository;
class Importer
@ -180,17 +182,29 @@ class Importer
foreach ($this->languages->getAvailable($this->import->getConfiguration()) as $language) {
$this->logger->info('Process entity for language.', ['language' => $language, 'targetEntity' => $targetEntity]);
$mappedEntity = $this->entityMapper->mapDataToEntity(
$jsonEntity,
$targetEntity,
[
JsonDecode::ACTIVE_LANGUAGE => $language,
]
);
if (!$mappedEntity instanceof MapsToType) {
$this->logger->alert('Mapping did not result in an MapsToType instance.', ['class' => get_class($mappedEntity)]);
try {
$mappedEntity = $this->entityMapper->mapDataToEntity(
$jsonEntity,
$targetEntity,
[
JsonDecode::ACTIVE_LANGUAGE => $language,
]
);
} catch (MappingException $e) {
$this->logger->error('Could not map data to entity.', [
'url' => $e->getUrl(),
'language' => $language,
'mappingError' => $e->getMessage(),
]);
$this->import->getLog()->addEntry(new MappingError($e));
continue;
}
if (!$mappedEntity instanceof MapsToType) {
$this->logger->error('Mapping did not result in an MapsToType instance.', ['class' => get_class($mappedEntity)]);
continue;
}
$convertedEntity = $this->converter->convert(
$mappedEntity,
$this->import->getConfiguration(),
@ -198,7 +212,7 @@ class Importer
);
if ($convertedEntity === null) {
$this->logger->alert('Could not convert entity.', ['language' => $language, 'targetEntity' => $targetEntity]);
$this->logger->error('Could not convert entity.', ['language' => $language, 'targetEntity' => $targetEntity]);
continue;
}
$entities->add($convertedEntity);

View file

@ -28,7 +28,7 @@ use TYPO3\CMS\Core\DataHandling\DataHandler;
use WerkraumMedia\ThueCat\Domain\Import\Model\Entity;
use WerkraumMedia\ThueCat\Domain\Import\Model\EntityCollection;
use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLog;
use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry;
use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\SavingEntity;
class SaveData
{
@ -65,7 +65,7 @@ class SaveData
$this->updateEntities($entityCollection);
foreach ($entityCollection->getEntities() as $entity) {
$log->addEntry(new ImportLogEntry($entity, $this->errorLog));
$log->addEntry(new SavingEntity($entity, $this->errorLog));
}
}

View file

@ -23,8 +23,8 @@ declare(strict_types=1);
namespace WerkraumMedia\ThueCat\Domain\Import\Typo3Converter;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Log\Logger;
use TYPO3\CMS\Extbase\Persistence\QueryResultInterface;
use WerkraumMedia\ThueCat\Domain\Import\Entity\AccessibilitySpecification;
use WerkraumMedia\ThueCat\Domain\Import\Entity\Base;
@ -50,10 +50,8 @@ use WerkraumMedia\ThueCat\Domain\Repository\Backend\OrganisationRepository;
use WerkraumMedia\ThueCat\Domain\Repository\Backend\ParkingFacilityRepository;
use WerkraumMedia\ThueCat\Domain\Repository\Backend\TownRepository;
class GeneralConverter implements Converter, LoggerAwareInterface
class GeneralConverter implements Converter
{
use LoggerAwareTrait;
/**
* @var ResolveForeignReference
*/
@ -89,6 +87,11 @@ class GeneralConverter implements Converter, LoggerAwareInterface
*/
private $nameExtractor;
/**
* @var Logger
*/
private $logger;
/**
* @var ImportConfiguration
*/
@ -113,7 +116,8 @@ class GeneralConverter implements Converter, LoggerAwareInterface
OrganisationRepository $organisationRepository,
TownRepository $townRepository,
ParkingFacilityRepository $parkingFacilityRepository,
NameExtractor $nameExtractor
NameExtractor $nameExtractor,
LogManager $logManager
) {
$this->resolveForeignReference = $resolveForeignReference;
$this->importer = $importer;
@ -122,6 +126,7 @@ class GeneralConverter implements Converter, LoggerAwareInterface
$this->townRepository = $townRepository;
$this->parkingFacilityRepository = $parkingFacilityRepository;
$this->nameExtractor = $nameExtractor;
$this->logger = $logManager->getLogger(__CLASS__);
}
public function convert(
@ -165,14 +170,14 @@ class GeneralConverter implements Converter, LoggerAwareInterface
$tableName = $this->getTableNameByEntityClass(get_class($entity));
if (!$entity instanceof Minimum) {
$this->logger->debug('Skipped conversion of entity, got unexpected type', [
$this->logger->info('Skipped conversion of entity, got unexpected type', [
'expectedType' => Minimum::class,
'actualType' => get_class($entity),
]);
return false;
}
if ($entity->hasName() === false) {
$this->logger->debug('Skipped conversion of entity, had no name', [
$this->logger->info('Skipped conversion of entity, had no name', [
'remoteId' => $entity->getId(),
]);
return false;
@ -186,7 +191,7 @@ class GeneralConverter implements Converter, LoggerAwareInterface
$languageUid > 0
&& isset($GLOBALS['TCA'][$tableName]['ctrl']['languageField']) === false
) {
$this->logger->debug('Skipped conversion of entity, table does not support translations', [
$this->logger->info('Skipped conversion of entity, table does not support translations', [
'remoteId' => $entity->getId(),
'requestedLanguage' => $language,
'resolvedLanguageUid' => $languageUid,
@ -196,7 +201,7 @@ class GeneralConverter implements Converter, LoggerAwareInterface
}
if ($tableName !== 'tx_thuecat_organisation' && $this->getManagerUid($entity) === '') {
$this->logger->debug('Skipped conversion of entity, is not an organisation and no manager was available', [
$this->logger->info('Skipped conversion of entity, is not an organisation and no manager was available', [
'remoteId' => $entity->getId(),
]);
return false;

View file

@ -25,6 +25,7 @@ namespace WerkraumMedia\ThueCat\Domain\Model\Backend;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity as Typo3AbstractEntity;
use TYPO3\CMS\Extbase\Persistence\ObjectStorage;
use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\SavingEntity;
class ImportLog extends Typo3AbstractEntity
{
@ -88,7 +89,11 @@ class ImportLog extends Typo3AbstractEntity
foreach ($this->getEntries() as $entry) {
if ($entry->hasErrors()) {
$errors = array_merge($errors, $entry->getErrors());
$entryErrors = array_map(function (string $error) use ($entry) {
return 'Resource: ' . $entry->getRemoteId() . ' Error: ' . $error;
}, $entry->getErrors());
$errors = array_merge($errors, $entryErrors);
$errors = array_unique($errors);
}
}
@ -111,7 +116,7 @@ class ImportLog extends Typo3AbstractEntity
{
$summary = [];
foreach ($this->getEntries() as $entry) {
foreach ($this->getSavingEntries() as $entry) {
if (isset($summary[$entry->getRecordDatabaseTableName()])) {
++$summary[$entry->getRecordDatabaseTableName()];
continue;
@ -139,4 +144,14 @@ class ImportLog extends Typo3AbstractEntity
$this->addEntry($entry);
}
}
/**
* @return SavingEntity[]
*/
private function getSavingEntries(): array
{
return array_filter($this->logEntries->getArray(), function (ImportLogEntry $entry) {
return $entry instanceof SavingEntity;
});
}
}

View file

@ -23,90 +23,25 @@ declare(strict_types=1);
namespace WerkraumMedia\ThueCat\Domain\Model\Backend;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity as Typo3AbstractEntity;
use WerkraumMedia\ThueCat\Domain\Import\Model\Entity;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
class ImportLogEntry extends Typo3AbstractEntity
abstract class ImportLogEntry extends AbstractEntity
{
/**
* @var string
*/
protected $remoteId = '';
abstract public function getRemoteId(): string;
abstract public function getErrors(): array;
abstract public function hasErrors(): bool;
/**
* @var bool
* The type as defined within TCA.
*/
protected $insertion = false;
abstract public function getType(): string;
/**
* @var int
* Return an column -> value array used for insertion into the database.
* Only return special for the concrete instance, or empty array.
* Defaults inherited by this class are already handled.
*/
protected $recordUid = 0;
/**
* @var int
*/
protected $recordPid = 0;
/**
* @var string
*/
protected $tableName = '';
/**
* @var string
*/
protected $errors = '';
/**
* @var string[]
*/
protected $errorsAsArray = [];
public function __construct(
Entity $entity,
array $dataHandlerErrorLog
) {
$this->remoteId = $entity->getRemoteId();
$this->insertion = $entity->wasCreated();
$this->recordUid = $entity->getTypo3Uid();
$this->recordPid = $entity->getTypo3StoragePid();
$this->tableName = $entity->getTypo3DatabaseTableName();
$this->errorsAsArray = $dataHandlerErrorLog;
}
public function getRemoteId(): string
{
return $this->remoteId;
}
public function wasInsertion(): bool
{
return $this->insertion;
}
public function getRecordUid(): int
{
return $this->recordUid;
}
public function getRecordDatabaseTableName(): string
{
return $this->tableName;
}
public function getErrors(): array
{
if ($this->errorsAsArray === [] && $this->errors !== '') {
$this->errorsAsArray = json_decode($this->errors, true);
$this->errorsAsArray = array_unique($this->errorsAsArray);
}
return $this->errorsAsArray;
}
public function hasErrors(): bool
{
return $this->getErrors() !== [];
}
abstract public function getInsertion(): array;
}

View file

@ -0,0 +1,76 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry;
use WerkraumMedia\ThueCat\Domain\Import\EntityMapper\MappingException;
use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry;
class MappingError extends ImportLogEntry
{
/**
* @var string
*/
protected $remoteId = '';
/**
* @var string
*/
protected $errors = '';
public function __construct(
MappingException $exception
) {
$this->remoteId = $exception->getUrl();
$this->errors = json_encode([$exception->getMessage()]) ?: '';
}
public function getRemoteId(): string
{
return $this->remoteId;
}
public function getErrors(): array
{
$errors = json_decode($this->errors, true);
if (is_array($errors) === false) {
throw new \Exception('Could not parse errors.', 1671097690);
}
return $errors;
}
public function hasErrors(): bool
{
return true;
}
public function getType(): string
{
return 'mappingError';
}
public function getInsertion(): array
{
return [];
}
}

View file

@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/*
* Copyright (C) 2022 Daniel Siepmann <coding@daniel-siepmann.de>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301, USA.
*/
namespace WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry;
use WerkraumMedia\ThueCat\Domain\Import\Model\Entity;
use WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry;
class SavingEntity extends ImportLogEntry
{
/**
* @var string
*/
protected $remoteId = '';
/**
* @var bool
*/
protected $insertion = false;
/**
* @var int
*/
protected $recordUid = 0;
/**
* @var int
*/
protected $recordPid = 0;
/**
* @var string
*/
protected $tableName = '';
/**
* @var string
*/
protected $errors = '';
/**
* @var string[]
*/
protected $errorsAsArray = [];
public function __construct(
Entity $entity,
array $dataHandlerErrorLog
) {
$this->remoteId = $entity->getRemoteId();
$this->insertion = $entity->wasCreated();
$this->recordUid = $entity->getTypo3Uid();
$this->recordPid = $entity->getTypo3StoragePid();
$this->tableName = $entity->getTypo3DatabaseTableName();
$this->errorsAsArray = $dataHandlerErrorLog;
}
public function getRemoteId(): string
{
return $this->remoteId;
}
public function wasInsertion(): bool
{
return $this->insertion;
}
public function getRecordUid(): int
{
return $this->recordUid;
}
public function getRecordDatabaseTableName(): string
{
return $this->tableName;
}
public function getErrors(): array
{
if ($this->errorsAsArray === [] && $this->errors !== '') {
$errorsAsArray = json_decode($this->errors, true);
if (is_array($errorsAsArray) === false) {
throw new \Exception('Could not parse errors.', 1671097690);
}
$this->errorsAsArray = array_unique($errorsAsArray);
}
return $this->errorsAsArray;
}
public function hasErrors(): bool
{
return $this->getErrors() !== [];
}
public function getType(): string
{
return 'savingEntity';
}
public function getInsertion(): array
{
return [
'insertion' => (int) $this->wasInsertion(),
'record_uid' => $this->getRecordUid(),
'table_name' => $this->getRecordDatabaseTableName(),
];
}
}

View file

@ -77,14 +77,16 @@ class ImportLogRepository extends Repository
foreach ($log->getEntries() as $entry) {
$number++;
$entries['NEW' . $number] = [
'pid' => 0,
'import_log' => 'NEW0',
'insertion' => (int) $entry->wasInsertion(),
'record_uid' => $entry->getRecordUid(),
'table_name' => $entry->getRecordDatabaseTableName(),
'errors' => json_encode($entry->getErrors()),
];
$entries['NEW' . $number] = array_merge(
$entry->getInsertion(),
[
'pid' => 0,
'import_log' => 'NEW0',
'type' => $entry->getType(),
'remote_id' => $entry->getRemoteId(),
'errors' => json_encode($entry->getErrors()),
]
);
}
return $entries;

View file

@ -22,6 +22,18 @@ return [
],
\WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry::class => [
'tableName' => 'tx_thuecat_import_log_entry',
'subclasses' => [
'savingEntity' => \WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\SavingEntity::class,
'mappingError' => \WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\MappingError::class,
],
],
\WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\SavingEntity::class => [
'tableName' => 'tx_thuecat_import_log_entry',
'recordType' => 'savingEntity',
],
\WerkraumMedia\ThueCat\Domain\Model\Backend\ImportLogEntry\MappingError::class => [
'tableName' => 'tx_thuecat_import_log_entry',
'recordType' => 'mappingError',
],
\WerkraumMedia\ThueCat\Domain\Model\Frontend\TouristAttraction::class => [
'tableName' => 'tx_thuecat_tourist_attraction',

View file

@ -9,8 +9,10 @@ return (static function (string $extensionKey, string $tableName) {
return [
'ctrl' => [
'label' => 'crdate',
'label_alt' => 'configuration',
'label_alt_force' => true,
'iconfile' => \WerkraumMedia\ThueCat\Extension::getIconPath() . $tableName . '.svg',
'default_sortby' => 'crdate',
'default_sortby' => 'crdate desc',
'tstamp' => 'tstamp',
'crdate' => 'crdate',
'cruser_id' => 'cruser_id',
@ -51,7 +53,7 @@ return (static function (string $extensionKey, string $tableName) {
],
'types' => [
'0' => [
'showitem' => 'crdate, log_entries, configuration',
'showitem' => 'crdate, configuration, log_entries',
],
],
];

View file

@ -8,10 +8,11 @@ return (static function (string $extensionKey, string $tableName) {
return [
'ctrl' => [
'label' => 'table_name',
'label_alt' => 'record_uid',
'label' => 'type',
'label_alt' => 'remote_id, table_name, record_uid',
'label_alt_force' => true,
'iconfile' => \WerkraumMedia\ThueCat\Extension::getIconPath() . $tableName . '.svg',
'type' => 'type',
'default_sortby' => 'crdate',
'tstamp' => 'tstamp',
'crdate' => 'crdate',
@ -24,6 +25,30 @@ return (static function (string $extensionKey, string $tableName) {
'hideTable' => true,
],
'columns' => [
'type' => [
'label' => $languagePath . '.type',
'config' => [
'type' => 'select',
'renderType' => 'selectSingle',
'items' => [
[
$languagePath . '.type.savingEntity',
'savingEntity',
],
[
$languagePath . '.type.mappingError',
'mappingError',
],
],
],
],
'remote_id' => [
'label' => $languagePath . '.remote_id',
'config' => [
'type' => 'input',
'readOnly' => true,
],
],
'insertion' => [
'label' => $languagePath . '.insertion',
'config' => [
@ -80,9 +105,18 @@ return (static function (string $extensionKey, string $tableName) {
],
],
],
'palettes' => [
'always' => [
'label' => $languagePath . '.palette.always',
'showitem' => 'type, remote_id, import_log, crdate',
],
],
'types' => [
'0' => [
'showitem' => 'table_name, record_uid, insertion, errors, import_log, crdate',
'savingEntity' => [
'showitem' => '--palette--;;always, table_name, record_uid, insertion, errors',
],
'mappingError' => [
'showitem' => '--palette--;;always, errors',
],
],
];

View file

@ -17,6 +17,11 @@ Features
This allows to provide a single entity, e.g. a Town that has multiple ``schema:containsPlace`` entries.
Each of them will be imported.
* Import will no longer break on mapping issues.
Those will be logged and are available within the existing backend module.
This allows to skip some objects which can not be handled yet.
The log can be used to open issues. We then can improve the mapping.
* Import author of media. This allows to either render the license author or the author.
* Filter and sort opening hours.

View file

@ -265,6 +265,18 @@
<trans-unit id="tx_thuecat_import_log_entry" xml:space="preserve">
<source>Import Log Entry</source>
</trans-unit>
<trans-unit id="tx_thuecat_import_log_entry.type" xml:space="preserve">
<source>Type</source>
</trans-unit>
<trans-unit id="tx_thuecat_import_log_entry.type.savingEntity" xml:space="preserve">
<source>Saved Entity</source>
</trans-unit>
<trans-unit id="tx_thuecat_import_log_entry.type.mappingError" xml:space="preserve">
<source>Mapping Error</source>
</trans-unit>
<trans-unit id="tx_thuecat_import_log_entry.remote_id" xml:space="preserve">
<source>Remote ID (URL)</source>
</trans-unit>
<trans-unit id="tx_thuecat_import_log_entry.insertion" xml:space="preserve">
<source>Was inserted or updated</source>
</trans-unit>

View file

@ -0,0 +1,195 @@
{
"@context": {
"cdb": "https://thuecat.org/ontology/cdb/1.0/",
"dachkg": "https://thuecat.org/ontology/dachkg/1.0/",
"dbo": "http://dbpedia.org/ontology/",
"dsv": "http://ontologies.sti-innsbruck.at/dsv/",
"foaf": "http://xmlns.com/foaf/0.1/",
"owl": "http://www.w3.org/2002/07/owl#",
"rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
"schema": "http://schema.org/",
"sh": "http://www.w3.org/ns/shacl#",
"thuecat": "https://thuecat.org/ontology/thuecat/1.0/",
"ttgds": "https://thuecat.org/ontology/ttgds/1.0/",
"xsd": "http://www.w3.org/2001/XMLSchema#"
},
"@graph": [
{
"@id": "https://thuecat.org/resources/mapping-exception",
"@type": [
"schema:Place",
"schema:CivicStructure",
"schema:PlaceOfWorship",
"schema:Thing",
"schema:Museum",
"schema:TouristAttraction",
"schema:Synagogue",
"ttgds:PointOfInterest",
"thuecat:Building",
"thuecat:ReligiousBuilding",
"thuecat:CultureHistoricalMuseum"
],
"schema:address": {
"@id": "genid-28b33237f71b41e3ad54a99e1da769b9-b0",
"@type": [
"schema:Intangible",
"schema:PostalAddress",
"schema:StructuredValue",
"schema:Thing",
"schema:ContactPoint"
],
"schema:addressCountry": {
"@type": "thuecat:AddressCountry",
"@value": "thuecat:Germany"
},
"schema:addressLocality": {
"@language": "de",
"@value": "Erfurt"
},
"schema:addressRegion": {
"@type": "thuecat:AddressFederalState",
"@value": "thuecat:Thuringia"
},
"schema:email": {
"@language": "de",
"@value": "altesynagoge@erfurt.de"
},
"schema:faxNumber": {
"@language": "de",
"@value": "+49 361 6551669"
},
"schema:postalCode": {
"@language": "de",
"@value": "99084"
},
"schema:streetAddress": {
"@language": "de",
"@value": "Waagegasse 8"
},
"schema:telephone": {
"@language": "de",
"@value": "+49 361 6551520"
},
"thuecat:typOfAddress": {
"@type": "thuecat:TypOfAddress",
"@value": "thuecat:HouseAddress"
}
},
"schema:availableLanguage": [
{
"@type": "thuecat:Language",
"@value": "thuecat:German"
},
{
"@type": "thuecat:Language",
"@value": "thuecat:English"
},
{
"@type": "thuecat:Language",
"@value": "thuecat:French"
}
],
"schema:description": [
{
"@language": "fr",
"@value": "La vieille synagogue (datant des ann\u00e9es 1100) est la synagogue la plus vieille d\u2019Europe totalement conserv\u00e9e, dans laquelle est expos\u00e9 un tr\u00e9sor datant des 13/14\u00e8mes si\u00e8cles avec une alliance juive unique et des \u00e9critures h\u00e9bra\u00efques (datant des 12\u00e8me, 13\u00e8me et 14\u00e8mes si\u00e8cles). Apr\u00e8s la red\u00e9couverte du Mikw\u00e9, Erfurt abrite des t\u00e9moins uniques et fascinants d\u2019une communaut\u00e9 juive m\u00e9di\u00e9vale. "
},
{
"@language": "en",
"@value": "The Old Synagogue is one of very few preserved medieval synagogues in Europe. Thanks to the extensive preservation of the original structure, it has a special place in the history of art and architecture and is among the most impressive and highly rated architectural monuments in Erfurt and Thuringia. The synagogue was constructed during the Middle Ages on the \"via regia\", one of the major European trade routes, at the heart of the historical old quarter very close to the Merchants Bridge and the town hall. Many parts of the structure still remain today, including all four thick outer walls, the Roman\u00adesque gemel window, the Gothic rose window and the entrance to the synagogue room. "
},
{
"@language": "de",
"@value": "Beispiel Beschreibung"
},
{
"@id": "genid-28b33237f71b41e3ad54a99e1da769b9-b1",
"@type": [
"thuecat:Html"
],
"schema:value": {
"@language": "de",
"@value": "Mit der Alten Synagoge weist Erfurt die \u00e4lteste bis zum Dach erhaltene Synagoge in Mitteleuropa vor. Hier waren bis Ende der 90er Jahre nur die Spitzen zweier Giebel sichtbar, welche aus einem Gewirr von Anbauten herausragten. Nach dem Abriss einiger Bauten ringsum konnte ein Bauforscher klar vier Bauphasen der Synagoge unterscheiden, dessen \u00e4lteste um 1100 zu datieren ist. \n\nDer Bau von 1270, mit der heute sichtbaren Westfassade samt Ma\u00dfwerkrosette, wurde nach Norden erweitert. Brandspuren am Mauerwerk verweisen auf einen Vorg\u00e4ngerbau, der wahrscheinlich einem Pogrom zum Opfer fiel. Die Synagoge diente bis 1349 als Gotteshaus. In diesem Jahr l\u00f6schte ein barbarisches Pestpogrom die erste j\u00fcdische Gemeinde Erfurts aus. Die Stadt verkaufte das Geb\u00e4ude an einen H\u00e4ndler, der es zum Speicher umbauen lie\u00df. Dabei wurde der hohe Raum von Balkendecken unterteilt, ein breiterer Eingang an Stelle des Thoraschreins geschaffen und die Synagoge unterkellert. Im Erdgeschoss zeugen noch einige Spuren von der Erstnutzung, wie bspw. ein Lichtergesims. \n\nDas Erdgeschoss mit der wuchtigen gotischen Balkendecke und der Keller werden ebenso wie das Obergeschoss, welches von der Festkultur des 19. Jahrhunderts zeugt, museal genutzt. Wer heute den Saal betritt, der f\u00fchlt sich in die vergangene Welt von Tango und Foxtrott unter Gouvernantenaufsicht zur\u00fcckversetzt. Schablonenmalerei sowie einige Tapetenreste schm\u00fccken die W\u00e4nde. \nIm Erdgeschoss wird die Baugeschichte thematisiert, der Keller ist dem Erfurter Schatz aus M\u00fcnzen, Gef\u00e4\u00dfen, gotischem Schmuck und dem j\u00fcdischen Hochzeitsring vorbehalten. \n\nIm Saal zeigt das Haus eine Sammlung von hebr\u00e4ischen Handschriften, welche der Erfurter Gemeinde geh\u00f6rten. Diese Hebraica werden heute in der Staatsbibliothek Berlin aufbewahrt. Abwechselnd k\u00f6nnen sie in Erfurt als Original oder Faksimile bestaunt werden. \n\nMit der Alten Synagoge und einer 2007 an der Kr\u00e4merbr\u00fccke gefundenen Mikwe aus der Gotik, deren wissenschaftliche Erforschung noch andauert, kann Erfurt einmalige und faszinierende Zeugnisse der noch wenig bekannten Geschichte einer mittelalterlichen Gemeinde vorweisen."
}
},
{
"@id": "genid-28b33237f71b41e3ad54a99e1da769b9-b2",
"@type": [
"thuecat:Html"
],
"schema:value": {
"@language": "fr",
"@value": "La vieille synagogue (datant des ann\u00e9es 1100) est la synagogue la plus vieille d\u2019Europe totalement conserv\u00e9e, dans laquelle est expos\u00e9 un tr\u00e9sor datant des 13/14\u00e8mes si\u00e8cles avec une alliance juive unique et des \u00e9critures h\u00e9bra\u00efques (datant des 12\u00e8me, 13\u00e8me et 14\u00e8mes si\u00e8cles). Apr\u00e8s la red\u00e9couverte du Mikw\u00e9, Erfurt abrite des t\u00e9moins uniques et fascinants d\u2019une communaut\u00e9 juive m\u00e9di\u00e9vale. "
}
},
{
"@id": "genid-28b33237f71b41e3ad54a99e1da769b9-b3",
"@type": [
"thuecat:Html"
],
"schema:value": {
"@language": "en",
"@value": "The Old Synagogue is one of very few preserved medieval synagogues in Europe. Thanks to the extensive preservation of the original structure, it has a special place in the history of art and architecture and is among the most impressive and highly rated architectural monuments in Erfurt and Thuringia. The synagogue was constructed during the Middle Ages on the \"via regia\", one of the major European trade routes, at the heart of the historical old quarter very close to the Merchants Bridge and the town hall. Many parts of the structure still remain today, including all four thick outer walls, the Roman\u00adesque gemel window, the Gothic rose window and the entrance to the synagogue room. "
}
}
],
"schema:identifier": {
"@type": "schema:URL",
"@value": "https://www.thueringen-entdecken.de/urlaub-hotel-reisen/alte-synagoge-erfurt-115157.html"
},
"schema:name": [
{
"@language": "de",
"@value": "Alte Synagoge"
},
{
"@language": "fr",
"@value": "La vieille synagogue"
},
{
"@language": "en",
"@value": "Old Synagogue"
}
],
"schema:url": {
"@type": "schema:URL",
"@value": "http://www.alte-synagoge.erfurt.de"
},
"thuecat:contentResponsible": {
"@id": "https://thuecat.org/resources/018132452787-ngbe"
},
"schema:openingHoursSpecification": {
"@id": "genid-28b33237f71b41e3ad54a99e1da769b9-b13",
"@type": [
"schema:Intangible",
"schema:StructuredValue",
"schema:Thing",
"schema:OpeningHoursSpecification"
],
"schema:closes": {
"@type": "schema:Time",
"@value": "18:00: 00"
},
"schema:dayOfWeek": [
{
"@type": "schema:DayOfWeek",
"@value": "schema:Wednesday"
}
],
"schema:opens": {
"@type": "schema:Time",
"@value": "10:00:00"
},
"schema:validFrom": {
"@type": "schema:Date",
"@value": "2021-03-01"
},
"schema:validThrough": {
"@type": "schema:Date",
"@value": "2099-12-31"
}
}
}
]
}

View file

@ -0,0 +1,18 @@
"tx_thuecat_tourist_attraction",,,,,,,,
,"uid","pid","sys_language_uid","remote_id","title",,,
,1,10,0,"https://thuecat.org/resources/165868194223-zmqf","Alte Synagoge",,,
,2,10,1,"https://thuecat.org/resources/165868194223-zmqf","Old Synagogue",,,
,3,10,2,"https://thuecat.org/resources/165868194223-zmqf","La vieille synagogue",,,
"tx_thuecat_import_log",,,,,,,,
,"uid","pid","configuration","log_entries",,,,
,1,0,1,0,,,,
"tx_thuecat_import_log_entry",,,,,,,,
,"uid","pid","type","import_log","record_uid","table_name","insertion","errors"
,1,0,"mappingError",1,0,,0,"[""Could not map incoming JSON-LD to target object: Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character""]"
,2,0,"mappingError",1,0,,0,"[""Could not map incoming JSON-LD to target object: Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character""]"
,3,0,"mappingError",1,0,,0,"[""Could not map incoming JSON-LD to target object: Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character""]"
,4,0,"savingEntity",1,1,"tx_thuecat_organisation",1,"[]"
,5,0,"savingEntity",1,1,"tx_thuecat_town",0,"[]"
,6,0,"savingEntity",1,1,"tx_thuecat_tourist_attraction",1,"[]"
,7,0,"savingEntity",1,2,"tx_thuecat_tourist_attraction",0,"[]"
,8,0,"savingEntity",1,3,"tx_thuecat_tourist_attraction",0,"[]"
1 tx_thuecat_tourist_attraction
2 uid pid sys_language_uid remote_id title
3 1 10 0 https://thuecat.org/resources/165868194223-zmqf Alte Synagoge
4 2 10 1 https://thuecat.org/resources/165868194223-zmqf Old Synagogue
5 3 10 2 https://thuecat.org/resources/165868194223-zmqf La vieille synagogue
6 tx_thuecat_import_log
7 uid pid configuration log_entries
8 1 0 1 0
9 tx_thuecat_import_log_entry
10 uid pid type import_log record_uid table_name insertion errors
11 1 0 mappingError 1 0 0 ["Could not map incoming JSON-LD to target object: Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character"]
12 2 0 mappingError 1 0 0 ["Could not map incoming JSON-LD to target object: Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character"]
13 3 0 mappingError 1 0 0 ["Could not map incoming JSON-LD to target object: Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character"]
14 4 0 savingEntity 1 1 tx_thuecat_organisation 1 []
15 5 0 savingEntity 1 1 tx_thuecat_town 0 []
16 6 0 savingEntity 1 1 tx_thuecat_tourist_attraction 1 []
17 7 0 savingEntity 1 2 tx_thuecat_tourist_attraction 0 []
18 8 0 savingEntity 1 3 tx_thuecat_tourist_attraction 0 []

View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<dataset>
<pages>
<uid>1</uid>
<pid>0</pid>
<tstamp>1613400587</tstamp>
<crdate>1613400558</crdate>
<cruser_id>1</cruser_id>
<doktype>4</doktype>
<title>Rootpage</title>
<is_siteroot>1</is_siteroot>
</pages>
<pages>
<uid>10</uid>
<pid>1</pid>
<tstamp>1613400587</tstamp>
<crdate>1613400558</crdate>
<cruser_id>1</cruser_id>
<doktype>254</doktype>
<title>Storage folder</title>
</pages>
<sys_language>
<uid>1</uid>
<pid>0</pid>
<title>English</title>
<flag>en-us-gb</flag>
<language_isocode>en</language_isocode>
</sys_language>
<sys_language>
<uid>2</uid>
<pid>0</pid>
<title>French</title>
<flag>fr</flag>
<language_isocode>fr</language_isocode>
</sys_language>
<tx_thuecat_import_configuration>
<uid>1</uid>
<pid>0</pid>
<tstamp>1613400587</tstamp>
<crdate>1613400558</crdate>
<cruser_id>1</cruser_id>
<disable>0</disable>
<title>Import With Exceptions</title>
<type>static</type>
<configuration><![CDATA[<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<T3FlexForms>
<data>
<sheet index="sDEF">
<language index="lDEF">
<field index="storagePid">
<value index="vDEF">10</value>
</field>
<field index="urls">
<el index="el">
<field index="602a89f54d694654233086">
<value index="url">
<el>
<field index="url">
<value index="vDEF">https://thuecat.org/resources/mapping-exception</value>
</field>
</el>
</value>
<value index="_TOGGLE">0</value>
</field>
<field index="602a89f5e6c52907080672">
<value index="url">
<el>
<field index="url">
<value index="vDEF">https://thuecat.org/resources/165868194223-zmqf</value>
</field>
</el>
</value>
<value index="_TOGGLE">0</value>
</field>
</el>
</field>
</language>
</sheet>
</data>
</T3FlexForms>
]]></configuration>
</tx_thuecat_import_configuration>
<tx_thuecat_town>
<uid>1</uid>
<pid>10</pid>
<tstamp>1613401129</tstamp>
<crdate>1613401129</crdate>
<cruser_id>1</cruser_id>
<disable>0</disable>
<remote_id>https://thuecat.org/resources/043064193523-jcyt</remote_id>
<managed_by>1</managed_by>
<tourist_information>0</tourist_information>
<title>Erfurt</title>
</tx_thuecat_town>
</dataset>

View file

@ -0,0 +1,18 @@
"tx_thuecat_tourist_attraction",,,,,,,,
,"uid","pid","sys_language_uid","remote_id","title",,,
,1,10,0,"https://thuecat.org/resources/165868194223-zmqf","Alte Synagoge",,,
,2,10,1,"https://thuecat.org/resources/165868194223-zmqf","Old Synagogue",,,
,3,10,2,"https://thuecat.org/resources/165868194223-zmqf","La vieille synagogue",,,
"tx_thuecat_import_log",,,,,,,,
,"uid","pid","configuration","log_entries",,,,
,1,0,1,0,,,,
"tx_thuecat_import_log_entry",,,,,,,,
,"uid","pid","type","import_log","record_uid","table_name","insertion","errors"
,1,0,"mappingError",1,0,,0,"[""Could not map incoming JSON-LD to target object: DateTimeImmutable::__construct(): Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character""]"
,2,0,"mappingError",1,0,,0,"[""Could not map incoming JSON-LD to target object: DateTimeImmutable::__construct(): Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character""]"
,3,0,"mappingError",1,0,,0,"[""Could not map incoming JSON-LD to target object: DateTimeImmutable::__construct(): Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character""]"
,4,0,"savingEntity",1,1,"tx_thuecat_organisation",1,"[]"
,5,0,"savingEntity",1,1,"tx_thuecat_town",0,"[]"
,6,0,"savingEntity",1,1,"tx_thuecat_tourist_attraction",1,"[]"
,7,0,"savingEntity",1,2,"tx_thuecat_tourist_attraction",0,"[]"
,8,0,"savingEntity",1,3,"tx_thuecat_tourist_attraction",0,"[]"
1 tx_thuecat_tourist_attraction
2 uid pid sys_language_uid remote_id title
3 1 10 0 https://thuecat.org/resources/165868194223-zmqf Alte Synagoge
4 2 10 1 https://thuecat.org/resources/165868194223-zmqf Old Synagogue
5 3 10 2 https://thuecat.org/resources/165868194223-zmqf La vieille synagogue
6 tx_thuecat_import_log
7 uid pid configuration log_entries
8 1 0 1 0
9 tx_thuecat_import_log_entry
10 uid pid type import_log record_uid table_name insertion errors
11 1 0 mappingError 1 0 0 ["Could not map incoming JSON-LD to target object: DateTimeImmutable::__construct(): Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character"]
12 2 0 mappingError 1 0 0 ["Could not map incoming JSON-LD to target object: DateTimeImmutable::__construct(): Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character"]
13 3 0 mappingError 1 0 0 ["Could not map incoming JSON-LD to target object: DateTimeImmutable::__construct(): Failed to parse time string (18:00: 00) at position 5 (:): Unexpected character"]
14 4 0 savingEntity 1 1 tx_thuecat_organisation 1 []
15 5 0 savingEntity 1 1 tx_thuecat_town 0 []
16 6 0 savingEntity 1 1 tx_thuecat_tourist_attraction 1 []
17 7 0 savingEntity 1 2 tx_thuecat_tourist_attraction 0 []
18 8 0 savingEntity 1 3 tx_thuecat_tourist_attraction 0 []

View file

@ -77,6 +77,17 @@ class ImportTest extends TestCase
];
protected $configurationToUseInTestInstance = [
'LOG' => [
'WerkraumMedia' => [
'writerConfiguration' => [
\TYPO3\CMS\Core\Log\LogLevel::DEBUG => [
\TYPO3\CMS\Core\Log\Writer\FileWriter::class => [
'logFileInfix' => 'debug',
],
],
],
],
],
'EXTENSIONS' => [
'thuecat' => [
'apiKey' => null,
@ -343,6 +354,32 @@ class ImportTest extends TestCase
$this->assertCSVDataSet('EXT:thuecat/Tests/Functional/Fixtures/Import/ImportsContainsPlace.csv');
}
/**
* @test
*/
public function importsFollowingRecordsInCaseOfAnMappingException(): void
{
$this->importDataSet(__DIR__ . '/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingException.xml');
GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/mapping-exception.json');
GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/165868194223-zmqf.json');
GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/018132452787-ngbe.json');
GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/043064193523-jcyt.json');
GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/573211638937-gmqb.json');
GuzzleClientFaker::appendResponseFromFile(__DIR__ . '/Fixtures/Import/Guzzle/thuecat.org/resources/497839263245-edbm.json');
for ($i = 1; $i <= 9; $i++) {
GuzzleClientFaker::appendNotFoundResponse();
}
$configuration = $this->get(ImportConfigurationRepository::class)->findByUid(1);
$this->get(Importer::class)->importConfiguration($configuration);
if (version_compare(PHP_VERSION, '8.1.0', '<')) {
$this->assertCSVDataSet('EXT:thuecat/Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingExceptionOldPhp.csv');
} else {
$this->assertCSVDataSet('EXT:thuecat/Tests/Functional/Fixtures/Import/ImportsFollowingRecordsInCaseOfAnMappingException.csv');
}
}
/**
* @test
* @testdox Referencing the same thing multiple times only adds it once.

View file

@ -23,7 +23,8 @@ declare(strict_types=1);
namespace WerkraumMedia\ThueCat\Tests\Unit\Domain\Import\Typo3Converter;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Log\Logger;
use WerkraumMedia\ThueCat\Domain\Import\Entity\Properties\ForeignReference;
use WerkraumMedia\ThueCat\Domain\Import\Entity\Town;
use WerkraumMedia\ThueCat\Domain\Import\Importer;
@ -55,6 +56,7 @@ class GeneralConverterTest extends TestCase
$townRepository = $this->createStub(TownRepository::class);
$parkingFacilityRepository = $this->createStub(ParkingFacilityRepository::class);
$nameExtractor = $this->createStub(NameExtractor::class);
$logManager = $this->createStub(LogManager::class);
$subject = new GeneralConverter(
$resolveForeignReference,
@ -63,7 +65,8 @@ class GeneralConverterTest extends TestCase
$organisationRepository,
$townRepository,
$parkingFacilityRepository,
$nameExtractor
$nameExtractor,
$logManager
);
self::assertInstanceOf(
@ -86,7 +89,8 @@ class GeneralConverterTest extends TestCase
$townRepository = $this->createStub(TownRepository::class);
$parkingFacilityRepository = $this->createStub(ParkingFacilityRepository::class);
$nameExtractor = $this->createStub(NameExtractor::class);
$logger = $this->createStub(LoggerInterface::class);
$logManager = $this->createStub(LogManager::class);
$logManager->method('getLogger')->willReturn($this->createStub(Logger::class));
$subject = new GeneralConverter(
$resolveForeignReference,
@ -95,9 +99,9 @@ class GeneralConverterTest extends TestCase
$organisationRepository,
$townRepository,
$parkingFacilityRepository,
$nameExtractor
$nameExtractor,
$logManager
);
$subject->setLogger($logger);
$contentResponsible = new ForeignReference();
$contentResponsible->setId('https://example.com/content-responsible');

View file

@ -15,11 +15,13 @@ CREATE TABLE tx_thuecat_import_log (
);
CREATE TABLE tx_thuecat_import_log_entry (
type varchar(255) DEFAULT 'savingEntity' NOT NULL,
import_log int(11) unsigned DEFAULT '0' NOT NULL,
errors text,
remote_id varchar(255) DEFAULT '' NOT NULL,
record_uid int(11) unsigned DEFAULT '0' NOT NULL,
table_name varchar(255) DEFAULT '' NOT NULL,
insertion TINYINT(1) unsigned DEFAULT '0' NOT NULL,
errors text,
);
CREATE TABLE tx_thuecat_organisation (

View file

@ -326,11 +326,11 @@ parameters:
-
message: "#^Cannot call method findByUid\\(\\) on mixed\\.$#"
count: 11
count: 12
path: Tests/Functional/ImportTest.php
-
message: "#^Cannot call method importConfiguration\\(\\) on mixed\\.$#"
count: 11
count: 12
path: Tests/Functional/ImportTest.php