first commit
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/metadata/CrosswalkFilter.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 CrosswalkFilter
|
||||
*
|
||||
* @ingroup metadata
|
||||
*
|
||||
* @see MetadataDescription
|
||||
*
|
||||
* @brief Class that provides methods to convert one type of
|
||||
* meta-data description into another. This is an abstract
|
||||
* class that must be sub-classed by specific cross-walk
|
||||
* implementations.
|
||||
*/
|
||||
|
||||
namespace PKP\metadata;
|
||||
|
||||
use PKP\filter\Filter;
|
||||
|
||||
class CrosswalkFilter extends Filter
|
||||
{
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $fromSchema fully qualified class name of supported input meta-data schema
|
||||
* @param string $toSchema fully qualified class name of supported output meta-data schema
|
||||
*/
|
||||
public function __construct($fromSchema, $toSchema)
|
||||
{
|
||||
parent::__construct('metadata::' . $fromSchema . '(*)', 'metadata::' . $toSchema . '(*)');
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\metadata\CrosswalkFilter', '\CrosswalkFilter');
|
||||
}
|
||||
@@ -0,0 +1,492 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/metadata/MetadataDataObjectAdapter.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 MetadataDataObjectAdapter
|
||||
*
|
||||
* @ingroup metadata
|
||||
*
|
||||
* @see DataObject
|
||||
* @see MetadataSchema
|
||||
* @see MetadataDescription
|
||||
*
|
||||
* @brief Class that injects/extracts a meta-data description
|
||||
* into/from an application entity object (DataObject).
|
||||
*
|
||||
* These adapters have to be persistable as they'll be provided
|
||||
* by plug-ins via the filter registry.
|
||||
*/
|
||||
|
||||
namespace PKP\metadata;
|
||||
|
||||
use PKP\core\DataObject;
|
||||
use PKP\filter\ClassTypeDescription;
|
||||
use PKP\filter\FilterGroup;
|
||||
use PKP\filter\PersistableFilter;
|
||||
|
||||
class MetadataDataObjectAdapter extends PersistableFilter
|
||||
{
|
||||
public const METADATA_DOA_INJECTION_MODE = 1;
|
||||
public const METADATA_DOA_EXTRACTION_MODE = 2;
|
||||
|
||||
/** @var int */
|
||||
public $_mode;
|
||||
|
||||
/** @var MetadataSchema */
|
||||
public $_metadataSchema;
|
||||
|
||||
/** @var string */
|
||||
public $_dataObjectClass;
|
||||
|
||||
/** @var array */
|
||||
public $_metadataFieldNames;
|
||||
|
||||
/** @var string */
|
||||
public $_metadataSchemaName;
|
||||
|
||||
/** @var int */
|
||||
public $_assocType;
|
||||
|
||||
/** @var string */
|
||||
public $_dataObjectName;
|
||||
|
||||
/** @var DataObject */
|
||||
public $_targetDataObject;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param FilterGroup $filterGroup
|
||||
* @param null|mixed $mode
|
||||
*/
|
||||
public function __construct($filterGroup, $mode = null)
|
||||
{
|
||||
// Initialize the adapter.
|
||||
parent::__construct($filterGroup);
|
||||
|
||||
// Extract information from the input/output types.
|
||||
|
||||
// Find out whether this filter is injecting or
|
||||
// extracting meta-data.
|
||||
$metadataTypeDescription = null; /** @var MetadataTypeDescription $metadataTypeDescription */
|
||||
$dataObjectTypeDescription = null; /** @var ClassTypeDescription $dataObjectTypeDescription */
|
||||
$inputType = & $this->getInputType();
|
||||
$outputType = & $this->getOutputType();
|
||||
if (is_null($mode)) {
|
||||
if ($inputType instanceof \PKP\metadata\MetadataTypeDescription) {
|
||||
$mode = self::METADATA_DOA_INJECTION_MODE;
|
||||
} else {
|
||||
$mode = self::METADATA_DOA_EXTRACTION_MODE;
|
||||
}
|
||||
}
|
||||
$this->_mode = $mode;
|
||||
|
||||
if ($mode == self::METADATA_DOA_INJECTION_MODE) {
|
||||
// We are in meta-data injection mode (or both input and output are meta-data descriptions).
|
||||
$metadataTypeDescription = & $inputType; /** @var MetadataTypeDescription $metadataTypeDescription */
|
||||
assert($outputType instanceof ClassTypeDescription);
|
||||
$dataObjectTypeDescription = & $outputType; /** @var ClassTypeDescription $dataObjectTypeDescription */
|
||||
} else {
|
||||
// We are in meta-data extraction mode.
|
||||
assert($outputType instanceof \PKP\metadata\MetadataTypeDescription);
|
||||
$metadataTypeDescription = & $outputType;
|
||||
assert($inputType instanceof ClassTypeDescription);
|
||||
$dataObjectTypeDescription = & $inputType;
|
||||
}
|
||||
|
||||
// Extract information from the input/output types.
|
||||
$this->_metadataSchemaName = $metadataTypeDescription->getMetadataSchemaClass();
|
||||
$this->_assocType = $metadataTypeDescription->getAssocType();
|
||||
$this->_dataObjectName = $dataObjectTypeDescription->getTypeName();
|
||||
|
||||
// Set the display name.
|
||||
if ($mode == self::METADATA_DOA_INJECTION_MODE) {
|
||||
$this->setDisplayName('Inject metadata into a(n) ' . $this->getDataObjectClass());
|
||||
} else {
|
||||
$this->setDisplayName('Extract metadata from a(n) ' . $this->getDataObjectClass());
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Getters and setters
|
||||
//
|
||||
/**
|
||||
* One of the METADATA_DOA_*_MODE constants.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getMode()
|
||||
{
|
||||
return $this->_mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully qualified class name of
|
||||
* the supported meta-data schema.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMetadataSchemaName()
|
||||
{
|
||||
return $this->_metadataSchemaName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the supported meta-data schema (lazy load)
|
||||
*
|
||||
* @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();
|
||||
assert(!is_null($metadataSchemaName));
|
||||
if (preg_match('/^[a-zA-Z0-9_.]+$/', $metadataSchemaName)) {
|
||||
// DEPRECATED as of 3.4.0: Schema class names should be fully qualified pkp/pkp-lib#8186
|
||||
$this->_metadataSchema = & instantiate($metadataSchemaName, (string) \PKP\metadata\MetadataSchema::class);
|
||||
} elseif (class_exists($metadataSchemaName)) {
|
||||
$this->_metadataSchema = new $metadataSchemaName();
|
||||
}
|
||||
if (!$this->_metadataSchema instanceof \PKP\metadata\MetadataSchema) {
|
||||
throw new \Exception('Metadata schema is unexpected class!');
|
||||
}
|
||||
assert(is_object($this->_metadataSchema));
|
||||
}
|
||||
return $this->_metadataSchema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method that returns the
|
||||
* meta-data name space.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getMetadataNamespace()
|
||||
{
|
||||
$metadataSchema = & $this->getMetadataSchema();
|
||||
return $metadataSchema->getNamespace();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the supported application entity (class) name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDataObjectName()
|
||||
{
|
||||
return $this->_dataObjectName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the data object class name
|
||||
* (without the package prefix)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDataObjectClass()
|
||||
{
|
||||
if (is_null($this->_dataObjectClass)) {
|
||||
$dataObjectName = $this->getDataObjectName();
|
||||
assert(!is_null($dataObjectName));
|
||||
$dataObjectNameParts = explode('.', $dataObjectName);
|
||||
$this->_dataObjectClass = array_pop($dataObjectNameParts);
|
||||
}
|
||||
return $this->_dataObjectClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the association type corresponding to the data
|
||||
* object type.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getAssocType()
|
||||
{
|
||||
return $this->_assocType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the target data object for meta-data injection.
|
||||
*
|
||||
* @param DataObject $targetDataObject
|
||||
*/
|
||||
public function setTargetDataObject(&$targetDataObject)
|
||||
{
|
||||
$this->_targetDataObject = & $targetDataObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target data object for meta-data injection.
|
||||
*/
|
||||
public function &getTargetDataObject()
|
||||
{
|
||||
return $this->_targetDataObject;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Abstract template methods
|
||||
//
|
||||
/**
|
||||
* Inject a MetadataDescription into the target DataObject
|
||||
*
|
||||
* @param MetadataDescription $metadataDescription
|
||||
* @param DataObject $targetDataObject
|
||||
*
|
||||
* @return DataObject
|
||||
*/
|
||||
public function &injectMetadataIntoDataObject(&$metadataDescription, &$targetDataObject)
|
||||
{
|
||||
// Must be implemented by sub-classes
|
||||
assert(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a MetadataDescription from a source DataObject.
|
||||
*
|
||||
* @param DataObject $sourceDataObject
|
||||
*
|
||||
* @return MetadataDescription
|
||||
*/
|
||||
public function extractMetadataFromDataObject(&$sourceDataObject)
|
||||
{
|
||||
// Must be implemented by sub-classes
|
||||
assert(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the additional field names introduced by the
|
||||
* meta-data schema that need to be persisted in the
|
||||
* ..._settings table corresponding to the DataObject
|
||||
* which is supported by this adapter.
|
||||
* NB: The field names must be prefixed with the meta-data
|
||||
* schema namespace identifier.
|
||||
*
|
||||
* @param bool $translated if true, return localized field
|
||||
* names, otherwise return additional field names.
|
||||
*
|
||||
* @return array an array of field names to be persisted.
|
||||
*/
|
||||
public function getDataObjectMetadataFieldNames($translated = true)
|
||||
{
|
||||
// By default return all field names
|
||||
return $this->getMetadataFieldNames($translated);
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Implement template methods from Filter
|
||||
//
|
||||
/**
|
||||
* Convert a MetadataDescription to an application
|
||||
* object or vice versa.
|
||||
*
|
||||
* @see Filter::process()
|
||||
*
|
||||
* @param mixed $input either a MetadataDescription or an application object
|
||||
*
|
||||
* @return mixed either a MetadataDescription or an application object
|
||||
*/
|
||||
public function &process(&$input)
|
||||
{
|
||||
// Do we inject or extract metadata?
|
||||
switch ($this->getMode()) {
|
||||
case self::METADATA_DOA_INJECTION_MODE:
|
||||
$targetDataObject = & $this->getTargetDataObject();
|
||||
|
||||
// Instantiate a new data object if none was given.
|
||||
if (is_null($targetDataObject)) {
|
||||
$targetDataObject = & $this->instantiateDataObject();
|
||||
assert(is_a($targetDataObject, $this->getDataObjectName()));
|
||||
}
|
||||
|
||||
// Inject meta-data into the data object.
|
||||
$output = & $this->injectMetadataIntoDataObject($input, $targetDataObject);
|
||||
break;
|
||||
|
||||
case self::METADATA_DOA_EXTRACTION_MODE:
|
||||
$output = $this->extractMetadataFromDataObject($input);
|
||||
break;
|
||||
|
||||
default:
|
||||
// Input should be validated by now.
|
||||
assert(false);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Protected helper methods
|
||||
//
|
||||
/**
|
||||
* Instantiate a new data object of the
|
||||
* correct type.
|
||||
*
|
||||
* NB: This can be overridden by sub-classes for more complex
|
||||
* data objects. The standard implementation assumes there are
|
||||
* no constructor args to be set or configurations to be made.
|
||||
*
|
||||
* @return DataObject
|
||||
*/
|
||||
public function &instantiateDataObject()
|
||||
{
|
||||
$dataObjectName = $this->getDataObjectName();
|
||||
assert(!is_null($dataObjectName));
|
||||
$dataObject = & instantiate($dataObjectName, $this->getDataObjectClass());
|
||||
return $dataObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a meta-data description that conforms to the
|
||||
* settings of this adapter.
|
||||
*
|
||||
* @return MetadataDescription
|
||||
*/
|
||||
public function &instantiateMetadataDescription()
|
||||
{
|
||||
$metadataDescription = new MetadataDescription($this->getMetadataSchemaName(), $this->getAssocType());
|
||||
return $metadataDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all field names introduced by the
|
||||
* meta-data schema that might have to be persisted.
|
||||
*
|
||||
* @param bool $translated if true, return localized field
|
||||
* names, otherwise return additional field names.
|
||||
*
|
||||
* @return array an array of field names to be persisted.
|
||||
*/
|
||||
public function getMetadataFieldNames($translated = true)
|
||||
{
|
||||
// Do we need to build the field name cache first?
|
||||
if (is_null($this->_metadataFieldNames)) {
|
||||
// Initialize the cache array
|
||||
$this->_metadataFieldNames = [];
|
||||
|
||||
// Retrieve all properties and add
|
||||
// their names to the cache
|
||||
$metadataSchema = & $this->getMetadataSchema();
|
||||
$metadataSchemaNamespace = $metadataSchema->getNamespace();
|
||||
$properties = & $metadataSchema->getProperties();
|
||||
foreach ($properties as $property) {
|
||||
$propertyAssocTypes = $property->getAssocTypes();
|
||||
if (in_array($this->_assocType, $propertyAssocTypes)) {
|
||||
// Separate translated and non-translated property names
|
||||
// and add the name space so that field names are unique
|
||||
// across various meta-data schemas.
|
||||
$this->_metadataFieldNames[$property->getTranslated()][] = $metadataSchemaNamespace . ':' . $property->getName();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the field names
|
||||
return $this->_metadataFieldNames[$translated];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set several localized statements in a meta-data schema.
|
||||
*
|
||||
* @param MetadataDescription $metadataDescription
|
||||
* @param string $propertyName
|
||||
* @param array $localizedValues (keys: locale, values: localized values)
|
||||
*/
|
||||
public function addLocalizedStatements(&$metadataDescription, $propertyName, $localizedValues)
|
||||
{
|
||||
if (is_array($localizedValues)) {
|
||||
foreach ($localizedValues as $locale => $values) {
|
||||
// Handle cardinality "many" and "one" in the same way.
|
||||
if (is_scalar($values)) {
|
||||
$values = [$values];
|
||||
}
|
||||
foreach ($values as $value) {
|
||||
$metadataDescription->addStatement($propertyName, $value, $locale);
|
||||
unset($value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly inject all fields that are not mapped to the
|
||||
* data object into the data object's data array for
|
||||
* automatic persistence by the meta-data framework.
|
||||
*
|
||||
* @param MetadataDescription $metadataDescription
|
||||
* @param DataObject $dataObject
|
||||
*/
|
||||
public function injectUnmappedDataObjectMetadataFields(&$metadataDescription, &$dataObject)
|
||||
{
|
||||
// Handle translated and non-translated statements separately.
|
||||
foreach ([true, false] as $translated) {
|
||||
// Retrieve the unmapped fields.
|
||||
foreach ($this->getDataObjectMetadataFieldNames($translated) as $unmappedProperty) {
|
||||
// Identify the corresponding property name.
|
||||
[$namespace, $propertyName] = explode(':', $unmappedProperty);
|
||||
|
||||
// Find out whether we have a statement for this unmapped property.
|
||||
if ($metadataDescription->hasStatement($propertyName)) {
|
||||
// Add the unmapped statement directly to the
|
||||
// data object.
|
||||
if ($translated) {
|
||||
$dataObject->setData($unmappedProperty, $metadataDescription->getStatementTranslations($propertyName));
|
||||
} else {
|
||||
$dataObject->setData($unmappedProperty, $metadataDescription->getStatement($propertyName));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Directly extract all fields that are not mapped to the
|
||||
* data object from the data object's data array.
|
||||
*
|
||||
* @param DataObject $dataObject
|
||||
* @param MetadataDescription $metadataDescription
|
||||
*/
|
||||
public function extractUnmappedDataObjectMetadataFields(&$dataObject, &$metadataDescription)
|
||||
{
|
||||
$metadataSchema = & $this->getMetadataSchema();
|
||||
$handledNamespace = $metadataSchema->getNamespace();
|
||||
|
||||
// Handle translated and non-translated statements separately.
|
||||
foreach ([true, false] as $translated) {
|
||||
// Retrieve the unmapped fields.
|
||||
foreach ($this->getDataObjectMetadataFieldNames($translated) as $unmappedProperty) {
|
||||
// Find out whether we have a statement for this unmapped property.
|
||||
if ($dataObject->hasData($unmappedProperty)) {
|
||||
// Identify the corresponding property name and namespace.
|
||||
[$namespace, $propertyName] = explode(':', $unmappedProperty);
|
||||
|
||||
// Only extract data if the namespace of the property
|
||||
// is the same as the one handled by this adapter and the
|
||||
// property is within the current description.
|
||||
if ($namespace == $handledNamespace && $metadataSchema->hasProperty($propertyName)) {
|
||||
// Add the unmapped statement to the metadata description.
|
||||
if ($translated) {
|
||||
$this->addLocalizedStatements($metadataDescription, $propertyName, $dataObject->getData($unmappedProperty));
|
||||
} else {
|
||||
$metadataDescription->addStatement($propertyName, $dataObject->getData($unmappedProperty));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\metadata\MetadataDataObjectAdapter', '\MetadataDataObjectAdapter');
|
||||
define('METADATA_DOA_INJECTION_MODE', MetadataDataObjectAdapter::METADATA_DOA_INJECTION_MODE);
|
||||
define('METADATA_DOA_EXTRACTION_MODE', MetadataDataObjectAdapter::METADATA_DOA_EXTRACTION_MODE);
|
||||
}
|
||||
@@ -0,0 +1,663 @@
|
||||
<?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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file classes/metadata/MetadataRecord.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 MetadataRecord
|
||||
*
|
||||
* @ingroup metadata
|
||||
*
|
||||
* @see MetadataProperty
|
||||
* @see MetadataDescription
|
||||
*
|
||||
* @brief Class modeling a meta-data record (DCMI abstract model: an instance
|
||||
* of a description set, RDF: a graph of several subject nodes with associated
|
||||
* object nodes).
|
||||
*/
|
||||
|
||||
namespace PKP\metadata;
|
||||
|
||||
class MetadataRecord
|
||||
{
|
||||
/** @var array the MetadataDescriptions in this record */
|
||||
public $_descriptions = [];
|
||||
|
||||
//
|
||||
// Get/set methods
|
||||
//
|
||||
/**
|
||||
* Add a meta-data description.
|
||||
*
|
||||
* @param MetadataDescription $metadataDescription
|
||||
* @param bool $replace whether to replace a description if a description for
|
||||
* the same application entity instance already exists.
|
||||
*
|
||||
* @return bool true if a valid description was added, otherwise false
|
||||
*/
|
||||
public function addDescription($metadataDescription, $replace = true)
|
||||
{
|
||||
assert($metadataDescription instanceof \PKP\metadata\MetadataDescription);
|
||||
|
||||
// Check that the description complies with the meta-data schema
|
||||
$descriptionMetadataSchema = $metadataDescription->getMetadataSchema();
|
||||
$recordMetadataSchema = $this->getMetadataSchema();
|
||||
if ($descriptionMetadataSchema->getName() != $recordMetadataSchema->getName()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check whether we already have a description for the same
|
||||
// application entity instance.
|
||||
$applicationEntityId = $this->getApplicationEntityIdFromMetadataDescription($metadataDescription);
|
||||
if (isset($this->_descriptions[$applicationEntityId]) && !$replace) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Add the description
|
||||
$this->_descriptions[$applicationEntityId] = & $metadataDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove description.
|
||||
*
|
||||
* @param string $applicationEntityId consisting of 'assocType:assocId'
|
||||
*
|
||||
* @return bool true if the description was found and removed, otherwise false
|
||||
*
|
||||
* @see MetadataRecord::getApplicationEntityIdFromMetadataDescription()
|
||||
*/
|
||||
public function removeDescription($applicationEntityId)
|
||||
{
|
||||
// Remove the description if it exists
|
||||
if (isset($applicationEntityId) && isset($this->_descriptions[$applicationEntityId])) {
|
||||
unset($this->_descriptions[$applicationEntityId]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all descriptions
|
||||
*
|
||||
* @return array statements
|
||||
*/
|
||||
public function &getDescriptions()
|
||||
{
|
||||
return $this->_descriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific description
|
||||
*
|
||||
* @param string $applicationEntityId consisting of 'assocType:assocId'
|
||||
*
|
||||
* @return ?bool true if the description was found and removed, otherwise false
|
||||
*
|
||||
* @see MetadataRecord::getApplicationEntityIdFromMetadataDescription()
|
||||
*/
|
||||
public function &getDescription($applicationEntityId)
|
||||
{
|
||||
assert(isset($applicationEntityId));
|
||||
|
||||
// Retrieve the description
|
||||
if (isset($this->_descriptions[$applicationEntityId])) {
|
||||
return $this->_descriptions[$applicationEntityId];
|
||||
} else {
|
||||
$nullValue = null;
|
||||
return $nullValue;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace all descriptions at once. If one of the descriptions
|
||||
* is invalid then the meta-data record will be empty after this
|
||||
* operation.
|
||||
*
|
||||
* @param array $descriptions descriptions
|
||||
*
|
||||
* @return bool true if all descriptions could be added, false otherwise
|
||||
*/
|
||||
public function setDescriptions(&$descriptions)
|
||||
{
|
||||
// Delete existing statements
|
||||
$this->_descriptions = [];
|
||||
|
||||
// Add descriptions one by one to validate them.
|
||||
foreach ($descriptions as $description) {
|
||||
if (!($this->addDescription($description, false))) {
|
||||
$this->_descriptions = [];
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\metadata\MetadataRecord', '\MetadataRecord');
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @defgroup metadata Metadata
|
||||
* Implements the metadata framework, which allows for the flexible description
|
||||
* of objects in many schemas, and conversion of metadata from one schema to
|
||||
* another.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @file classes/metadata/MetadataSchema.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 MetadataSchema
|
||||
*
|
||||
* @ingroup metadata
|
||||
*
|
||||
* @see MetadataProperty
|
||||
* @see MetadataRecord
|
||||
*
|
||||
* @brief Class that represents a meta-data schema (e.g. NLM element-citation,
|
||||
* OpenURL, dc(terms), MODS) or a subset of it.
|
||||
*
|
||||
* We only implement such subsets of meta-data schemas that contain elements which
|
||||
* can be mapped to PKP application objects. Meta-data schemas are not meant to
|
||||
* represent any meta-data in the given schema just PKP application meta-data. The
|
||||
* constructor argument uniquely identifies the application objects this meta-data
|
||||
* schema can be mapped to. There should never be two MetadataSchemas with the same
|
||||
* namespace that map to the same application object type. This also means that we
|
||||
* implement composite elements if and only if the composite complies with our
|
||||
* internal class composition schema and not only because the schema allows a composite
|
||||
* in a certain position. See MetadataDescription and MetadataProperty for further
|
||||
* information about composite meta-data properties.
|
||||
*
|
||||
* Example: We implement a composite to represent authors that correspond to the
|
||||
* \PKP\author\Author class. We do not implement composites for title meta-data
|
||||
* even if the chosen schema allows this (e.g. abbreviated title, alternative title)
|
||||
* as this data is implemented as fields of the Submission object. This doesn't mean
|
||||
* that such data cannot be mapped to composites in external bindings, e.g. in an
|
||||
* XML binding of the meta-data schema. We can always map a flat list of key/value
|
||||
* pairs to a hierarchical representation in an external binding.
|
||||
*
|
||||
* This coupling allows us to flexibly configure meta-data entry for application
|
||||
* objects. We can identify appropriate meta-data fields for application objects
|
||||
* when displaying or entering object meta-data in application forms. Thereby users
|
||||
* can dynamically add meta-data fields for input/output if they require these for
|
||||
* their local meta-data partners (e.g. libraries, repositories, harvesters, indexers).
|
||||
*
|
||||
* We assume that all properties defined within a schema can potentially be assigned
|
||||
* to the objects represented by the given association types. Users should, however,
|
||||
* be able to enable / disable properties on a per-assoc-type basis so that only a
|
||||
* sub-set of properties will actually be available in the user interface as well as
|
||||
* exported or imported for these objects.
|
||||
*
|
||||
* New schemas can be dynamically added to the mix at any time if they provide fields
|
||||
* not provided by already existing schemas.
|
||||
*
|
||||
* NB: We currently provide meta-data schemas as classes for better performance
|
||||
* and code readability. It might, however, be necessary to maintain meta-data
|
||||
* schemas in the database for higher flexibility and easier run-time configuration/
|
||||
* installation of new schemas.
|
||||
*/
|
||||
|
||||
namespace PKP\metadata;
|
||||
|
||||
class MetadataSchema
|
||||
{
|
||||
/** @var array */
|
||||
public $_assocTypes;
|
||||
|
||||
/** @var string */
|
||||
public $_name;
|
||||
|
||||
/** @var string */
|
||||
public $_namespace;
|
||||
|
||||
/** @var string */
|
||||
public $_classname;
|
||||
|
||||
/**
|
||||
* @var array meta-data properties (predicates)
|
||||
* supported for this meta-data schema.
|
||||
*/
|
||||
public $_properties = [];
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $name the meta-data schema name
|
||||
* @param string $namespace a globally unique namespace for
|
||||
* the schema. Property names must be unique within this
|
||||
* namespace.
|
||||
* @param string $classname the fully qualified class name of
|
||||
* this schema
|
||||
* @param array|int $assocTypes the association types of
|
||||
* PKP application objects that can be described using
|
||||
* this schema. A single association type can be given as
|
||||
* a scalar.
|
||||
*/
|
||||
public function __construct($name, $namespace, $classname, $assocTypes)
|
||||
{
|
||||
assert(is_string($name) && is_string($namespace) && is_string($classname));
|
||||
assert(is_array($assocTypes) || is_integer($assocTypes));
|
||||
|
||||
// Set name and namespace.
|
||||
$this->_name = $name;
|
||||
$this->_namespace = $namespace;
|
||||
$this->_classname = $classname;
|
||||
|
||||
// Normalize and set the association types.
|
||||
if (!is_array($assocTypes)) {
|
||||
$assocTypes = [$assocTypes];
|
||||
}
|
||||
$this->_assocTypes = $assocTypes;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Getters and Setters
|
||||
//
|
||||
/**
|
||||
* Get the name of the schema
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the internal namespace qualifier of the schema
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNamespace()
|
||||
{
|
||||
return $this->_namespace;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fully qualified class name of this schema.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getClassName()
|
||||
{
|
||||
return $this->_classname;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the association types for PKP application objects
|
||||
* that can be described with this schema.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getAssocTypes()
|
||||
{
|
||||
return $this->_assocTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the properties of the meta-data schema.
|
||||
*
|
||||
* @return array an array of MetadataProperties
|
||||
*/
|
||||
public function &getProperties()
|
||||
{
|
||||
return $this->_properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a property. Returns null if the property
|
||||
* doesn't exist.
|
||||
*
|
||||
* @return MetadataProperty
|
||||
*/
|
||||
public function &getProperty($propertyName)
|
||||
{
|
||||
assert(is_string($propertyName));
|
||||
if ($this->hasProperty($propertyName)) {
|
||||
$property = & $this->_properties[$propertyName];
|
||||
} else {
|
||||
$property = null;
|
||||
}
|
||||
return $property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the property id with prefixed name space
|
||||
* for use in an external context (e.g. Forms, Templates).
|
||||
*
|
||||
* @param string $propertyName
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getNamespacedPropertyId($propertyName)
|
||||
{
|
||||
$property = & $this->getProperty($propertyName);
|
||||
assert($property instanceof \PKP\metadata\MetadataProperty);
|
||||
return $this->getNamespace() . ucfirst($property->getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* (Re-)set all properties of this meta-data schema.
|
||||
*
|
||||
* @param array $properties an array of MetadataProperties
|
||||
*/
|
||||
public function setProperties(&$properties)
|
||||
{
|
||||
// Remove the existing properties
|
||||
$this->_properties = [];
|
||||
|
||||
// Insert the new properties
|
||||
foreach ($properties as $property) {
|
||||
$this->addProperty($property);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a property to this meta-data schema.
|
||||
*
|
||||
* @param string $name the unique name of the property within a meta-data schema (can be a property URI)
|
||||
* @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 addProperty(
|
||||
$name,
|
||||
$allowedTypes = MetadataProperty::METADATA_PROPERTY_TYPE_STRING,
|
||||
$translated = false,
|
||||
$cardinality = MetadataProperty::METADATA_PROPERTY_CARDINALITY_ONE,
|
||||
$displayName = null,
|
||||
$validationMessage = null,
|
||||
$mandatory = false
|
||||
) {
|
||||
// Make sure that this property has not been added before
|
||||
assert(!is_null($name) && !isset($this->_properties[$name]));
|
||||
|
||||
// Instantiate the property.
|
||||
$property = new MetadataProperty($name, $this->_assocTypes, $allowedTypes, $translated, $cardinality, $displayName, $validationMessage, $mandatory);
|
||||
|
||||
// Add the property
|
||||
$this->_properties[$name] = & $property;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the property names defined for this meta-data schema
|
||||
*
|
||||
* @return array an array of string values representing valid property names
|
||||
*/
|
||||
public function getPropertyNames()
|
||||
{
|
||||
return array_keys($this->_properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the names of properties with a given data type.
|
||||
*
|
||||
* @param mixed $propertyType a valid property type description
|
||||
*
|
||||
* @return array an array of string values representing valid property names
|
||||
*/
|
||||
public function getPropertyNamesByType($propertyType)
|
||||
{
|
||||
assert(in_array($propertyType, MetadataProperty::getSupportedTypes()));
|
||||
|
||||
$propertyNames = [];
|
||||
foreach ($this->_properties as $property) {
|
||||
$allowedPropertyTypes = $property->getAllowedTypes();
|
||||
if (isset($allowedPropertyTypes[$propertyType])) {
|
||||
$propertyNames[] = $property->getName();
|
||||
}
|
||||
}
|
||||
|
||||
return $propertyNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a property exists in the meta-data schema
|
||||
*
|
||||
* @param string $propertyName
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasProperty($propertyName)
|
||||
{
|
||||
return isset($this->_properties[$propertyName]);
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\metadata\MetadataSchema', '\MetadataSchema');
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
/**
|
||||
* @file classes/metadata/MetadataTypeDescription.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 MetadataTypeDescription
|
||||
*
|
||||
* @ingroup metadata
|
||||
*
|
||||
* @brief Type validator for metadata input/output.
|
||||
*
|
||||
* This type description accepts descriptors of the following form:
|
||||
* metadata::fully.qualified.MetadataSchema(ASSOC)
|
||||
*
|
||||
* The assoc form must be the final part of a Application::ASSOC_TYPE_* definition.
|
||||
* It can be '*' to designate any assoc type.
|
||||
*/
|
||||
|
||||
namespace PKP\metadata;
|
||||
|
||||
use PKP\filter\ClassTypeDescription;
|
||||
|
||||
class MetadataTypeDescription extends ClassTypeDescription
|
||||
{
|
||||
public const ASSOC_TYPE_ANY = -1;
|
||||
|
||||
/** @var string the expected meta-data schema package */
|
||||
public $_metadataSchemaPackageName;
|
||||
|
||||
/** @var string the expected meta-data schema class */
|
||||
public $_metadataSchemaClassName;
|
||||
|
||||
/** @var int the expected assoc type of the meta-data description */
|
||||
public $_assocType;
|
||||
|
||||
|
||||
//
|
||||
// Setters and Getters
|
||||
//
|
||||
/**
|
||||
* @see TypeDescription::getNamespace()
|
||||
*/
|
||||
public function getNamespace()
|
||||
{
|
||||
return \PKP\filter\TypeDescriptionFactory::TYPE_DESCRIPTION_NAMESPACE_METADATA;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string the fully qualified class name of the meta-data schema.
|
||||
*/
|
||||
public function getMetadataSchemaClass()
|
||||
{
|
||||
// If using PSR-based FQCNs, the package name will be empty.
|
||||
// If using deprecated period.separated.package.names, the package name will be provided.
|
||||
$prefix = (string) $this->_metadataSchemaPackageName;
|
||||
if ($prefix) {
|
||||
$prefix .= '.';
|
||||
}
|
||||
return $prefix . $this->_metadataSchemaClassName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getAssocType()
|
||||
{
|
||||
return $this->_assocType;
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Implement abstract template methods from TypeDescription
|
||||
//
|
||||
/**
|
||||
* @see TypeDescription::parseTypeName()
|
||||
*/
|
||||
public function parseTypeName($typeName)
|
||||
{
|
||||
// Configure the parent class type description
|
||||
// with the expected meta-data class.
|
||||
parent::parseTypeName('lib.pkp.classes.metadata.MetadataDescription');
|
||||
|
||||
// Split the type name into class name and assoc type.
|
||||
$typeNameParts = explode('(', $typeName);
|
||||
if (!count($typeNameParts) == 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The meta-data schema class must be
|
||||
// a fully qualified class name.
|
||||
$splitMetadataSchemaClass = $this->splitClassName($typeNameParts[0]);
|
||||
if ($splitMetadataSchemaClass === false) {
|
||||
return false;
|
||||
}
|
||||
[$this->_metadataSchemaPackageName, $this->_metadataSchemaClassName] = $splitMetadataSchemaClass;
|
||||
|
||||
// Identify the assoc type.
|
||||
$assocTypeString = trim($typeNameParts[1], ')');
|
||||
if ($assocTypeString == '*') {
|
||||
$this->_assocType = self::ASSOC_TYPE_ANY;
|
||||
} else {
|
||||
// Make sure that the given assoc type exists.
|
||||
$assocTypeString = 'ASSOC_TYPE_' . $assocTypeString;
|
||||
if (!defined($assocTypeString)) {
|
||||
return false;
|
||||
}
|
||||
$this->_assocType = constant($assocTypeString);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see TypeDescription::checkType()
|
||||
*/
|
||||
public function checkType($object)
|
||||
{
|
||||
// First of all check whether this is a
|
||||
// meta-data description at all.
|
||||
if (!parent::checkType($object)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the meta-data schema.
|
||||
$metadataSchema = & $object->getMetadataSchema();
|
||||
if (!$metadataSchema instanceof $this->_metadataSchemaClassName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check the assoc type
|
||||
if ($this->_assocType != self::ASSOC_TYPE_ANY) {
|
||||
if ($object->getAssocType() != $this->_assocType) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!PKP_STRICT_MODE) {
|
||||
class_alias('\PKP\metadata\MetadataTypeDescription', '\MetadataTypeDescription');
|
||||
define('ASSOC_TYPE_ANY', MetadataTypeDescription::ASSOC_TYPE_ANY);
|
||||
}
|
||||
Reference in New Issue
Block a user