PageRenderTime 49ms CodeModel.GetById 23ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/pkp/classes/metadata/MetadataDescription.inc.php

https://github.com/lib-uoguelph-ca/ocs
PHP | 508 lines | 188 code | 48 blank | 272 comment | 35 complexity | ac43a9dc312aeb8e7af46d780fad52c0 MD5 | raw file
Possible License(s): GPL-2.0
  1. <?php
  2. /**
  3. * @file classes/metadata/MetadataDescription.inc.php
  4. *
  5. * Copyright (c) 2000-2012 John Willinsky
  6. * Distributed under the GNU GPL v2. For full terms see the file docs/COPYING.
  7. *
  8. * @class MetadataDescription
  9. * @ingroup metadata
  10. * @see MetadataProperty
  11. * @see MetadataRecord
  12. * @see MetadataSchema
  13. *
  14. * @brief Class modeling a description (DCMI abstract model) or subject-
  15. * predicate-object graph (RDF). This class and its children provide
  16. * meta-data (DCMI abstract model: statements of property-value pairs,
  17. * RDF: assertions of predicate-object pairs) about a given PKP application
  18. * entity instance (DCMI abstract model: described resource, RDF: subject).
  19. *
  20. * This class has primarily been designed to describe journals, journal
  21. * issues, articles, conferences, conference proceedings (conference papers),
  22. * monographs (books), monograph components (book chapters) or citations.
  23. *
  24. * It is, however, flexible enough to be extended to describe any
  25. * application entity in the future. Descriptions can be retrieved from
  26. * any application object that implements the MetadataProvider interface.
  27. *
  28. * Special attention has been paid to the compatibility of the class
  29. * implementation with the implementation of several meta-data standards
  30. * that we consider especially relevant to our use cases.
  31. *
  32. * We distinguish two main use cases for meta-data: discovery and delivery
  33. * of described resources. We have chosen the element-citation tag from the
  34. * NLM standard <http://dtd.nlm.nih.gov/publishing/tag-library/3.0/n-8xa0.html>
  35. * as our primary representation of delivery meta-data and dcterms
  36. * <http://dublincore.org/documents/dcmi-terms/> as our primary
  37. * representation of discovery meta-data.
  38. *
  39. * Our specific use of meta-data has important implications and determines
  40. * our design goals:
  41. * * Neither NLM-citation nor dcterms have been designed with an object
  42. * oriented encoding in mind. NLM-citation is usually XML encoded
  43. * while typical dcterms encodings are HTML meta-tags, RDF or XML.
  44. * * We believe that trying to implement a super-set of meta-data
  45. * standards ("least common denominator" or super-schema approach)
  46. * is fundamentally flawed as meta-data standards are always
  47. * developed with specific use-cases in mind that require potentially
  48. * incompatible data properties or encodings.
  49. * * Although we think that NLM-citation and dcterms are sensible default
  50. * meta-data schemes our design should remain flexible enough for
  51. * users to implement and use other schemes as an internal meta-data
  52. * standard.
  53. * * We have to make sure that we can easily extract/inject meta-data
  54. * from/to PKP application objects.
  55. * * We have to avoid code duplication to keep maintenance cost under
  56. * control.
  57. * * We have to minimize the "impedance mismatch" between our own
  58. * object oriented encoding and fully standard compliant external
  59. * encodings (i.e. XML, RDF, HTML meta-tags, ...) to allow for easy
  60. * conversion between encodings.
  61. * * We have to make sure that we can switch between internal and
  62. * external encodings without any data loss.
  63. * * We have to make sure that crosswalks to and from other important
  64. * meta-data standards (e.g. OpenURL variants, MODS, MARC) can be
  65. * performed in a well-defined and easy way while minimizing data
  66. * loss.
  67. * * We have to make sure that we can support qualified fields (e.g.
  68. * qualified DC).
  69. * * We have to make sure that we can support RDF triples.
  70. *
  71. * We took the following design decisions to achieve these goals:
  72. * * We only implement properties that are justified by strong real-world
  73. * use-cases. We recognize that the limiting factor is not the data that
  74. * we could represent but the data we actually have. This is not determined
  75. * by the chosen standard but by the PKP application objects we want to
  76. * represent. Additional meta-data properties/predicates can be added as
  77. * required.
  78. * * We do adapt data structures as long as we can make sure that a
  79. * fully standard compliant encoding can always be re-constructed. This
  80. * is especially true for NLM-citation which is designed with
  81. * XML in mind and therefore uses hierarchical constructs that are
  82. * difficult to represent in an OO class model.
  83. * This means that our meta-data framework only supports (nested) key/
  84. * value-based schemas which can however be converted to hierarchical
  85. * representations.
  86. * * We borrow class and property names from the DCMI abstract model as
  87. * the terms used there provide better readability for developers less
  88. * acquainted with formal model theory. We'll, however, make sure that
  89. * data can easily be RDF encoded within our data model.
  90. * * Data validation must ensure that meta-data always complies with a
  91. * specific meta-data standard. As we are speaking about an object
  92. * oriented encoding that is not defined in the original standard, we
  93. * define compliance as "roundtripability". This means we must be able
  94. * to convert our object oriented data encoding to a fully standard
  95. * compliant encoding and back without any data loss.
  96. *
  97. * TODO: Develop an object representation for NLM's "collab", "anonymous" and "etal".
  98. *
  99. * TODO: Move Harvester's Schema and Record to the new Metadata object model.
  100. */
  101. // $Id$
  102. import('core.DataObject');
  103. define('METADATA_DESCRIPTION_REPLACE_ALL', 0x01);
  104. define('METADATA_DESCRIPTION_REPLACE_PROPERTIES', 0x02);
  105. define('METADATA_DESCRIPTION_REPLACE_NOTHING', 0x03);
  106. class MetadataDescription extends DataObject {
  107. /** @var MetadataSchema the schema this description complies to */
  108. var $_metadataSchema;
  109. /** @var int association type (the type of the described resource) */
  110. var $_assocType;
  111. /** @var int association id (the identifier of the described resource) */
  112. var $_assocId;
  113. /**
  114. * Constructor
  115. */
  116. function MetadataDescription(&$metadataSchema, $assocType) {
  117. assert(is_a($metadataSchema, 'MetadataSchema') && is_integer($assocType));
  118. parent::DataObject();
  119. $this->_metadataSchema =& $metadataSchema;
  120. $this->_assocType = $assocType;
  121. }
  122. //
  123. // Get/set methods
  124. //
  125. /**
  126. * Get the metadata schema
  127. * @return MetadataSchema
  128. */
  129. function &getMetadataSchema() {
  130. return $this->_metadataSchema;
  131. }
  132. /**
  133. * Get the association type (described resource type)
  134. * @return int
  135. */
  136. function getAssocType() {
  137. return $this->_assocType;
  138. }
  139. /**
  140. * Get the association id (described resource identifier)
  141. * @return int
  142. */
  143. function getAssocId() {
  144. return $this->_assocId;
  145. }
  146. /**
  147. * Set the association id (described resource identifier)
  148. * @param $assocId int
  149. */
  150. function setAssocId($assocId) {
  151. $this->_assocId = $assocId;
  152. }
  153. /**
  154. * Construct a meta-data application entity id
  155. * (described resource id / subject id) for
  156. * this meta-data description object.
  157. * @return string
  158. */
  159. function getAssoc() {
  160. $assocType = $this->getAssocType();
  161. $assocId = $this->getAssocId();
  162. assert(isset($assocType) && isset($assocId));
  163. return $assocType.':'.$assocId;
  164. }
  165. /**
  166. * Add a meta-data statement. Statements can only be added
  167. * for properties that are part of the meta-data schema. This
  168. * method will also check the validity of the value for the
  169. * given property before adding the statement.
  170. * @param $propertyName string The name of the property
  171. * @param $value mixed The value to be assigned to the property
  172. * @param $locale string
  173. * @param $replace boolean whether to replace an existing statement
  174. * @return boolean true if a valid statement was added, otherwise false
  175. */
  176. function addStatement($propertyName, &$value, $locale = null, $replace = false) {
  177. // Check the property
  178. $property =& $this->getProperty($propertyName);
  179. if (is_null($property)) return false;
  180. assert(is_a($property, 'MetadataProperty'));
  181. // Check that the property is allowed for the described resource
  182. if (!in_array($this->_assocType, $property->getAssocTypes())) return false;
  183. // Check that the value is compliant with the property specification
  184. if (!$property->isValid($value)) return false;
  185. // Handle translation
  186. $translated = $property->getTranslated();
  187. if (isset($locale) && !$translated) return false;
  188. if (!isset($locale) && $translated) {
  189. // Retrieve the current locale
  190. $locale = AppLocale::getLocale();
  191. }
  192. // Handle cardinality
  193. $existingValue =& $this->getStatement($propertyName, $locale);
  194. switch ($property->getCardinality()) {
  195. case METADATA_PROPERTY_CARDINALITY_ONE:
  196. if (isset($existingValue) && !$replace) return false;
  197. $newValue =& $value;
  198. break;
  199. case METADATA_PROPERTY_CARDINALITY_MANY:
  200. if (isset($existingValue) && !$replace) {
  201. assert(is_array($existingValue));
  202. $newValue = $existingValue;
  203. array_push($newValue, $value);
  204. } else {
  205. $newValue = array(&$value);
  206. }
  207. break;
  208. default:
  209. assert(false);
  210. }
  211. // Add the value
  212. $this->setData($propertyName, $newValue, $locale);
  213. return true;
  214. }
  215. /**
  216. * Remove statement. If the property has cardinality 'many'
  217. * then all statements for the property will be removed at once.
  218. * If the property is translated and the locale is null then
  219. * the statements for all locales will be removed.
  220. * @param $propertyName string
  221. * @param $locale string
  222. * @return boolean true if the statement was found and removed, otherwise false
  223. */
  224. function removeStatement($propertyName, $locale = null) {
  225. // Remove the statement if it exists
  226. if (isset($propertyName) && $this->hasData($propertyName, $locale)) {
  227. $this->setData($propertyName, null, $locale);
  228. return true;
  229. }
  230. return false;
  231. }
  232. /**
  233. * Get all statements
  234. * @return array statements
  235. */
  236. function &getStatements() {
  237. // Do not retrieve the data by-ref
  238. // otherwise the following unset()
  239. // will change internal state.
  240. $allData = $this->getAllData();
  241. // Unset data variables that are not statements
  242. unset($allData['id']);
  243. return $allData;
  244. }
  245. /**
  246. * Get a specific statement
  247. * @param $propertyName string
  248. * @param $locale string
  249. * @return mixed a scalar property value or an array of property values
  250. * if the cardinality of the property is 'many'.
  251. */
  252. function &getStatement($propertyName, $locale = null) {
  253. // Check the property
  254. $property =& $this->getProperty($propertyName);
  255. assert(isset($property) && is_a($property, 'MetadataProperty'));
  256. // Handle translation
  257. $translated = $property->getTranslated();
  258. if (!$translated) assert(is_null($locale));
  259. if ($translated && !isset($locale)) {
  260. // Retrieve the current locale
  261. $locale = AppLocale::getLocale();
  262. }
  263. // Retrieve the value
  264. return $this->getData($propertyName, $locale);
  265. }
  266. /**
  267. * Returns all translations of a translated property
  268. * @param $propertyName string
  269. * @return array all translations of a given property; if the
  270. * property has cardinality "many" then this returns a two-dimensional
  271. * array whereby the first key represents the locale and the second.
  272. */
  273. function &getStatementTranslations($propertyName) {
  274. assert($this->isTranslatedProperty($propertyName));
  275. return $this->getData($propertyName);
  276. }
  277. /**
  278. * Add several statements at once. If one of the statements
  279. * is invalid then the meta-data description will remain in its
  280. * initial state.
  281. * * Properties with a cardinality of 'many' must be passed in as
  282. * sub-arrays.
  283. * * Translated properties with a cardinality of 'one' must be
  284. * passed in as sub-arrays with the locale as a key.
  285. * * Translated properties with a cardinality of 'many' must be
  286. * passed in as sub-sub-arrays with the locale as the first key.
  287. * @param $statements array statements
  288. * @param $replace integer one of the allowed replace levels.
  289. * @return boolean true if all statements could be added, false otherwise
  290. */
  291. function setStatements(&$statements, $replace = METADATA_DESCRIPTION_REPLACE_PROPERTIES) {
  292. assert(in_array($replace, $this->_allowedReplaceLevels()));
  293. // Make a backup copy of all existing statements.
  294. $statementsBackup = $this->getAllData();
  295. if ($replace == METADATA_DESCRIPTION_REPLACE_ALL) {
  296. // Delete existing statements
  297. $emptyArray = array();
  298. $this->setAllData($emptyArray);
  299. }
  300. // Add statements one by one to detect invalid values.
  301. foreach($statements as $propertyName => $content) {
  302. assert(!empty($content));
  303. // Transform scalars or translated fields to arrays so that
  304. // we can handle properties with different cardinalities in
  305. // the same way.
  306. if (is_scalar($content) || is_string(key($content))) {
  307. $values = array(&$content);
  308. } else {
  309. $values =& $content;
  310. }
  311. if ($replace == METADATA_DESCRIPTION_REPLACE_PROPERTIES) {
  312. $replaceProperty = true;
  313. } else {
  314. $replaceProperty = false;
  315. }
  316. foreach($values as $value) {
  317. // Is this a translated property?
  318. if (is_array($value)) {
  319. foreach($value as $locale => $translation) {
  320. // Handle cardinality many and one in the same way
  321. if (is_scalar($translation)) {
  322. $translationValues = array(&$translation);
  323. } else {
  324. $translationValues =& $translation;
  325. }
  326. foreach($translationValues as $translationValue) {
  327. // Add a statement (replace existing statement if any)
  328. if (!($this->addStatement($propertyName, $translationValue, $locale, $replaceProperty))) {
  329. $this->setAllData($statementsBackup);
  330. return false;
  331. }
  332. // Reset the $replaceProperty flag to avoid that subsequent
  333. // value entries will overwrite previous value entries.
  334. $replaceProperty = false;
  335. unset($translationValue);
  336. }
  337. unset($translationValues);
  338. }
  339. unset($translation);
  340. } else {
  341. // Add a statement (replace existing statement if any)
  342. if (!($this->addStatement($propertyName, $value, null, $replaceProperty))) {
  343. $this->setAllData($statementsBackup);
  344. return false;
  345. }
  346. // Reset the $replaceProperty flag to avoid that subsequent
  347. // value entries will overwrite previous value entries.
  348. $replaceProperty = false;
  349. }
  350. unset($value);
  351. }
  352. unset($values);
  353. }
  354. return true;
  355. }
  356. /**
  357. * Convenience method that returns the properties of
  358. * the underlying meta-data schema.
  359. * @return array an array of MetadataProperties
  360. */
  361. function &getProperties() {
  362. $metadataSchema =& $this->getMetadataSchema();
  363. return $metadataSchema->getProperties();
  364. }
  365. /**
  366. * Convenience method that returns a property from
  367. * the underlying meta-data schema.
  368. * @param $propertyName string
  369. * @return MetadataProperty
  370. */
  371. function &getProperty($propertyName) {
  372. $metadataSchema =& $this->getMetadataSchema();
  373. return $metadataSchema->getProperty($propertyName);
  374. }
  375. /**
  376. * Convenience method that returns a property id
  377. * the underlying meta-data schema.
  378. * @param $propertyName string
  379. * @return string
  380. */
  381. function getNamespacedPropertyId($propertyName) {
  382. $metadataSchema =& $this->getMetadataSchema();
  383. return $metadataSchema->getNamespacedPropertyId($propertyName);
  384. }
  385. /**
  386. * Convenience method that returns the valid
  387. * property names of the underlying meta-data schema.
  388. * @return array an array of string values representing valid property names
  389. */
  390. function getPropertyNames() {
  391. $metadataSchema =& $this->getMetadataSchema();
  392. return $metadataSchema->getPropertyNames();
  393. }
  394. /**
  395. * Convenience method that returns the names of properties with a
  396. * given data type of the underlying meta-data schema.
  397. * @param $propertyType string
  398. * @return array an array of string values representing valid property names
  399. */
  400. function getPropertyNamesByType($propertyType) {
  401. $metadataSchema =& $this->getMetadataSchema();
  402. return $metadataSchema->getPropertyNamesByType($propertyType);
  403. }
  404. /**
  405. * Returns an array of property names for
  406. * which statements exist.
  407. * @return array an array of string values representing valid property names
  408. */
  409. function getSetPropertyNames() {
  410. return array_keys($this->getStatements());
  411. }
  412. /**
  413. * Convenience method that checks the existence
  414. * of a property in the underlying meta-data schema.
  415. * @param $propertyName string
  416. * @return boolean
  417. */
  418. function hasProperty($propertyName) {
  419. $metadataSchema =& $this->getMetadataSchema();
  420. return $metadataSchema->hasProperty($propertyName);
  421. }
  422. /**
  423. * Check the existence of a statement for the given property.
  424. * @param $propertyName string
  425. * @return boolean
  426. */
  427. function hasStatement($propertyName) {
  428. $statements =& $this->getStatements();
  429. return (isset($statements[$propertyName]));
  430. }
  431. /**
  432. * Convenience method that checks whether a given property
  433. * is translated.
  434. * @param $propertyName string
  435. * @return boolean
  436. */
  437. function isTranslatedProperty($propertyName) {
  438. $property = $this->getProperty($propertyName);
  439. assert(is_a($property, 'MetadataProperty'));
  440. return $property->getTranslated();
  441. }
  442. //
  443. // Private helper methods
  444. //
  445. /**
  446. * The allowed replace levels for the
  447. * setStatements() method.
  448. * NB: Workaround for PHP4 which doesn't allow
  449. * static class members.
  450. */
  451. function _allowedReplaceLevels() {
  452. static $allowedReplaceLevels = array(
  453. METADATA_DESCRIPTION_REPLACE_ALL,
  454. METADATA_DESCRIPTION_REPLACE_PROPERTIES,
  455. METADATA_DESCRIPTION_REPLACE_NOTHING
  456. );
  457. return $allowedReplaceLevels;
  458. }
  459. }
  460. ?>