<?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));
}
}
|