Skip to content

Commit

Permalink
Convert data types (#2231)
Browse files Browse the repository at this point in the history
  • Loading branch information
jimsafley authored Dec 6, 2024
1 parent 7ca3603 commit 2a7334e
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 36 deletions.
3 changes: 3 additions & 0 deletions application/src/Api/Adapter/AbstractResourceEntityAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
48 changes: 48 additions & 0 deletions application/src/Api/Adapter/ValueHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
}
}
}
23 changes: 23 additions & 0 deletions application/src/DataType/ConversionTargetInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
namespace Omeka\DataType;

use Omeka\Entity\Value;

interface ConversionTargetInterface
{
/**
* Convert a value to the target data type.
*
* Return true if the conversion was succesfully done. Return false if the
* conversion was not possible. Conversions should avoid data loss by not
* overwriting existing data. The value hydrator will log when conversions
* are not possible, and it will change the data type for you, so there's no
* need to do it here.
*
* @param Value $valueObject
* @param string $dataTypeTarget Only needed for extreme edge cases where
* the data type does not know its own name.
* @return bool
*/
public function convert(Value $valueObject, string $dataTypeTarget) : bool;
}
33 changes: 25 additions & 8 deletions application/src/DataType/Literal.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
use Omeka\Entity\Value;
use Laminas\View\Renderer\PhpRenderer;

class Literal extends AbstractDataType implements ValueAnnotatingInterface
class Literal extends AbstractDataType implements ValueAnnotatingInterface, ConversionTargetInterface
{
public function getName()
{
Expand All @@ -25,13 +25,12 @@ public function form(PhpRenderer $view)

public function isValid(array $valueObject)
{
if (isset($valueObject['@value'])
&& is_string($valueObject['@value'])
&& '' !== trim($valueObject['@value'])
) {
return true;
}
return false;
return $this->literalIsValid($valueObject['@value'] ?? null);
}

public function literalIsValid($literal)
{
return (is_string($literal) && '' !== trim($literal));
}

public function hydrate(array $valueObject, Value $value, AbstractEntityAdapter $adapter)
Expand Down Expand Up @@ -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;
}
}
18 changes: 15 additions & 3 deletions application/src/DataType/Resource/AbstractResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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;
}
}
36 changes: 27 additions & 9 deletions application/src/DataType/Uri.php
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand All @@ -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)
Expand Down Expand Up @@ -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;
}
}
14 changes: 13 additions & 1 deletion application/src/Form/ResourceBatchUpdateForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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']);
}
Expand All @@ -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) {
Expand Down
17 changes: 11 additions & 6 deletions application/src/View/Helper/DataType.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?php
namespace Omeka\View\Helper;

use Omeka\DataType\ConversionTargetInterface;
use Omeka\DataType\Manager as DataTypeManager;
use Omeka\DataType\ValueAnnotatingInterface;
use Laminas\Form\Element\Select;
Expand Down Expand Up @@ -45,20 +46,24 @@ public function __construct(DataTypeManager $dataTypeManager, array $valueAnnota
* @param string|array $value
* @param array $attributes
*/
public function getSelect($name, $value = null, $attributes = [])
public function getSelect($name, $value = null, $attributes = [], $options = [])
{
$options = [];
$valueOptions = [];
$optgroupOptions = [];
foreach ($this->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
// data types without an optgroup.
$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,
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 2a7334e

Please sign in to comment.