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