first commit
This commit is contained in:
@@ -0,0 +1,556 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/metadata/MetadataProperty.php
|
||||
*
|
||||
* Copyright (c) 2014-2021 Simon Fraser University
|
||||
* Copyright (c) 2000-2021 John Willinsky
|
||||
* Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
|
||||
*
|
||||
* @class MetadataProperty
|
||||
*
|
||||
* @ingroup metadata
|
||||
*
|
||||
* @see MetadataSchema
|
||||
* @see MetadataRecord
|
||||
*
|
||||
* @brief Class representing metadata properties. It specifies type and cardinality
|
||||
* of a meta-data property (=term, field, ...) and whether the property can
|
||||
* be internationalized. It also provides a validator to test whether input
|
||||
* conforms to the property specification.
|
||||
*
|
||||
* In the DCMI abstract model, this class specifies a property together with its
|
||||
* allowed range and cardinality.
|
||||
*
|
||||
* We also define the resource types (application entities, association types)
|
||||
* that can be described with the property. This allows us to check that only
|
||||
* valid resource associations are made. It also allows us to prepare property
|
||||
* entry forms or displays for a given resource type and integrate these in the
|
||||
* work-flow of the resource. By dynamically adding or removing assoc types,
|
||||
* end users will be able to configure the meta-data fields that they wish to
|
||||
* make available, persist or enter in their application.
|
||||
*/
|
||||
|
||||
namespace PKP\metadata;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use PKP\core\PKPString;
|
||||
use PKP\db\DAORegistry;
|
||||
use PKP\validation\ValidatorControlledVocab;
|
||||
use PKP\validation\ValidatorFactory;
|
||||
|
||||
class MetadataProperty
|
||||
{
|
||||
// literal values (plain)
|
||||
public const METADATA_PROPERTY_TYPE_STRING = 1;
|
||||
|
||||
// literal values (typed)
|
||||
public const METADATA_PROPERTY_TYPE_DATE = 2; // This is W3CDTF encoding without time (YYYY[-MM[-DD]])!
|
||||
public const METADATA_PROPERTY_TYPE_INTEGER = 3;
|
||||
|
||||
// non-literal value string from a controlled vocabulary
|
||||
public const METADATA_PROPERTY_TYPE_VOCABULARY = 4;
|
||||
|
||||
// non-literal value URI
|
||||
public const METADATA_PROPERTY_TYPE_URI = 5;
|
||||
|
||||
// non-literal value pointing to a separate description set instance (=another MetadataRecord object)
|
||||
public const METADATA_PROPERTY_TYPE_COMPOSITE = 6;
|
||||
|
||||
// allowed cardinality of statements for a given property type in a meta-data schema
|
||||
public const METADATA_PROPERTY_CARDINALITY_ONE = 1;
|
||||
public const METADATA_PROPERTY_CARDINALITY_MANY = 2;
|
||||
|
||||
/** @var string property name */
|
||||
public $_name;
|
||||
|
||||
/** @var string a translation id */
|
||||
public $_displayName;
|
||||
|
||||
/** @var int the resource types that can be described with this property */
|
||||
public $_assocTypes;
|
||||
|
||||
/** @var array allowed property types */
|
||||
public $_allowedTypes;
|
||||
|
||||
/** @var bool flag that defines whether the property can be translated */
|
||||
public $_translated;
|
||||
|
||||
/** @var int property cardinality */
|
||||
public $_cardinality;
|
||||
|
||||
/** @var string validation message */
|
||||
public $_validationMessage;
|
||||
|
||||
/** @var bool */
|
||||
public $_mandatory;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $name the unique name of the property within a meta-data schema (can be a property URI)
|
||||
* @param array $assocTypes an array of integers that define the application entities that can
|
||||
* be described with this property.
|
||||
* @param mixed $allowedTypes must be a scalar or an array with the supported types, default: METADATA_PROPERTY_TYPE_STRING
|
||||
* @param bool $translated whether the property may have various language versions, default: false
|
||||
* @param int $cardinality must be on of the supported cardinalities, default: METADATA_PROPERTY_CARDINALITY_ONE
|
||||
* @param string $displayName
|
||||
* @param string $validationMessage A string that can be displayed in case a user tries to set an invalid value for this property.
|
||||
* @param bool $mandatory Is this a mandatory property within the schema?
|
||||
*/
|
||||
public function __construct(
|
||||
$name,
|
||||
$assocTypes = [],
|
||||
$allowedTypes = self::METADATA_PROPERTY_TYPE_STRING,
|
||||
$translated = false,
|
||||
$cardinality = self::METADATA_PROPERTY_CARDINALITY_ONE,
|
||||
$displayName = null,
|
||||
$validationMessage = null,
|
||||
$mandatory = false
|
||||
) {
|
||||
// Validate name and assoc type array
|
||||
if (!is_string($name)) {
|
||||
throw new InvalidArgumentException('$name should be a string.');
|
||||
}
|
||||
if (!is_array($assocTypes)) {
|
||||
throw new InvalidArgumentException('$assocTypes should be an array.');
|
||||
}
|
||||
|
||||
// A single type will be transformed to an
|
||||
// array of types so that we can handle them
|
||||
// uniformly.
|
||||
if (is_scalar($allowedTypes) || count($allowedTypes) == 1) {
|
||||
$allowedTypes = [$allowedTypes];
|
||||
}
|
||||
|
||||
// Validate types
|
||||
$canonicalizedTypes = [];
|
||||
foreach ($allowedTypes as $allowedType) {
|
||||
if (is_array($allowedType)) {
|
||||
// We expect an array with a single entry
|
||||
// of the form "type => additional parameter".
|
||||
assert(count($allowedType) == 1);
|
||||
// Reset the array, just in case...
|
||||
reset($allowedType);
|
||||
// Extract the type and the additional parameter
|
||||
$allowedTypeId = key($allowedType);
|
||||
$allowedTypeParam = current($allowedType);
|
||||
} else {
|
||||
// No additional parameter has been set.
|
||||
$allowedTypeId = $allowedType;
|
||||
$allowedTypeParam = null;
|
||||
}
|
||||
|
||||
// Validate type
|
||||
if (!in_array($allowedTypeId, MetadataProperty::getSupportedTypes())) {
|
||||
throw new InvalidArgumentException('Allowed types must be supported types!');
|
||||
}
|
||||
|
||||
// Transform the type array in a
|
||||
// structure that is easy to handle
|
||||
// in for loops.
|
||||
$canonicalizedTypes[$allowedTypeId][] = $allowedTypeParam;
|
||||
|
||||
// Validate additional type parameter.
|
||||
switch ($allowedTypeId) {
|
||||
case self::METADATA_PROPERTY_TYPE_COMPOSITE:
|
||||
// Validate the assoc id of the composite.
|
||||
if (!is_integer($allowedTypeParam)) {
|
||||
throw new InvalidArgumentException('Allowed type parameter should be an integer.');
|
||||
}
|
||||
// Properties that allow composite types cannot be translated.
|
||||
if ($translated) {
|
||||
throw new InvalidArgumentException('Properties that allow composite types cannot be translated.');
|
||||
}
|
||||
break;
|
||||
|
||||
case self::METADATA_PROPERTY_TYPE_VOCABULARY:
|
||||
// Validate the symbolic name of the vocabulary.
|
||||
if (!is_string($allowedTypeParam)) {
|
||||
throw new InvalidArgumentException('Allowed type parameter should be a string.');
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// No other types support an additional parameter
|
||||
if (!is_null($allowedTypeParam)) {
|
||||
throw new InvalidArgumentException('An additional parameter was supplied for an unsupported metadata property type.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate translation and cardinality
|
||||
if (!is_bool($translated)) {
|
||||
throw new InvalidArgumentException('$translated must be a boolean');
|
||||
}
|
||||
if (!in_array($cardinality, MetadataProperty::getSupportedCardinalities())) {
|
||||
throw new InvalidArgumentException('$cardinality must be a supported cardinality.');
|
||||
}
|
||||
|
||||
// Default display name
|
||||
if (is_null($displayName)) {
|
||||
$displayName = 'metadata.property.displayName.' . $name;
|
||||
}
|
||||
if (!is_string($displayName)) {
|
||||
throw new InvalidArgumentException('$displayName must be a string.');
|
||||
}
|
||||
|
||||
// Default validation message
|
||||
if (is_null($validationMessage)) {
|
||||
$validationMessage = 'metadata.property.validationMessage.' . $name;
|
||||
}
|
||||
if (!is_string($validationMessage)) {
|
||||
throw new InvalidArgumentException('$validationMessage must be a string.');
|
||||
}
|
||||
|
||||
|
||||
// Initialize the class
|
||||
$this->_name = (string)$name;
|
||||
$this->_assocTypes = & $assocTypes;
|
||||
$this->_allowedTypes = & $canonicalizedTypes;
|
||||
$this->_translated = (bool)$translated;
|
||||
$this->_cardinality = (int)$cardinality;
|
||||
$this->_displayName = (string)$displayName;
|
||||
$this->_validationMessage = (string)$validationMessage;
|
||||
$this->_mandatory = (bool)$mandatory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a canonical form of the property
|
||||
* name ready to be used as a property id in an
|
||||
* external context (e.g. Forms or Templates).
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
// Replace special characters in XPath-like names
|
||||
// as 'person-group[@person-group-type="author"]'.
|
||||
$from = [
|
||||
'[', ']', '@', '"', '='
|
||||
];
|
||||
$to = [
|
||||
'-', '', '', '', '-'
|
||||
];
|
||||
$propertyId = trim(str_replace($from, $to, $this->getName()), '-');
|
||||
$propertyId = PKPString::camelize($propertyId);
|
||||
return $propertyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the translation id representing
|
||||
* the display name of the property.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDisplayName()
|
||||
{
|
||||
return $this->_displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the allowed association types
|
||||
* (resources that can be described
|
||||
* with this property)
|
||||
*
|
||||
* @return array a list of integers representing
|
||||
* association types.
|
||||
*/
|
||||
public function &getAssocTypes()
|
||||
{
|
||||
return $this->_assocTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the allowed type
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getAllowedTypes()
|
||||
{
|
||||
return $this->_allowedTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this property translated?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getTranslated()
|
||||
{
|
||||
return $this->_translated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cardinality
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getCardinality()
|
||||
{
|
||||
return $this->_cardinality;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation message
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getValidationMessage()
|
||||
{
|
||||
return $this->_validationMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this property mandatory?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getMandatory()
|
||||
{
|
||||
return $this->_mandatory;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Public methods
|
||||
//
|
||||
/**
|
||||
* Validate a given input against the property specification
|
||||
*
|
||||
* The given value must validate against at least one of the
|
||||
* allowed types. The first allowed type id will be returned as
|
||||
* validation result. If the given value fits none of the allowed
|
||||
* types, then we'll return 'false'.
|
||||
*
|
||||
* @param mixed $value the input to be validated
|
||||
* @param string $locale the locale to be used for validation
|
||||
*
|
||||
* @return array|boolean an array with a single entry of the format
|
||||
* "type => additional type parameter" against which the value
|
||||
* validated or boolean false if not validated at all.
|
||||
*/
|
||||
public function isValid($value, $locale = null)
|
||||
{
|
||||
// We never accept null values or arrays.
|
||||
if (is_null($value) || is_array($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Translate the locale.
|
||||
if (is_null($locale)) {
|
||||
$locale = '';
|
||||
}
|
||||
|
||||
// MetadataProperty::getSupportedTypes() returns an ordered
|
||||
// list of possible meta-data types with the most specific
|
||||
// type coming first so that we always correctly identify
|
||||
// specializations (e.g. a date is a specialized string).
|
||||
$allowedTypes = $this->getAllowedTypes();
|
||||
foreach (MetadataProperty::getSupportedTypes() as $testedType) {
|
||||
if (isset($allowedTypes[$testedType])) {
|
||||
foreach ($allowedTypes[$testedType] as $allowedTypeParam) {
|
||||
// Type specific validation
|
||||
switch ($testedType) {
|
||||
case self::METADATA_PROPERTY_TYPE_COMPOSITE:
|
||||
// Composites can either be represented by a meta-data description
|
||||
// or by a string of the form AssocType:AssocId if the composite
|
||||
// has already been persisted in the database.
|
||||
switch (true) {
|
||||
// Test for MetadataDescription format
|
||||
case $value instanceof \PKP\metadata\MetadataDescription:
|
||||
$assocType = $value->getAssocType();
|
||||
break;
|
||||
|
||||
// Test for AssocType:AssocId format
|
||||
case is_string($value):
|
||||
$valueParts = explode(':', $value);
|
||||
if (count($valueParts) != 2) {
|
||||
break 2;
|
||||
} // break the outer switch
|
||||
[$assocType, $assocId] = $valueParts;
|
||||
if (!(is_numeric($assocType) && is_numeric($assocId))) {
|
||||
break 2;
|
||||
} // break the outer switch
|
||||
$assocType = (int)$assocType;
|
||||
break;
|
||||
|
||||
default:
|
||||
// None of the allowed types
|
||||
break;
|
||||
}
|
||||
|
||||
// Check that the association type matches
|
||||
// with the allowed association type (which
|
||||
// is configured as an additional type parameter).
|
||||
if (isset($assocType) && $assocType === $allowedTypeParam) {
|
||||
return [self::METADATA_PROPERTY_TYPE_COMPOSITE => $assocType];
|
||||
}
|
||||
break;
|
||||
|
||||
case self::METADATA_PROPERTY_TYPE_VOCABULARY:
|
||||
// Interpret the type parameter of this type like this:
|
||||
// symbolic[:assoc-type:assoc-id]. If no assoc type/id are
|
||||
// given then we assume :0:0 to represent site-wide vocabs.
|
||||
$vocabNameParts = explode(':', $allowedTypeParam);
|
||||
$vocabNamePartsCount = count($vocabNameParts);
|
||||
switch ($vocabNamePartsCount) {
|
||||
case 1:
|
||||
// assume a site-wide vocabulary
|
||||
$symbolic = $allowedTypeParam;
|
||||
$assocType = $assocId = 0;
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// assume a context-specific vocabulary
|
||||
[$symbolic, $assocType, $assocId] = $vocabNameParts;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Invalid configuration
|
||||
assert(false);
|
||||
}
|
||||
|
||||
if (is_string($value)) {
|
||||
// Try to translate the string value into a controlled vocab entry
|
||||
$controlledVocabEntryDao = DAORegistry::getDAO('ControlledVocabEntryDAO'); /** @var ControlledVocabEntryDAO $controlledVocabEntryDao */
|
||||
if (!is_null($controlledVocabEntryDao->getBySetting($value, $symbolic, $assocType, $assocId, 'name', $locale))) {
|
||||
// The string was successfully translated so mark it as "valid".
|
||||
return [self::METADATA_PROPERTY_TYPE_VOCABULARY => $allowedTypeParam];
|
||||
}
|
||||
}
|
||||
|
||||
if (is_integer($value)) {
|
||||
// Validate with controlled vocabulary validator
|
||||
$validator = new ValidatorControlledVocab($symbolic, $assocType, $assocId);
|
||||
if ($validator->isValid($value)) {
|
||||
return [self::METADATA_PROPERTY_TYPE_VOCABULARY => $allowedTypeParam];
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case self::METADATA_PROPERTY_TYPE_URI:
|
||||
$validator = ValidatorFactory::make(
|
||||
['uri' => $value],
|
||||
['uri' => 'url']
|
||||
);
|
||||
if (!$validator->fails()) {
|
||||
return [self::METADATA_PROPERTY_TYPE_URI => null];
|
||||
}
|
||||
break;
|
||||
|
||||
case self::METADATA_PROPERTY_TYPE_DATE:
|
||||
// We allow the following patterns:
|
||||
// YYYY-MM-DD, YYYY-MM and YYYY
|
||||
$datePattern = '/^[0-9]{4}(-[0-9]{2}(-[0-9]{2})?)?$/';
|
||||
if (!preg_match($datePattern, $value)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Check whether the given string is really a valid date
|
||||
$dateParts = explode('-', $value);
|
||||
// Set the day and/or month to 1 if not set
|
||||
$dateParts = array_pad($dateParts, 3, 1);
|
||||
// Extract the date parts
|
||||
[$year, $month, $day] = $dateParts;
|
||||
// Validate the date (only leap days will pass unnoticed ;-) )
|
||||
// Who invented this argument order?
|
||||
if (checkdate($month, $day, $year)) {
|
||||
return [self::METADATA_PROPERTY_TYPE_DATE => null];
|
||||
}
|
||||
break;
|
||||
|
||||
case self::METADATA_PROPERTY_TYPE_INTEGER:
|
||||
if (is_integer($value)) {
|
||||
return [self::METADATA_PROPERTY_TYPE_INTEGER => null];
|
||||
}
|
||||
break;
|
||||
|
||||
case self::METADATA_PROPERTY_TYPE_STRING:
|
||||
if (is_string($value)) {
|
||||
return [self::METADATA_PROPERTY_TYPE_STRING => null];
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown type. As we validate type in the setter, this
|
||||
// should be unreachable code.
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return false if the value didn't validate against any
|
||||
// of the allowed types.
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
// Public static methods
|
||||
//
|
||||
/**
|
||||
* Return supported meta-data property types
|
||||
*
|
||||
* NB: These types are sorted from most specific to
|
||||
* most general and will be validated in this order
|
||||
* so that we'll always identify more specific types
|
||||
* as such (see MetadataProperty::isValid() for more
|
||||
* details).
|
||||
*
|
||||
* @return array supported meta-data property types
|
||||
*/
|
||||
public static function getSupportedTypes()
|
||||
{
|
||||
static $_supportedTypes = [
|
||||
self::METADATA_PROPERTY_TYPE_COMPOSITE,
|
||||
self::METADATA_PROPERTY_TYPE_VOCABULARY,
|
||||
self::METADATA_PROPERTY_TYPE_URI,
|
||||
self::METADATA_PROPERTY_TYPE_DATE,
|
||||
self::METADATA_PROPERTY_TYPE_INTEGER,
|
||||
self::METADATA_PROPERTY_TYPE_STRING
|
||||
];
|
||||
return $_supportedTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return supported cardinalities
|
||||
*
|
||||
* @return array supported cardinalities
|
||||
*/
|
||||
public static function getSupportedCardinalities()
|
||||
{
|
||||
static $_supportedCardinalities = [
|
||||
self::METADATA_PROPERTY_CARDINALITY_ONE,
|
||||
self::METADATA_PROPERTY_CARDINALITY_MANY
|
||||
];
|
||||
return $_supportedCardinalities;
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\metadata\MetadataProperty', '\MetadataProperty');
|
||||
foreach ([
|
||||
'METADATA_PROPERTY_TYPE_STRING',
|
||||
'METADATA_PROPERTY_TYPE_DATE',
|
||||
'METADATA_PROPERTY_TYPE_INTEGER',
|
||||
'METADATA_PROPERTY_TYPE_VOCABULARY',
|
||||
'METADATA_PROPERTY_TYPE_URI',
|
||||
'METADATA_PROPERTY_TYPE_COMPOSITE',
|
||||
'METADATA_PROPERTY_CARDINALITY_ONE',
|
||||
'METADATA_PROPERTY_CARDINALITY_MANY',
|
||||
] as $constantName) {
|
||||
define($constantName, constant('\MetadataProperty::' . $constantName));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user