* as our primary representation of delivery meta-data and dcterms * 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. */ import('lib.pkp.classes.core.DataObject'); define('METADATA_DESCRIPTION_REPLACE_ALL', 0x01); define('METADATA_DESCRIPTION_REPLACE_PROPERTIES', 0x02); define('METADATA_DESCRIPTION_REPLACE_NOTHING', 0x03); define('METADATA_DESCRIPTION_UNKNOWN_LOCALE', 'unknown'); class MetadataDescription extends DataObject { /** @var string fully qualified class name of the meta-data schema this description complies to */ var $_metadataSchemaName; /** @var MetadataSchema the schema this description complies to */ var $_metadataSchema; /** @var int association type (the type of the described resource) */ var $_assocType; /** @var int association id (the identifier of the described resource) */ var $_assocId; /** * @var string an (optional) display name that describes the contents * of this meta-data description to the end user. */ var $_displayName; /** * @var integer sequence id used when saving several descriptions * of the same subject. */ var $_seq; /** * Constructor */ function MetadataDescription($metadataSchemaName, $assocType) { assert(is_string($metadataSchemaName) && is_integer($assocType)); parent::DataObject(); $this->_metadataSchemaName = $metadataSchemaName; $this->_assocType = $assocType; } // // Get/set methods // /** * Get the fully qualified class name of * the supported meta-data schema. */ function getMetadataSchemaName() { return $this->_metadataSchemaName; } /** * Get the metadata schema * @return MetadataSchema */ function &getMetadataSchema() { // Lazy-load the meta-data schema if this has // not been done before. if (is_null($this->_metadataSchema)) { $this->_metadataSchema =& instantiate($this->getMetadataSchemaName(), 'MetadataSchema'); assert(is_object($this->_metadataSchema)); } return $this->_metadataSchema; } /** * Get the association type (described resource type) * @return int */ function getAssocType() { return $this->_assocType; } /** * Get the association id (described resource identifier) * @return int */ function getAssocId() { return $this->_assocId; } /** * Set the association id (described resource identifier) * @param $assocId int */ 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 */ function getAssoc() { $assocType = $this->getAssocType(); $assocId = $this->getAssocId(); assert(isset($assocType) && isset($assocId)); return $assocType.':'.$assocId; } /** * Set the (optional) display name * @param $displayName string */ function setDisplayName($displayName) { $this->_displayName = $displayName; } /** * Get the (optional) display name * @return string */ function getDisplayName() { return $this->_displayName; } /** * Set the sequence id * @param $seq integer */ function setSeq($seq) { $this->_seq = $seq; } /** * Get the sequence id * @return integer */ function getSeq() { 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 $propertyName string The name of the property * @param $value mixed The value to be assigned to the property * @param $locale string * @param $replace boolean whether to replace an existing statement * @return boolean true if a valid statement was added, otherwise false */ function addStatement($propertyName, &$value, $locale = null, $replace = false) { // Check the property $property =& $this->getProperty($propertyName); if (is_null($property)) return false; assert(is_a($property, '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 = AppLocale::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 METADATA_PROPERTY_CARDINALITY_ONE: if (isset($existingValue) && !$replace) return false; $newValue =& $value; break; case METADATA_PROPERTY_CARDINALITY_MANY: if (isset($existingValue) && !$replace) { assert(is_array($existingValue)); $newValue = $existingValue; array_push($newValue, $value); } else { $newValue = array(&$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 $propertyName string * @param $locale string * @return boolean true if the statement was found and removed, otherwise false */ 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 */ 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 $propertyName string * @param $locale string * @return mixed a scalar property value or an array of property values * if the cardinality of the property is 'many'. */ function &getStatement($propertyName, $locale = null) { // Check the property $property =& $this->getProperty($propertyName); assert(isset($property) && is_a($property, 'MetadataProperty')); // Handle translation $translated = $property->getTranslated(); if (!$translated) assert(is_null($locale)); if ($translated && !isset($locale)) { // Retrieve the current locale $locale = AppLocale::getLocale(); } // Retrieve the value return $this->getData($propertyName, $locale); } /** * Returns all translations of a translated property * @param $propertyName string * @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. */ 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 $statements array statements * @param $replace integer one of the allowed replace levels. * @return boolean true if all statements could be added, false otherwise */ function setStatements(&$statements, $replace = METADATA_DESCRIPTION_REPLACE_PROPERTIES) { assert(in_array($replace, $this->_allowedReplaceLevels())); // Make a backup copy of all existing statements. $statementsBackup = $this->getAllData(); if ($replace == METADATA_DESCRIPTION_REPLACE_ALL) { // Delete existing statements $emptyArray = array(); $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 = array(&$content); } else { $values =& $content; } if ($replace == 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 = array(&$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 */ function &getProperties() { $metadataSchema =& $this->getMetadataSchema(); return $metadataSchema->getProperties(); } /** * Convenience method that returns a property from * the underlying meta-data schema. * @param $propertyName string * @return MetadataProperty */ function &getProperty($propertyName) { $metadataSchema =& $this->getMetadataSchema(); return $metadataSchema->getProperty($propertyName); } /** * Convenience method that returns a property id * the underlying meta-data schema. * @param $propertyName string * @return string */ 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 */ 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 $propertyType string * @return array an array of string values representing valid property names */ 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 */ function getSetPropertyNames() { return array_keys($this->getStatements()); } /** * Convenience method that checks the existence * of a property in the underlying meta-data schema. * @param $propertyName string * @return boolean */ function hasProperty($propertyName) { $metadataSchema =& $this->getMetadataSchema(); return $metadataSchema->hasProperty($propertyName); } /** * Check the existence of a statement for the given property. * @param $propertyName string * @return boolean */ function hasStatement($propertyName) { $statements =& $this->getStatements(); return (isset($statements[$propertyName])); } /** * Convenience method that checks whether a given property * is translated. * @param $propertyName string * @return boolean */ function isTranslatedProperty($propertyName) { $property = $this->getProperty($propertyName); assert(is_a($property, 'MetadataProperty')); return $property->getTranslated(); } // // Private helper methods // /** * The allowed replace levels for the * setStatements() method. * NB: Workaround for PHP4 which doesn't allow * static class members. */ function _allowedReplaceLevels() { static $allowedReplaceLevels = array( METADATA_DESCRIPTION_REPLACE_ALL, METADATA_DESCRIPTION_REPLACE_PROPERTIES, METADATA_DESCRIPTION_REPLACE_NOTHING ); return $allowedReplaceLevels; } } ?>