From 2a7334ed3486111a40e6069937cf5e1d97326809 Mon Sep 17 00:00:00 2001 From: Jim Safley Date: Fri, 6 Dec 2024 16:49:23 -0500 Subject: [PATCH] Convert data types (#2231) --- .../Adapter/AbstractResourceEntityAdapter.php | 3 + application/src/Api/Adapter/ValueHydrator.php | 48 ++++++++++++ .../DataType/ConversionTargetInterface.php | 23 ++++++ application/src/DataType/Literal.php | 33 +++++++-- .../DataType/Resource/AbstractResource.php | 18 ++++- application/src/DataType/Uri.php | 36 ++++++--- .../src/Form/ResourceBatchUpdateForm.php | 14 +++- application/src/View/Helper/DataType.php | 17 +++-- .../common/property-form-batch-edit.phtml | 73 ++++++++++++++++--- 9 files changed, 229 insertions(+), 36 deletions(-) create mode 100644 application/src/DataType/ConversionTargetInterface.php diff --git a/application/src/Api/Adapter/AbstractResourceEntityAdapter.php b/application/src/Api/Adapter/AbstractResourceEntityAdapter.php index 3f8e3e4b22..3fab50c0f8 100644 --- a/application/src/Api/Adapter/AbstractResourceEntityAdapter.php +++ b/application/src/Api/Adapter/AbstractResourceEntityAdapter.php @@ -737,6 +737,9 @@ public function preprocessBatchUpdate(array $data, Request $request) if (isset($rawData['set_value_visibility'])) { $data['set_value_visibility'] = $rawData['set_value_visibility']; } + if (isset($rawData['convert_data_types'])) { + $data['convert_data_types'] = $rawData['convert_data_types']; + } // Add values that satisfy the bare minimum needed to identify them. foreach ($rawData as $term => $valueObjects) { diff --git a/application/src/Api/Adapter/ValueHydrator.php b/application/src/Api/Adapter/ValueHydrator.php index bae4afd96e..1208cee564 100644 --- a/application/src/Api/Adapter/ValueHydrator.php +++ b/application/src/Api/Adapter/ValueHydrator.php @@ -3,6 +3,7 @@ use Doctrine\Common\Collections\Criteria; use Omeka\Api\Request; +use Omeka\DataType\ConversionTargetInterface; use Omeka\Entity\Resource; use Omeka\Entity\Value; use Omeka\Entity\ValueAnnotation; @@ -170,5 +171,52 @@ public function hydrate(Request $request, Resource $entity, foreach ($newValues as $newValue) { $valueCollection->add($newValue); } + + // Convert data types. + if ($isUpdate) { + $logger = $adapter->getServiceLocator()->get('Omeka\Logger'); + $convertSpecs = $representation['convert_data_types'] ?? []; + foreach ($convertSpecs as $convertSpec) { + $propertyId = $convertSpec['convert_property_id'] ?? null; + $dataTypeSource = $convertSpec['convert_data_type_source'] ?? null; + $dataTypeTarget = $convertSpec['convert_data_type_target'] ?? null; + + // Get the target data type. + $dataType = $dataTypes->get($dataTypeTarget); + if (!($dataType instanceof ConversionTargetInterface)) { + // Cannot convert to this data type. + continue; + } + + // Filter values by property and source data type (if given). + $property = $entityManager->getReference('Omeka\Entity\Property', $propertyId); + $criteria = Criteria::create()->where(Criteria::expr()->eq('property', $property)); + if ($dataTypeSource) { + $criteria->andWhere(Criteria::expr()->eq('type', $dataTypeSource)); + } + $values = $valueCollection->matching($criteria); + + // Iterate each value, converting if possible. + foreach ($values as $value) { + $converted = $dataType->convert($value, $dataTypeTarget); + if ($converted) { + // The conversion was successful. Set the new data type. + $value->setType($dataTypeTarget); + } else { + // The conversion was not successful. Log a NOTICE message. + $property = $value->getProperty(); + $vocabulary = $property->getVocabulary(); + $message = sprintf( + 'Convert data type - could not convert to "%s" from "%s" for property "%s" resource "%s"', // @translate + $dataTypeTarget, + $value->getType(), + sprintf('%s:%s', $vocabulary->getPrefix(), $property->getLocalName()), + $value->getResource()->getId() + ); + $logger->notice($message); + } + } + } + } } } diff --git a/application/src/DataType/ConversionTargetInterface.php b/application/src/DataType/ConversionTargetInterface.php new file mode 100644 index 0000000000..23c0022f75 --- /dev/null +++ b/application/src/DataType/ConversionTargetInterface.php @@ -0,0 +1,23 @@ +literalIsValid($valueObject['@value'] ?? null); + } + + public function literalIsValid($literal) + { + return (is_string($literal) && '' !== trim($literal)); } public function hydrate(array $valueObject, Value $value, AbstractEntityAdapter $adapter) @@ -68,4 +67,22 @@ public function valueAnnotationForm(PhpRenderer $view) { return $view->partial('common/data-type/value-annotation-literal'); } + + public function convert(Value $valueObject, string $dataTypeTarget) : bool + { + $value = $valueObject->getValue(); + $uri = $valueObject->getUri(); + + // Note that, in order to prevent data loss, we do not convert if a URI + // and value are present. + if ($this->literalIsValid($uri) && !$this->literalIsValid($value)) { + $valueObject->setValue($uri); + $valueObject->setUri(null); + return true; + } + if ($this->literalIsValid($value) && !$this->literalIsValid($uri)) { + return true; + } + return false; + } } diff --git a/application/src/DataType/Resource/AbstractResource.php b/application/src/DataType/Resource/AbstractResource.php index e2830d31b3..6486108e4c 100644 --- a/application/src/DataType/Resource/AbstractResource.php +++ b/application/src/DataType/Resource/AbstractResource.php @@ -4,12 +4,13 @@ use Omeka\Api\Adapter\AbstractEntityAdapter; use Omeka\Api\Exception; use Omeka\Api\Representation\ValueRepresentation; +use Omeka\DataType\ConversionTargetInterface; use Omeka\DataType\DataTypeWithOptionsInterface; -use Omeka\Entity; +use Omeka\Entity\Value; use Laminas\View\Renderer\PhpRenderer; use Omeka\Stdlib\Message; -abstract class AbstractResource implements DataTypeWithOptionsInterface +abstract class AbstractResource implements DataTypeWithOptionsInterface, ConversionTargetInterface { /** * Get the class names of valid value resources. @@ -45,7 +46,7 @@ public function isValid(array $valueObject) return false; } - public function hydrate(array $valueObject, Entity\Value $value, AbstractEntityAdapter $adapter) + public function hydrate(array $valueObject, Value $value, AbstractEntityAdapter $adapter) { $serviceLocator = $adapter->getServiceLocator(); @@ -104,4 +105,15 @@ public function getFulltextText(PhpRenderer $view, ValueRepresentation $value) { return $value->valueResource()->title(); } + + public function convert(Value $valueObject, string $dataTypeTarget) : bool + { + $value = $valueObject->getValue(); + $uri = $valueObject->getUri(); + + if (is_numeric($valueObject->getValueResource())) { + return true; + } + return false; + } } diff --git a/application/src/DataType/Uri.php b/application/src/DataType/Uri.php index 8fa4bd10e8..ae5a3deb6f 100644 --- a/application/src/DataType/Uri.php +++ b/application/src/DataType/Uri.php @@ -6,7 +6,7 @@ use Omeka\Entity\Value; use Laminas\View\Renderer\PhpRenderer; -class Uri extends AbstractDataType implements ValueAnnotatingInterface +class Uri extends AbstractDataType implements ValueAnnotatingInterface, ConversionTargetInterface { public function getName() { @@ -25,16 +25,17 @@ public function form(PhpRenderer $view) public function isValid(array $valueObject) { - if (!isset($valueObject['@id']) - || !is_string($valueObject['@id']) - ) { + return $this->uriIsValid($valueObject['@id'] ?? null); + } + + public function uriIsValid($uri) + { + if (!is_string($uri)) { return false; } - - $trimmed = trim($valueObject['@id']); - $scheme = parse_url($trimmed, \PHP_URL_SCHEME); - - return !('' === $trimmed || $scheme === 'javascript'); + $uri = trim($uri); + $scheme = parse_url($uri, \PHP_URL_SCHEME); + return !('' === $uri || 'javascript' === $scheme); } public function hydrate(array $valueObject, Value $value, AbstractEntityAdapter $adapter) @@ -84,4 +85,21 @@ public function valueAnnotationForm(PhpRenderer $view) { return $view->partial('common/data-type/value-annotation-uri'); } + + public function convert(Value $valueObject, string $dataTypeTarget) : bool + { + $value = $valueObject->getValue(); + $uri = $valueObject->getUri(); + + if ($this->uriIsValid($uri)) { + return true; + } + if ($this->uriIsValid($value)) { + // Move the value to the URI. + $valueObject->setUri($value); + $valueObject->setValue(null); + return true; + } + return false; + } } diff --git a/application/src/Form/ResourceBatchUpdateForm.php b/application/src/Form/ResourceBatchUpdateForm.php index 8ea97a277d..8d9ac44057 100644 --- a/application/src/Form/ResourceBatchUpdateForm.php +++ b/application/src/Form/ResourceBatchUpdateForm.php @@ -238,6 +238,15 @@ public function init() ], ]); + // This hidden element manages the elements "convert_data_types" added in the view. + $this->add([ + 'name' => 'convert_data_types', + 'type' => Element\Hidden::class, + 'attributes' => [ + 'value' => '', + ], + ]); + $addEvent = new Event('form.add_elements', $this); $this->getEventManager()->triggerEvent($addEvent); @@ -401,6 +410,9 @@ public function preprocessData() $preData['append'][$value['property_id']][] = $valueObj; } } + if (isset($data['convert_data_types'])) { + $preData['replace']['convert_data_types'] = $data['convert_data_types']; + } if (isset($data['add_to_item_set'])) { $preData['append']['o:item_set'] = array_unique($data['add_to_item_set']); } @@ -415,7 +427,7 @@ public function preprocessData() 'remove_from_sites', 'add_to_sites', 'clear_property_values', 'set_value_visibility', 'clear_language', 'language', - 'csrf', 'id', 'o:id', 'value', + 'csrf', 'id', 'o:id', 'value', 'convert_data_types', ]; foreach ($data as $key => $value) { diff --git a/application/src/View/Helper/DataType.php b/application/src/View/Helper/DataType.php index cd2a9bad73..3b25279b94 100644 --- a/application/src/View/Helper/DataType.php +++ b/application/src/View/Helper/DataType.php @@ -1,6 +1,7 @@ dataTypes as $dataTypeName) { $dataType = $this->manager->get($dataTypeName); + if (isset($options['is_conversion_target']) && !($dataType instanceof ConversionTargetInterface)) { + // Filter out data types that are not convertable. + continue; + } $label = $dataType->getLabel(); if ($optgroupLabel = $dataType->getOptgroupLabel()) { // Hash the optgroup key to avoid collisions when merging with @@ -58,7 +63,7 @@ public function getSelect($name, $value = null, $attributes = []) $optgroupKey = md5($optgroupLabel); // Put resource data types before ones added by modules. $optionsVal = in_array($dataTypeName, ['resource', 'resource:item', 'resource:itemset', 'resource:media']) - ? 'options' : 'optgroupOptions'; + ? 'valueOptions' : 'optgroupOptions'; if (!isset(${$optionsVal}[$optgroupKey])) { ${$optionsVal}[$optgroupKey] = [ 'label' => $optgroupLabel, @@ -67,16 +72,16 @@ public function getSelect($name, $value = null, $attributes = []) } ${$optionsVal}[$optgroupKey]['options'][$dataTypeName] = $label; } else { - $options[$dataTypeName] = $label; + $valueOptions[$dataTypeName] = $label; } } // Always put data types not organized in option groups before data // types organized within option groups. - $options = array_merge($options, $optgroupOptions); + $valueOptions = array_merge($valueOptions, $optgroupOptions); $element = new Select($name); $element->setEmptyOption('') - ->setValueOptions($options) + ->setValueOptions($valueOptions) ->setAttributes($attributes); if (!$element->getAttribute('multiple') && is_array($value)) { $value = reset($value); diff --git a/application/view/common/property-form-batch-edit.phtml b/application/view/common/property-form-batch-edit.phtml index eefcee9c89..a9e3deb626 100644 --- a/application/view/common/property-form-batch-edit.phtml +++ b/application/view/common/property-form-batch-edit.phtml @@ -4,7 +4,13 @@ $escape = $this->plugin('escapeHtml'); $selectProperty = $this->propertySelect([ 'name' => 'value[__INDEX__][property_id]', - 'options' => ['empty_option' => $translate('Select property')], + 'options' => [ + 'empty_option' => '', + ], + 'attributes' => [ + 'class' => 'property-select', + 'data-placeholder' => $translate('Select property'), + ], ]); $templateLiteral = '
@@ -46,7 +52,38 @@ $templateUri = '
- + +
+'; + +$convertDataTypePropertySelect = $this->propertySelect([ + 'name' => 'convert_data_types[__INDEX__][convert_property_id]', + 'options' => [ + 'empty_option' => '', + ], + 'attributes' => [ + 'class' => 'property-id-select chosen-select', + 'data-placeholder' => $translate('Select property'), + ], +]); +$convertDataTypeDataTypeSelect = $this->dataType()->getSelect( + 'convert_data_types[__INDEX__][convert_data_type_target]', + null, + [ + 'class' => 'data-type-target-select chosen-select', + 'data-placeholder' => $translate('Select data type'), + ], + ['is_conversion_target' => true] +); +$convertDataTypeTemplate = ' +
+
+ +
+
+ ' . $convertDataTypePropertySelect . ' + ' . $convertDataTypeDataTypeSelect . ' +
'; ?> @@ -69,6 +106,12 @@ $templateUri = ' +
+
+ +