Open Journal Systems  3.3.0
OAIMetadataFormat_JATS.inc.php
1 <?php
2 
27  protected function _findJats($record) {
28  $article = $record->getData('article');
29  $galleys = $record->getData('galleys');
30 
31  import('lib.pkp.classes.submission.SubmissionFile'); // SUBMISSION_FILE_... constants
32  $submissionFileDao = DAORegistry::getDAO('SubmissionFileDAO');
33  $candidateFiles = array();
34 
35  // First, look for candidates in the galleys area (published content).
36  foreach ($galleys as $galley) {
37  $galleyFiles = $submissionFileDao->getLatestRevisionsByAssocId(ASSOC_TYPE_GALLEY, $galley->getId(), $article->getId(), SUBMISSION_FILE_PROOF);
38  foreach ($galleyFiles as $galleyFile) {
39  if ($this->_isCandidateFile($galleyFile)) $candidateFiles[] = $galleyFile;
40  }
41  }
42 
43  // If no candidates were found, look in the layout area (unpublished content).
44  if (empty($candidateFiles)) {
45  $layoutFiles = $submissionFileDao->getLatestRevisions($article->getId(), SUBMISSION_FILE_PRODUCTION_READY);
46  foreach ($layoutFiles as $layoutFile) {
47  if ($this->_isCandidateFile($layoutFile)) $candidateFiles[] = $layoutFile;
48  }
49  }
50 
51  $doc = null;
52  HookRegistry::call('OAIMetadataFormat_JATS::findJats', array(&$this, &$record, &$candidateFiles, &$doc));
53 
54  // If no candidate files were located, return the null XML.
55  if (!$doc && empty($candidateFiles)) {
56  return null;
57  }
58  if (count($candidateFiles) > 1) error_log('WARNING: More than one JATS XML candidate documents were located for submission ' . $article->getId() . '.');
59 
60  // Fetch the XML document
61  if (!$doc) {
62  $candidateFile = array_shift($candidateFiles);
63  $doc = new DOMDocument;
64  $doc->loadXML(file_get_contents($candidateFile->getFilePath()));
65  }
66 
67  return $doc;
68  }
69 
73  function toXml($record, $format = null) {
74  $oaiDao = DAORegistry::getDAO('OAIDAO');
75  $journal = $record->getData('journal');
76  $article = $record->getData('article');
77  $section = $record->getData('section');
78  $issue = $record->getData('issue');
79 
80  // Check access
81  import('classes.issue.IssueAction');
82  $issueAction = new IssueAction();
83  $subscriptionRequired = $issueAction->subscriptionRequired($issue, $journal);
84  $isSubscribedDomain = $issueAction->subscribedDomain(Application::get()->getRequest(), $journal, $issue->getId(), $article->getId());
85  if ($subscriptionRequired && !$isSubscribedDomain) {
86  $oaiDao->oai->error('cannotDisseminateFormat', 'Cannot disseminate format (JATS XML not available)');
87  exit();
88  }
89 
90  $doc = $this->_findJats($record);
91  if (!$doc) {
92  $oaiDao->oai->error('cannotDisseminateFormat', 'Cannot disseminate format (JATS XML not available)');
93  exit();
94  }
95  $this->_mungeMetadata($doc, $journal, $article, $section, $issue);
96 
97  return $doc->saveXml($doc->getElementsByTagName('article')->item(0));
98  }
99 
106  protected function _addChildInOrder($parentNode, $childNode) {
107  $permittedElementOrders = array(
108  'front' => array('article-meta', 'journal-meta'),
109  'article-meta' => array('article-id', 'article-categories', 'title-group', 'contrib-group', 'aff', 'aff-alternatives', 'x', 'author-notes', 'pub-date', 'volume', 'volume-id', 'volume-series', 'issue', 'issue-id', 'issue-title', 'issue-sponsor', 'issue-part', 'isbn', 'supplement', 'fpage', 'lpage', 'page-range', 'elocation-id', 'email', 'ext-link', 'uri', 'product', 'supplementary-material', 'history', 'permissions', 'self-uri', 'related-article', 'related-object', 'abstract', 'trans-abstract', 'kwd-group', 'funding-group', 'conference', 'counts', 'custom-meta-group'),
110  'journal-meta' => array('journal-id', 'journal-title-group', 'contrib-group', 'aff', 'aff-alternatives', 'issn', 'issn-l', 'isbn', 'publisher', 'notes', 'self-uri', 'custom-meta-group'),
111  'counts' => array('count', 'fig-count', 'table-count', 'equation-count', 'ref-count', 'page-count', 'word-count'),
112  );
113 
114  assert(isset($permittedElementOrders[$parentNode->nodeName])); // We have an order list for the parent node
115  $order = $permittedElementOrders[$parentNode->nodeName];
116  $position = array_search($childNode->nodeName, $order);
117  assert($position !== false); // The child node appears in the order list
118 
119  $followingElements = array_slice($order, $position);
120  $followingElement = null;
121  foreach ($parentNode->childNodes as $node) {
122  if (in_array($node->nodeName, $followingElements)) {
123  $followingElement = $node;
124  break;
125  }
126  }
127 
128  return $parentNode->insertBefore($childNode, $followingElement);
129  }
130 
139  protected function _mungeMetadata($doc, $journal, $article, $section, $issue) {
140  $xpath = new DOMXPath($doc);
141  $articleMetaNode = $xpath->query('//article/front/article-meta')->item(0);
142  $journalMetaNode = $xpath->query('//article/front/journal-meta')->item(0);
143  if (!$journalMetaNode) {
144  $frontNode = $xpath->query('//article/front')->item(0);
145  $journalMetaNode = $this->_addChildInOrder($frontNode, $doc->createElement('journal-meta'));
146  }
147 
148  $request = Application::get()->getRequest();
149  $publication = $article->getCurrentPublication();
150 
151  $articleNode = $xpath->query('//article')->item(0);
152  $articleNode->setAttribute('xml:lang', substr($article->getLocale(),0,2));
153  $articleNode->setAttribute('dtd-version', '1.1');
154  $articleNode->setAttribute('specific-use', 'eps-0.1');
155  $articleNode->setAttribute('xmlns', 'https://jats.nlm.nih.gov/publishing/1.1/');
156 
157  // Set the article publication date. http://erudit-ps-documentation.readthedocs.io/en/latest/tagset/element-pub-date.html
158  if ($datePublished = $article->getDatePublished()) {
159  $datePublished = strtotime($datePublished);
160  $match = $xpath->query("//article/front/article-meta/pub-date[@date-type='pub' and @publication-format='epub']");
161  if ($match->length) {
162  // An existing pub-date was found; empty and re-use.
163  $dateNode = $match->item(0);
164  while ($dateNode->hasChildNodes()) $dateNode->removeChild($dateNode->firstChild);
165  } else {
166  // No pub-date was found; create a new one.
167  $dateNode = $this->_addChildInOrder($articleMetaNode, $doc->createElement('pub-date'));
168  $dateNode->setAttribute('date-type', 'pub');
169  $dateNode->setAttribute('publication-format', 'epub');
170  }
171 
172  $dateNode->appendChild($doc->createElement('day'))->nodeValue = strftime('%d', $datePublished);
173  $dateNode->appendChild($doc->createElement('month'))->nodeValue = strftime('%m', $datePublished);
174  $dateNode->appendChild($doc->createElement('year'))->nodeValue = strftime('%Y', $datePublished);
175  }
176 
177  // Set the issue publication date. http://erudit-ps-documentation.readthedocs.io/en/latest/tagset/element-pub-date.html
178  $issueYear = null;
179  if ($issue && $issue->getShowYear()) $issueYear = $issue->getYear();
180  if (!$issueYear && $issue && $issue->getDatePublished()) $issueYear = strftime('%Y', strtotime($issue->getDatePublished()));
181  if (!$issueYear && $datePublished) $issueYear = strftime('%Y', $datePublished);
182  if ($issueYear) {
183  $match = $xpath->query("//article/front/article-meta/pub-date[@date-type='collection']");
184  if ($match->length) {
185  // An existing pub-date was found; empty and re-use.
186  $dateNode = $match->item(0);
187  while ($dateNode->hasChildNodes()) $dateNode->removeChild($dateNode->firstChild);
188  } else {
189  // No pub-date was found; create a new one.
190  $dateNode = $this->_addChildInOrder($articleMetaNode, $doc->createElement('pub-date'));
191  $dateNode->setAttribute('date-type', 'collection');
192  }
193  $dateNode->appendChild($doc->createElement('year'))->nodeValue = $issueYear;
194  }
195 
196  // Remove all article-meta/self-uri nodes in preparation for additions below
197  $match = $xpath->query('//article/front/article-meta/self-uri');
198  foreach ($match as $node) $articleMetaNode->removeChild($node);
199 
200  // Set the article URLs: Landing page
201  $uriNode = $this->_addChildInOrder($articleMetaNode, $doc->createElement('self-uri'));
202  $uriNode->setAttribute('xlink:href', $request->url(null, 'article', 'view', $article->getBestArticleId()));
203 
204  // Set the article URLs: Galleys
205  foreach ($article->getGalleys() as $galley) {
206  $uriNode = $this->_addChildInOrder($articleMetaNode, $doc->createElement('self-uri'));
207  $uriNode->setAttribute('xlink:href', $request->url(null, 'article', 'view', array($article->getBestArticleId(), $galley->getId())));
208  if (!$galley->getData('urlRemote')) $uriNode->setAttribute('content-type', $galley->getFileType());
209  }
210 
211  // Set the issue volume (if applicable).
212  if ($issue && $issue->getShowVolume()) {
213  $match = $xpath->query('//article/front/article-meta/volume');
214  if ($match->length) $volumeNode = $match->item(0);
215  else $volumeNode = $this->_addChildInOrder($articleMetaNode, $doc->createElement('volume'));
216  $volumeNode->nodeValue = $issue->getVolume();
217  }
218 
219  // Set the issue number (if applicable).
220  if ($issue && $issue->getShowNumber()) {
221  $match = $xpath->query('//article/front/article-meta/issue');
222  if ($match->length) $numberNode = $match->item(0);
223  else $numberNode = $this->_addChildInOrder($articleMetaNode, $doc->createElement('issue'));
224  $numberNode->nodeValue = $issue->getNumber();
225  }
226 
227  // Set the issue title (if applicable).
228  if ($issue && $issue->getShowTitle()) {
229  $match = $xpath->query('//article/front/article-meta/issue-title');
230  foreach ($match as $node) $articleMetaNode->removeChild($node);
231  foreach ($issue->getTitle(null) as $locale => $title) {
232  if (empty($title)) continue;
233  $titleNode = $this->_addChildInOrder($articleMetaNode, $doc->createElement('issue-title'));
234  $titleText = $doc->createTextNode($title);
235  $titleNode->appendChild($titleText);
236  $titleNode->setAttribute('xml:lang', substr($locale,0,2));
237  }
238  }
239 
240 
241  // Set the article title.
242  $titleGroupNode = $xpath->query('//article/front/article-meta/title-group')->item(0);
243  while ($titleGroupNode->hasChildNodes()) $titleGroupNode->removeChild($titleGroupNode->firstChild);
244  $titleNode = $titleGroupNode->appendChild($doc->createElement('article-title'));
245  $titleNode->setAttribute('xml:lang', substr($article->getLocale(),0,2));
246  $articleTitleText = $doc->createTextNode($article->getTitle($article->getLocale()));
247  $titleNode->appendChild($articleTitleText);
248  if (!empty($subtitle = $article->getSubtitle($article->getLocale()))) {
249  $subtitleText = $doc->createTextNode($subtitle);
250  $subtitleNode = $titleGroupNode->appendChild($doc->createElement('subtitle'));
251  $subtitleNode->setAttribute('xml:lang', substr($article->getLocale(),0,2));
252  $subtitleNode->appendChild($subtitleText);
253  }
254  foreach ($article->getTitle(null) as $locale => $title) {
255  if ($locale == $article->getLocale()) continue;
256  if (trim($title) === '') continue;
257  $transTitleGroupNode = $titleGroupNode->appendChild($doc->createElement('trans-title-group'));
258  $transTitleGroupNode->setAttribute('xml:lang', substr($locale,0,2));
259  $titleNode = $transTitleGroupNode->appendChild($doc->createElement('trans-title'));
260  $titleText = $doc->createTextNode($title);
261  $titleNode->appendChild($titleText);
262  if (!empty($subtitle = $article->getSubtitle($locale))) {
263  $subtitleNode = $transTitleGroupNode->appendChild($doc->createElement('trans-subtitle'));
264  $subtitleText = $doc->createTextNode($subtitle);
265  $subtitleNode->appendChild($subtitleText);
266  }
267  }
268 
269  // Set the article keywords.
270  $keywordGroupNode = $xpath->query('//article/front/article-meta/kwd-group')->item(0);
271  $submissionKeywordDao = DAORegistry::getDAO('SubmissionKeywordDAO');
272  foreach ($articleMetaNode->getElementsByTagName('kwd-group') as $kwdGroupNode) $articleMetaNode->removeChild($kwdGroupNode);
273  foreach ($submissionKeywordDao->getKeywords($publication->getId(), $journal->getSupportedLocales()) as $locale => $keywords) {
274  if (empty($keywords)) continue;
275 
276  // Load the article.subject locale key in possible other languages
277  AppLocale::requireComponents(LOCALE_COMPONENT_APP_COMMON, $locale);
278 
279  $kwdGroupNode = $this->_addChildInOrder($articleMetaNode, $doc->createElement('kwd-group'));
280  $kwdGroupNode->setAttribute('xml:lang', substr($locale,0,2));
281  $kwdGroupNode->appendChild($doc->createElement('title'))->nodeValue = __('article.subject', array(), $locale);
282  foreach ($keywords as $keyword) $kwdGroupNode->appendChild($doc->createElement('kwd'))->nodeValue = $keyword;
283  }
284 
285  // Set the article abstract.
286  static $purifier;
287  if (!$purifier) {
288  $config = HTMLPurifier_Config::createDefault();
289  $config->set('HTML.Allowed', 'p');
290  $config->set('Cache.SerializerPath', 'cache');
291  $purifier = new HTMLPurifier($config);
292  }
293  foreach ($articleMetaNode->getElementsByTagName('abstract') as $abstractNode) $articleMetaNode->removeChild($abstractNode);
294  foreach ((array) $publication->getData('abstract') as $locale => $abstract) {
295  if (empty($abstract)) continue;
296  $isPrimary = $locale == $article->getLocale();
297  $abstractDoc = new DOMDocument;
298  if (strpos($abstract, '<p>')===null) $abstract = "<p>$abstract</p>";
299  $abstractDoc->loadXML(($isPrimary?'<abstract>':'<trans-abstract>') . $purifier->purify($abstract) . ($isPrimary?'</abstract>':'</trans-abstract>'));
300  $abstractNode = $this->_addChildInOrder($articleMetaNode, $doc->importNode($abstractDoc->documentElement, true));
301  if (!$isPrimary) $abstractNode->setAttribute('xml:lang', substr($locale,0,2));
302  }
303 
304  // Set the journal-id[publisher-id']
305  $match = $xpath->query("//article/front/journal-meta/journal-id[@journal-id-type='publisher']");
306  if ($match->length) $match->item(0)->nodeValue = $journal->getPath();
307  else {
308  $journalIdNode = $this->_addChildInOrder($journalMetaNode, $doc->createElement('journal-id'));
309  $journalIdNode->setAttribute('journal-id-type', 'publisher');
310  $journalIdNode->nodeValue = $journal->getPath();
311  }
312 
313  // Store the DOI
314  if ($doi = trim($article->getStoredPubId('doi'))) {
315  $match = $xpath->query("//article/front/article-meta/article-id[@pub-id-type='doi']");
316  if ($match->length) $match->item(0)->nodeValue = $doi;
317  else {
318  $articleIdNode = $this->_addChildInOrder($articleMetaNode, $doc->createElement('article-id'));
319  $articleIdNode->setAttribute('pub-id-type', 'doi');
320  $articleIdNode->nodeValue = $doi;
321  }
322  }
323 
324  // Override permissions, when not supplied in the document
325  $match = $xpath->query('//article/front/article-meta/permissions');
326  $copyrightHolder = $article->getLocalizedCopyrightHolder($article->getLocale());
327  $copyrightYear = $article->getCopyrightYear();
328  $licenseUrl = $article->getLicenseURL();
329  if (!$match->length && ($copyrightHolder || $copyrightYear || $licenseUrl)) {
330  $permissionsNode = $this->_addChildInOrder($articleMetaNode, $doc->createElement('permissions'));
331  if ($copyrightYear || $copyrightHolder) $permissionsNode->appendChild($doc->createElement('copyright-statement'))->nodeValue = __('submission.copyrightStatement', array('copyrightYear' => $copyrightYear, 'copyrightHolder' => $copyrightHolder));
332  if ($copyrightYear) $permissionsNode->appendChild($doc->createElement('copyright-year'))->nodeValue = $copyrightYear;
333  if ($copyrightHolder) $permissionsNode->appendChild($doc->createElement('copyright-holder'))->nodeValue = $copyrightHolder;
334  if ($licenseUrl) {
335  $licenseNode = $permissionsNode->appendChild($doc->createElement('license'));
336  $licenseNode->setAttribute('xlink:href', $licenseUrl);
337  }
338  }
339 
340  // Section information
341  $match = $xpath->query("//article/front/article-meta/article-categories");
342  if ($match->length) $articleCategoriesNode = $match->item(0);
343  else {
344  $articleCategoriesNode = $this->_addChildInOrder($articleMetaNode, $doc->createElement('article-categories'));
345  }
346  while ($articleCategoriesNode->hasChildNodes()) $articleCategoriesNode->removeChild($articleCategoriesNode->firstChild);
347  foreach ($section->getTitle(null) as $locale => $title) {
348  if (empty($title)) continue;
349  $subjGroupNode = $articleCategoriesNode->appendChild($doc->createElement('subj-group'));
350  $subjGroupNode->setAttribute('subj-group-type', 'heading');
351  $subjGroupNode->setAttribute('xml:lang', substr($locale,0,2));
352  $subjectNode = $subjGroupNode->appendChild($doc->createElement('subject'));
353  $subjectNode->nodeValue = $title;
354  }
355 
356  // Article sequence information
357  import('classes.submission.Submission'); // import STATUS_ constants
358  $publishedArticles = iterator_to_array(Services::get('submission')->getMany([
359  'contextId' => $journal->getId(),
360  'issueIds' => [$issue->getId()],
361  'status' => STATUS_PUBLISHED,
362  ]));
363  $articleIds = array_map(function($publishedArticle) {
364  return $publishedArticle->getId();
365  }, $publishedArticles);
366  foreach (array('volume', 'issue') as $nodeName) {
367  $match = $xpath->query("//article/front/article-meta/$nodeName");
368  if ($match->length) {
369  $match->item(0)->setAttribute('seq', array_search($article->getId(), $articleIds)+1);
370  break;
371  }
372  }
373 
374  // Issue ID
375  $match = $xpath->query("//article/front/article-meta/issue-id");
376  if ($match->length) $match->item(0)->nodeValue = $issue->getId();
377  else {
378  $issueIdNode = $this->_addChildInOrder($articleMetaNode, $doc->createElement('issue-id'));
379  $issueIdNode->nodeValue = $issue->getId();
380  }
381 
382  // Issue cover page
383  if ($coverUrl = $issue->getLocalizedCoverImageUrl()) {
384  $customMetaGroupNode = $this->_addChildInOrder($articleMetaNode, $doc->createElement('custom-meta-group'));
385  $customMetaNode = $customMetaGroupNode->appendChild($doc->createElement('custom-meta'));
386  $metaNameNode = $customMetaNode->appendChild($doc->createElement('meta-name'));
387  $metaNameNode->nodeValue = 'issue-cover';
388  $metaValueNode = $customMetaNode->appendChild($doc->createElement('meta-value'));
389  $inlineGraphicNode = $metaValueNode->appendChild($doc->createElement('inline-graphic'));
390  $inlineGraphicNode->setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
391  $inlineGraphicNode->setAttribute('xlink:href', $coverUrl);
392  }
393 
394  // Article type
395  if ($articleType = strtolower(trim($section->getLocalizedIdentifyType()))) {
396  $articleNode = $xpath->query("//article")->item(0);
397  $articleNode->setAttribute('article-type', $articleType);
398  }
399 
400  // Editorial team
401  $userGroupDao = DAORegistry::getDAO('UserGroupDAO');
402  $userGroups = $userGroupDao->getByContextId($journal->getId());
403  $journalMetaNode = $xpath->query('//article/front/journal-meta')->item(0);
404  $contribGroupNode = $this->_addChildInOrder($journalMetaNode, $doc->createElement('contrib-group'));
405  $keyContribTypeMapping = array(
406  'default.groups.name.manager' => 'jmanager',
407  'default.groups.name.editor' => 'editor',
408  'default.groups.name.sectionEditor' => 'secteditor',
409  );
410  while ($userGroup = $userGroups->next()) {
411  if (!isset($keyContribTypeMapping[$userGroup->getData('nameLocaleKey')])) continue;
412 
413  $users = $userGroupDao->getUsersById($userGroup->getId());
414  while ($user = $users->next()) {
415  $contribNode = $contribGroupNode->appendChild($doc->createElement('contrib'));
416  $contribNode->setAttribute('contrib-type', $keyContribTypeMapping[$userGroup->getData('nameLocaleKey')]);
417  $nameNode = $contribNode->appendChild($doc->createElement('name'));
418  $surname = method_exists($user, 'getLastName')?$user->getLastName():$user->getLocalizedFamilyName();
419  if ($surname != '') {
420  $surnameNode = $nameNode->appendChild($doc->createElement('surname'));
421  $surnameNode->nodeValue = $surname;
422  }
423  $givenNamesNode = $nameNode->appendChild($doc->createElement('given-names'));
424  $givenNamesNode->nodeValue = method_exists($user, 'getFirstName')?$user->getFirstName():$user->getLocalizedGivenName();
425  if (method_exists($user, 'getMiddleName') && $s = $user->getMiddleName()) $givenNamesNode->nodeValue .= " $s";
426  }
427  }
428 
429  }
430 
436  protected function _isCandidateFile($submissionFile) {
437  // The file type isn't XML.
438  if (!in_array($submissionFile->getFileType(), array('application/xml', 'text/xml'))) return false;
439 
440  static $genres = array();
441  $genreDao = DAORegistry::getDAO('GenreDAO');
442  $genreId = $submissionFile->getGenreId();
443  if (!isset($genres[$genreId])) $genres[$genreId] = $genreDao->getById($genreId);
444  assert($genres[$genreId]);
445  $genre = $genres[$genreId];
446 
447  // The genre doesn't look like a main submission document.
448  if ($genre->getCategory() != GENRE_CATEGORY_DOCUMENT) return false;
449  if ($genre->getDependent()) return false;
450  if ($genre->getSupplementary()) return false;
451 
452  return true;
453  }
454 }
OAIMetadataFormat_JATS\_addChildInOrder
_addChildInOrder($parentNode, $childNode)
Definition: OAIMetadataFormat_JATS.inc.php:106
AppLocale\requireComponents
static requireComponents()
Definition: env1/MockAppLocale.inc.php:56
OAIMetadataFormat_JATS\_mungeMetadata
_mungeMetadata($doc, $journal, $article, $section, $issue)
Definition: back.inc.php:127
DAORegistry\getDAO
static & getDAO($name, $dbconn=null)
Definition: DAORegistry.inc.php:57
OAIMetadataFormat
Definition: OAIStruct.inc.php:183
OAIMetadataFormat_JATS\_findJats
_findJats($article, $galleys)
Definition: back.inc.php:22
OAIMetadataFormat_JATS
OAI metadata format class – JATS.
Definition: back.inc.php:21
IssueAction
IssueAction class.
Definition: IssueAction.inc.php:17
OAIMetadataFormat_JATS\toXml
toXml($record, $format=null)
Definition: OAIMetadataFormat_JATS.inc.php:73
OAIMetadataFormat_JATS\_isCandidateFile
_isCandidateFile($submissionFile)
Definition: back.inc.php:312
PKPApplication\get
static get()
Definition: PKPApplication.inc.php:235
HookRegistry\call
static call($hookName, $args=null)
Definition: HookRegistry.inc.php:86
OAIMetadataFormat_JATS\_findJats
_findJats($record)
Definition: OAIMetadataFormat_JATS.inc.php:27
PKPServices\get
static get($service)
Definition: PKPServices.inc.php:49