664 lines
22 KiB
PHP
664 lines
22 KiB
PHP
<?php
|
|
|
|
/**
|
|
* @file classes/metadata/MetadataDescription.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 MetadataDescription
|
|
*
|
|
* @ingroup metadata
|
|
*
|
|
* @see MetadataProperty
|
|
* @see MetadataRecord
|
|
* @see MetadataSchema
|
|
*
|
|
* @brief Class modeling a description (DCMI abstract model) or subject-
|
|
* predicate-object graph (RDF). This class and its children provide
|
|
* meta-data (DCMI abstract model: statements of property-value pairs,
|
|
* RDF: assertions of predicate-object pairs) about a given PKP application
|
|
* entity instance (DCMI abstract model: described resource, RDF: subject).
|
|
*
|
|
* This class has primarily been designed to describe journals, journal
|
|
* issues, articles, conferences, conference proceedings (conference papers),
|
|
* monographs (books), monograph components (book chapters) or citations.
|
|
*
|
|
* It is, however, flexible enough to be extended to describe any
|
|
* application entity in the future. Descriptions can be retrieved from
|
|
* any application object that implements the MetadataProvider interface.
|
|
*
|
|
* Special attention has been paid to the compatibility of the class
|
|
* implementation with the implementation of several meta-data standards
|
|
* that we consider especially relevant to our use cases.
|
|
*
|
|
* We distinguish two main use cases for meta-data: discovery and delivery
|
|
* of described resources. We have chosen the element-citation tag from the
|
|
* NLM standard <http://dtd.nlm.nih.gov/publishing/tag-library/3.0/n-8xa0.html>
|
|
* as our primary representation of delivery meta-data and dcterms
|
|
* <http://dublincore.org/documents/dcmi-terms/> as our primary
|
|
* representation of discovery meta-data.
|
|
*
|
|
* Our specific use of meta-data has important implications and determines
|
|
* our design goals:
|
|
* * Neither NLM-citation nor dcterms have been designed with an object
|
|
* oriented encoding in mind. NLM-citation is usually XML encoded
|
|
* while typical dcterms encodings are HTML meta-tags, RDF or XML.
|
|
* * We believe that trying to implement a super-set of meta-data
|
|
* standards ("least common denominator" or super-schema approach)
|
|
* is fundamentally flawed as meta-data standards are always
|
|
* developed with specific use-cases in mind that require potentially
|
|
* incompatible data properties or encodings.
|
|
* * Although we think that NLM-citation and dcterms are sensible default
|
|
* meta-data schemes our design should remain flexible enough for
|
|
* users to implement and use other schemes as an internal meta-data
|
|
* standard.
|
|
* * We have to make sure that we can easily extract/inject meta-data
|
|
* from/to PKP application objects.
|
|
* * We have to avoid code duplication to keep maintenance cost under
|
|
* control.
|
|
* * We have to minimize the "impedance mismatch" between our own
|
|
* object oriented encoding and fully standard compliant external
|
|
* encodings (i.e. XML, RDF, HTML meta-tags, ...) to allow for easy
|
|
* conversion between encodings.
|
|
* * We have to make sure that we can switch between internal and
|
|
* external encodings without any data loss.
|
|
* * We have to make sure that crosswalks to and from other important
|
|
* meta-data standards (e.g. OpenURL variants, MODS, MARC) can be
|
|
* performed in a well-defined and easy way while minimizing data
|
|
* loss.
|
|
* * We have to make sure that we can support qualified fields (e.g.
|
|
* qualified DC).
|
|
* * We have to make sure that we can support RDF triples.
|
|
*
|
|
* We took the following design decisions to achieve these goals:
|
|
* * We only implement properties that are justified by strong real-world
|
|
* use-cases. We recognize that the limiting factor is not the data that
|
|
* we could represent but the data we actually have. This is not determined
|
|
* by the chosen standard but by the PKP application objects we want to
|
|
* represent. Additional meta-data properties/predicates can be added as
|
|
* required.
|
|
* * We do adapt data structures as long as we can make sure that a
|
|
* fully standard compliant encoding can always be re-constructed. This
|
|
* is especially true for NLM-citation which is designed with
|
|
* XML in mind and therefore uses hierarchical constructs that are
|
|
* difficult to represent in an OO class model.
|
|
* This means that our meta-data framework only supports (nested) key/
|
|
* value-based schemas which can however be converted to hierarchical
|
|
* representations.
|
|
* * We borrow class and property names from the DCMI abstract model as
|
|
* the terms used there provide better readability for developers less
|
|
* acquainted with formal model theory. We'll, however, make sure that
|
|
* data can easily be RDF encoded within our data model.
|
|
* * Data validation must ensure that meta-data always complies with a
|
|
* specific meta-data standard. As we are speaking about an object
|
|
* oriented encoding that is not defined in the original standard, we
|
|
* define compliance as "roundtripability". This means we must be able
|
|
* to convert our object oriented data encoding to a fully standard
|
|
* compliant encoding and back without any data loss.
|
|
*/
|
|
|
|
namespace PKP\metadata;
|
|
|
|
use APP\core\Application;
|
|
use PKP\facades\Locale;
|
|
|
|
class MetadataDescription extends \PKP\core\DataObject
|
|
{
|
|
public const METADATA_DESCRIPTION_REPLACE_ALL = 1;
|
|
public const METADATA_DESCRIPTION_REPLACE_PROPERTIES = 2;
|
|
public const METADATA_DESCRIPTION_REPLACE_NOTHING = 3;
|
|
|
|
public const METADATA_DESCRIPTION_UNKNOWN_LOCALE = 'unknown';
|
|
|
|
/** @var string fully qualified class name of the meta-data schema this description complies to */
|
|
public $_metadataSchemaName;
|
|
|
|
/** @var MetadataSchema the schema this description complies to */
|
|
public $_metadataSchema;
|
|
|
|
/** @var int association type (the type of the described resource) */
|
|
public $_assocType;
|
|
|
|
/** @var int association id (the identifier of the described resource) */
|
|
public $_assocId;
|
|
|
|
/**
|
|
* @var string an (optional) display name that describes the contents
|
|
* of this meta-data description to the end user.
|
|
*/
|
|
public $_displayName;
|
|
|
|
/**
|
|
* @var int sequence id used when saving several descriptions
|
|
* of the same subject.
|
|
*/
|
|
public $_seq;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct($metadataSchemaName, $assocType)
|
|
{
|
|
assert(is_string($metadataSchemaName) && is_integer($assocType));
|
|
parent::__construct();
|
|
$this->_metadataSchemaName = $metadataSchemaName;
|
|
$this->_assocType = $assocType;
|
|
}
|
|
|
|
//
|
|
// Get/set methods
|
|
//
|
|
/**
|
|
* Get the fully qualified class name of
|
|
* the supported meta-data schema.
|
|
*/
|
|
public function getMetadataSchemaName()
|
|
{
|
|
return $this->_metadataSchemaName;
|
|
}
|
|
|
|
/**
|
|
* Get the metadata schema
|
|
*
|
|
* @return MetadataSchema
|
|
*/
|
|
public function &getMetadataSchema()
|
|
{
|
|
// Lazy-load the meta-data schema if this has
|
|
// not been done before.
|
|
if (is_null($this->_metadataSchema)) {
|
|
$metadataSchemaName = $this->getMetadataSchemaName();
|
|
if (preg_match('/^[a-zA-Z0-9_.]+$/', $metadataSchemaName)) {
|
|
// DEPRECATED as of 3.4.0: non-PSR classloading pkp/pkp-lib#8186
|
|
$this->_metadataSchema = & instantiate($metadataSchemaName, \PKP\metadata\MetadataSchema::class);
|
|
} elseif (class_exists($metadataSchemaName)) {
|
|
$this->_metadataSchema = new $metadataSchemaName();
|
|
}
|
|
if (! $this->_metadataSchema instanceof \PKP\metadata\MetadataSchema) {
|
|
throw new \Exception('Unexpected metadata schema class!');
|
|
}
|
|
}
|
|
return $this->_metadataSchema;
|
|
}
|
|
|
|
/**
|
|
* Get the association type (described resource type)
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getAssocType()
|
|
{
|
|
return $this->_assocType;
|
|
}
|
|
|
|
/**
|
|
* Get the association id (described resource identifier)
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getAssocId()
|
|
{
|
|
return $this->_assocId;
|
|
}
|
|
|
|
/**
|
|
* Set the association id (described resource identifier)
|
|
*
|
|
* @param int $assocId
|
|
*/
|
|
public function setAssocId($assocId)
|
|
{
|
|
$this->_assocId = $assocId;
|
|
}
|
|
|
|
/**
|
|
* Construct a meta-data application entity id
|
|
* (described resource id / subject id) for
|
|
* this meta-data description object.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getAssoc()
|
|
{
|
|
$assocType = $this->getAssocType();
|
|
$assocId = $this->getAssocId();
|
|
assert(isset($assocType) && isset($assocId));
|
|
return $assocType . ':' . $assocId;
|
|
}
|
|
|
|
/**
|
|
* Set the (optional) display name
|
|
*
|
|
* @param string $displayName
|
|
*/
|
|
public function setDisplayName($displayName)
|
|
{
|
|
$this->_displayName = $displayName;
|
|
}
|
|
|
|
/**
|
|
* Get the (optional) display name
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getDisplayName()
|
|
{
|
|
return $this->_displayName;
|
|
}
|
|
|
|
/**
|
|
* Set the sequence id
|
|
*
|
|
* @param int $seq
|
|
*/
|
|
public function setSequence($seq)
|
|
{
|
|
$this->_seq = $seq;
|
|
}
|
|
|
|
/**
|
|
* Get the sequence id
|
|
*
|
|
* @return int
|
|
*/
|
|
public function getSequence()
|
|
{
|
|
return $this->_seq;
|
|
}
|
|
|
|
/**
|
|
* Add a meta-data statement. Statements can only be added
|
|
* for properties that are part of the meta-data schema. This
|
|
* method will also check the validity of the value for the
|
|
* given property before adding the statement.
|
|
*
|
|
* @param string $propertyName The name of the property
|
|
* @param mixed $value The value to be assigned to the property
|
|
* @param string $locale
|
|
* @param bool $replace whether to replace an existing statement
|
|
*
|
|
* @return bool true if a valid statement was added, otherwise false
|
|
*/
|
|
public function addStatement($propertyName, $value, $locale = null, $replace = false)
|
|
{
|
|
// Check the property
|
|
$property = & $this->getProperty($propertyName);
|
|
if (is_null($property)) {
|
|
return false;
|
|
}
|
|
assert($property instanceof \PKP\metadata\MetadataProperty);
|
|
|
|
// Check that the property is allowed for the described resource
|
|
if (!in_array($this->_assocType, $property->getAssocTypes())) {
|
|
return false;
|
|
}
|
|
|
|
// Handle translation
|
|
$translated = $property->getTranslated();
|
|
if (isset($locale) && !$translated) {
|
|
return false;
|
|
}
|
|
if (!isset($locale) && $translated) {
|
|
// Retrieve the current locale
|
|
$locale = Locale::getLocale();
|
|
}
|
|
|
|
// Check that the value is compliant with the property specification
|
|
if ($property->isValid($value, $locale) === false) {
|
|
return false;
|
|
}
|
|
|
|
// Handle cardinality
|
|
$existingValue = & $this->getStatement($propertyName, $locale);
|
|
switch ($property->getCardinality()) {
|
|
case MetadataProperty::METADATA_PROPERTY_CARDINALITY_ONE:
|
|
if (isset($existingValue) && !$replace) {
|
|
return false;
|
|
}
|
|
$newValue = $value;
|
|
break;
|
|
|
|
case MetadataProperty::METADATA_PROPERTY_CARDINALITY_MANY:
|
|
if (isset($existingValue) && !$replace) {
|
|
assert(is_array($existingValue));
|
|
$newValue = $existingValue;
|
|
array_push($newValue, $value);
|
|
} else {
|
|
$newValue = [$value];
|
|
}
|
|
break;
|
|
|
|
default:
|
|
assert(false);
|
|
}
|
|
|
|
// Add the value
|
|
$this->setData($propertyName, $newValue, $locale);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Remove statement. If the property has cardinality 'many'
|
|
* then all statements for the property will be removed at once.
|
|
* If the property is translated and the locale is null then
|
|
* the statements for all locales will be removed.
|
|
*
|
|
* @param string $propertyName
|
|
* @param string $locale
|
|
*
|
|
* @return bool true if the statement was found and removed, otherwise false
|
|
*/
|
|
public function removeStatement($propertyName, $locale = null)
|
|
{
|
|
// Remove the statement if it exists
|
|
if (isset($propertyName) && $this->hasData($propertyName, $locale)) {
|
|
$this->setData($propertyName, null, $locale);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Get all statements
|
|
*
|
|
* @return array statements
|
|
*/
|
|
public function &getStatements()
|
|
{
|
|
// Do not retrieve the data by-ref
|
|
// otherwise the following unset()
|
|
// will change internal state.
|
|
$allData = $this->getAllData();
|
|
|
|
// Unset data variables that are not statements
|
|
unset($allData['id']);
|
|
return $allData;
|
|
}
|
|
|
|
/**
|
|
* Get a specific statement
|
|
*
|
|
* @param string $propertyName
|
|
* @param string $locale
|
|
*
|
|
* @return mixed a scalar property value or an array of property values
|
|
* if the cardinality of the property is 'many'.
|
|
*/
|
|
public function &getStatement($propertyName, $locale = null)
|
|
{
|
|
// Check the property
|
|
$property = & $this->getProperty($propertyName);
|
|
assert(isset($property) && $property instanceof \PKP\metadata\MetadataProperty);
|
|
|
|
// Handle translation
|
|
$translated = $property->getTranslated();
|
|
if (!$translated) {
|
|
assert(is_null($locale));
|
|
}
|
|
if ($translated && !isset($locale)) {
|
|
// Retrieve the current locale
|
|
$locale = Locale::getLocale();
|
|
}
|
|
|
|
// Retrieve the value
|
|
return $this->getData($propertyName, $locale);
|
|
}
|
|
|
|
/**
|
|
* Returns all translations of a translated property
|
|
*
|
|
* @param string $propertyName
|
|
*
|
|
* @return array all translations of a given property; if the
|
|
* property has cardinality "many" then this returns a two-dimensional
|
|
* array whereby the first key represents the locale and the second
|
|
* the translated values.
|
|
*/
|
|
public function &getStatementTranslations($propertyName)
|
|
{
|
|
assert($this->isTranslatedProperty($propertyName));
|
|
return $this->getData($propertyName);
|
|
}
|
|
|
|
/**
|
|
* Add several statements at once. If one of the statements
|
|
* is invalid then the meta-data description will remain in its
|
|
* initial state.
|
|
* * Properties with a cardinality of 'many' must be passed in as
|
|
* sub-arrays.
|
|
* * Translated properties with a cardinality of 'one' must be
|
|
* passed in as sub-arrays with the locale as a key.
|
|
* * Translated properties with a cardinality of 'many' must be
|
|
* passed in as sub-sub-arrays with the locale as the first key.
|
|
*
|
|
* @param array $statements statements
|
|
* @param int $replace one of the allowed replace levels.
|
|
*
|
|
* @return bool true if all statements could be added, false otherwise
|
|
*/
|
|
public function setStatements(&$statements, $replace = self::METADATA_DESCRIPTION_REPLACE_PROPERTIES)
|
|
{
|
|
assert(in_array($replace, $this->_allowedReplaceLevels()));
|
|
|
|
// Make a backup copy of all existing statements.
|
|
$statementsBackup = $this->getAllData();
|
|
|
|
if ($replace == self::METADATA_DESCRIPTION_REPLACE_ALL) {
|
|
// Delete existing statements
|
|
$emptyArray = [];
|
|
$this->setAllData($emptyArray);
|
|
}
|
|
|
|
// Add statements one by one to detect invalid values.
|
|
foreach ($statements as $propertyName => $content) {
|
|
assert(!empty($content));
|
|
|
|
// Transform scalars or translated fields to arrays so that
|
|
// we can handle properties with different cardinalities in
|
|
// the same way.
|
|
if (is_scalar($content) || is_string(key($content))) {
|
|
$values = [&$content];
|
|
} else {
|
|
$values = & $content;
|
|
}
|
|
|
|
if ($replace == self::METADATA_DESCRIPTION_REPLACE_PROPERTIES) {
|
|
$replaceProperty = true;
|
|
} else {
|
|
$replaceProperty = false;
|
|
}
|
|
|
|
$valueIndex = 0;
|
|
foreach ($values as $value) {
|
|
$firstValue = ($valueIndex == 0) ? true : false;
|
|
// Is this a translated property?
|
|
if (is_array($value)) {
|
|
foreach ($value as $locale => $translation) {
|
|
// Handle cardinality many and one in the same way
|
|
if (is_scalar($translation)) {
|
|
$translationValues = [&$translation];
|
|
} else {
|
|
$translationValues = & $translation;
|
|
}
|
|
$translationIndex = 0;
|
|
foreach ($translationValues as $translationValue) {
|
|
$firstTranslation = ($translationIndex == 0) ? true : false;
|
|
// Add a statement (replace existing statement if any)
|
|
if (!($this->addStatement($propertyName, $translationValue, $locale, $firstTranslation && $replaceProperty))) {
|
|
$this->setAllData($statementsBackup);
|
|
return false;
|
|
}
|
|
unset($translationValue);
|
|
$translationIndex++;
|
|
}
|
|
unset($translationValues);
|
|
}
|
|
unset($translation);
|
|
} else {
|
|
// Add a statement (replace existing statement if any)
|
|
if (!($this->addStatement($propertyName, $value, null, $firstValue && $replaceProperty))) {
|
|
$this->setAllData($statementsBackup);
|
|
return false;
|
|
}
|
|
}
|
|
unset($value);
|
|
$valueIndex++;
|
|
}
|
|
unset($values);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Convenience method that returns the properties of
|
|
* the underlying meta-data schema.
|
|
*
|
|
* @return array an array of MetadataProperties
|
|
*/
|
|
public function &getProperties()
|
|
{
|
|
$metadataSchema = & $this->getMetadataSchema();
|
|
return $metadataSchema->getProperties();
|
|
}
|
|
|
|
/**
|
|
* Convenience method that returns a property from
|
|
* the underlying meta-data schema.
|
|
*
|
|
* @param string $propertyName
|
|
*
|
|
* @return MetadataProperty
|
|
*/
|
|
public function &getProperty($propertyName)
|
|
{
|
|
$metadataSchema = & $this->getMetadataSchema();
|
|
return $metadataSchema->getProperty($propertyName);
|
|
}
|
|
|
|
/**
|
|
* Convenience method that returns a property id
|
|
* the underlying meta-data schema.
|
|
*
|
|
* @param string $propertyName
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getNamespacedPropertyId($propertyName)
|
|
{
|
|
$metadataSchema = & $this->getMetadataSchema();
|
|
return $metadataSchema->getNamespacedPropertyId($propertyName);
|
|
}
|
|
|
|
/**
|
|
* Convenience method that returns the valid
|
|
* property names of the underlying meta-data schema.
|
|
*
|
|
* @return array an array of string values representing valid property names
|
|
*/
|
|
public function getPropertyNames()
|
|
{
|
|
$metadataSchema = & $this->getMetadataSchema();
|
|
return $metadataSchema->getPropertyNames();
|
|
}
|
|
|
|
/**
|
|
* Convenience method that returns the names of properties with a
|
|
* given data type of the underlying meta-data schema.
|
|
*
|
|
* @param string $propertyType
|
|
*
|
|
* @return array an array of string values representing valid property names
|
|
*/
|
|
public function getPropertyNamesByType($propertyType)
|
|
{
|
|
$metadataSchema = & $this->getMetadataSchema();
|
|
return $metadataSchema->getPropertyNamesByType($propertyType);
|
|
}
|
|
|
|
/**
|
|
* Returns an array of property names for
|
|
* which statements exist.
|
|
*
|
|
* @return array an array of string values representing valid property names
|
|
*/
|
|
public function getSetPropertyNames()
|
|
{
|
|
return array_keys($this->getStatements());
|
|
}
|
|
|
|
/**
|
|
* Convenience method that checks the existence
|
|
* of a property in the underlying meta-data schema.
|
|
*
|
|
* @param string $propertyName
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasProperty($propertyName)
|
|
{
|
|
$metadataSchema = & $this->getMetadataSchema();
|
|
return $metadataSchema->hasProperty($propertyName);
|
|
}
|
|
|
|
/**
|
|
* Check the existence of a statement for the given property.
|
|
*
|
|
* @param string $propertyName
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasStatement($propertyName)
|
|
{
|
|
$statements = & $this->getStatements();
|
|
return (isset($statements[$propertyName]));
|
|
}
|
|
|
|
/**
|
|
* Convenience method that checks whether a given property
|
|
* is translated.
|
|
*
|
|
* @param string $propertyName
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function isTranslatedProperty($propertyName)
|
|
{
|
|
$property = $this->getProperty($propertyName);
|
|
assert($property instanceof \PKP\metadata\MetadataProperty);
|
|
return $property->getTranslated();
|
|
}
|
|
|
|
|
|
//
|
|
// Private helper methods
|
|
//
|
|
/**
|
|
* The allowed replace levels for the
|
|
* setStatements() method.
|
|
*/
|
|
public static function _allowedReplaceLevels()
|
|
{
|
|
static $allowedReplaceLevels = [
|
|
self::METADATA_DESCRIPTION_REPLACE_ALL,
|
|
self::METADATA_DESCRIPTION_REPLACE_PROPERTIES,
|
|
self::METADATA_DESCRIPTION_REPLACE_NOTHING
|
|
];
|
|
return $allowedReplaceLevels;
|
|
}
|
|
}
|
|
|
|
if (!PKP_STRICT_MODE) {
|
|
class_alias('\PKP\metadata\MetadataDescription', '\MetadataDescription');
|
|
foreach ([
|
|
'METADATA_DESCRIPTION_REPLACE_ALL',
|
|
'METADATA_DESCRIPTION_REPLACE_PROPERTIES',
|
|
'METADATA_DESCRIPTION_REPLACE_NOTHING',
|
|
'METADATA_DESCRIPTION_UNKNOWN_LOCALE'
|
|
] as $constantName) {
|
|
define($constantName, constant('\MetadataDescription::' . $constantName));
|
|
}
|
|
}
|