MODS to RDF
TL; DR
Islandora 8 used the MODS to RDF Mapping Recommendations document from Samvera MODS to RDF Working Group to create initial mappings. Islandora consist of multiple yaml files for different content types corporate_body, family, geo_location, person, resource_type and subject. Create a mapping for each one of these and the bulk of the work is done. YAML files can iterate over several posibilities and output the one desired RDF value. It directly links and pulls data from several namespace APIs with some configuration. Minting object mappings will need the URI sourced from an external vocabulary, URI for a local object, or a literal value (text string). Minting is covered on the OAI-PMH and DOI minting.
Overview
In Islandora, the JSON-LD Module transforms nodes (or media, or taxonomy terms) into the RDF that is synced into Fedora and the Triplestore. It uses RDF mappings, a concept defined by the RDF Module, and exposes them through the REST API at ?_format=jsonld
. Functionality can be expanded by RDF schema building schema.org configuration tool (RDF UI).
JSON-LD Module
Using the RDF mapping configurations provided by the RDF module, the JSON-LD Module exposes the RDF-mapped entity in JSON-LD, through the REST API, at
- Node:
node/[nid]?_format=jsonld
- Media:
media/[mid]?_format=jsonld
- Terms:
taxonomy/term/[tid]?_format=jsonld
RDF Mappings
In an out-of-the-box islandora-playbook, the RDF mappings that exist were loaded from config files, and correspond to the rdf.mapping.[...].yml
files located in:
- Media terms:
/islandora/modules/islandora_core_feature/config/install/
- Taxonomy terms:
/islandora_defaults/config/install/
- Repository_item and the islandora_access vocabulary:
/controlled_access_terms/modules/controlled_access_terms_defaults/config/install/
- The default corporate_body, family, geo_location, person, resource_type and subject vocabularies:
/core/profiles/standard/config/install/ (articles, pages, comments, and tags)
For a detailed description on how to generate and/or edit RDF mappings link
Namespaces ldp, ebucore, pcdm, are premis are registered in islandora.module using hook_rdf_namespaces(). To register your own namespaces, you will need to locate create a custom module that implements that hook.
Sample MODS for knoxgardens:119
<mods
xmlns="http://www.loc.gov/mods/v3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xsi:schemaLocation="http://www.loc.gov/mods/v3 http://www.loc.gov/standards/mods/v3/mods-3-5.xsd">
<identifier type="local">0012_000463_000218</identifier>
<identifier type="pid">knoxgardens:119</identifier>
<identifier type="slide number">Slide 5</identifier>
<identifier type="film number">Film 93</identifier>
<identifier type="spc">record_spc_4493</identifier>
<titleInfo>
<title>Guyot, Chapman & Sequoyah</title>
</titleInfo>
<abstract>Glass slide of mountains, Guyot, Chapman & Sequoyah</abstract>
<originInfo>
<dateCreated qualifier="inferred">1927-1935</dateCreated>
<dateCreated encoding="edtf" point="start" qualifier="inferred" keyDate="yes">1930</dateCreated>
<dateCreated encoding="edtf" point="end" qualifier="inferred">1939</dateCreated>
</originInfo>
<physicalDescription>
<form authority="aat" valueURI="http://vocab.getty.edu/aat/300134977">lantern slides</form>
<extent>3 1/4 x 5 inches</extent>
<internetMediaType>image/jp2</internetMediaType>
</physicalDescription>
<name valueURI="http://id.loc.gov/authorities/names/no2018075078" authority="naf">
<namePart>Jim Thompson Company</namePart>
<role>
<roleTerm authority="marcrelator" valueURI="http://id.loc.gov/vocabulary/relators/pht">Photographer</roleTerm>
</role>
</name>
<subject authority="lcsh" valueURI="http://id.loc.gov/authorities/subjects/sh85090290">
<topic>Nature photography</topic>
</subject>
<subject authority="lcsh" valueURI="http://id.loc.gov/authorities/subjects/sh85087947">
<topic>Mountains--Tennessee</topic>
</subject>
<subject authority="lcsh" valueURI="http://id.loc.gov/authorities/subjects/sh85057008">
<topic>Great Smoky Mountains (N.C. and Tenn.)</topic>
</subject>
<subject authority="geonames" valueURI="http://sws.geonames.org/4626066">
<geographic>Great Smoky Mountains</geographic>
<cartographics>
<coordinates>35.58343, -83.50822</coordinates>
</cartographics>
</subject>
<note>Mrs. A. C. Bruner donated this collection to the University of Tennessee. Creation dates were inferred from the dates associated with the archival collection and the activity dates of the Jim Thompson Company.</note>
<relatedItem displayLabel="Project" type="host">
<titleInfo>
<title>Knoxville Garden Slides</title>
</titleInfo>
</relatedItem>
<typeOfResource>still image</typeOfResource>
<relatedItem displayLabel="Collection" type="host">
<titleInfo>
<title>Knoxville Gardens Slides</title>
</titleInfo>
<identifier>MS.1324</identifier>
<location>
<url>https://n2t.net/ark:/87290/v88w3bgf</url>
</location>
</relatedItem>
<location>
<physicalLocation valueURI="http://id.loc.gov/authorities/names/no2014027633">University of Tennessee, Knoxville. Special Collections</physicalLocation>
</location>
<recordInfo>
<recordContentSource valueURI="http://id.loc.gov/authorities/names/n87808088">University of Tennessee, Knoxville. Libraries</recordContentSource>
<languageOfCataloging>
<languageTerm type="text" authority="iso639-2b">English</languageTerm>
</languageOfCataloging>
</recordInfo>
<accessCondition type="use and reproduction" xlink:href="http://rightsstatements.org/vocab/InC/1.0/">In Copyright</accessCondition>
</mods>
JSON-LD for this object
These values may not be correct. I put them in by hand and I’m not experienced with MODS to RDF conversion.
{
"@graph": [{
"@id": "http:\/\/www.utkislandora8testing.com\/node\/3",
"@type": ["http:\/\/pcdm.org\/models#Object", "http:\/\/purl.org\/coar\/resource_type\/c_c513"],
"http:\/\/schema.org\/author": [{
"@id": "http:\/\/www.utkislandora8testing.com\/user\/1"
}],
"http:\/\/purl.org\/dc\/terms\/title": [{
"@value": "Guyot, Chapman",
"@language": "en"
}],
"http:\/\/schema.org\/dateCreated": [{
"@value": "2020-07-06T18:46:09+00:00",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#dateTime"
}],
"http:\/\/schema.org\/dateModified": [{
"@value": "2020-07-06T20:09:15+00:00",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#dateTime"
}],
"http:\/\/purl.org\/dc\/terms\/description": [{
"@value": "Glass slide of mountains, Guyot, Chapman & Sequoyah",
"@language": "en"
}],
"http:\/\/purl.org\/dc\/terms\/created": [{
"@value": "1927",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#string"
}, {
"@value": "1927",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#gYear"
}],
"http:\/\/purl.org\/dc\/terms\/extent": [{
"@value": "1 item",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#string"
}],
"http:\/\/purl.org\/dc\/terms\/spatial": [{
"@id": "http:\/\/www.utkislandora8testing.com\/taxonomy\/term\/34"
}],
"http:\/\/purl.org\/dc\/terms\/identifier": [{
"@value": "MS.1324",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#string"
}, {
"@value": "Slide 5",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#string"
}, {
"@value": "Film 93",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#string"
}, {
"@value": "record_spc_4493",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#string"
}, {
"@value": "0012_000463_000218",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#string"
}],
"http:\/\/purl.org\/dc\/terms\/subject": [{
"@value": "Jim Thompson Company",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#string"
}, {
"@value": "Photographer",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#string"
}, {
"@value": "Nature photography",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#string"
}, {
"@value": "Mountains--Tennessee",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#string"
}, {
"@value": "Great Smoky Mountains (N.C. and Tenn.)",
"@type": "http:\/\/www.w3.org\/2001\/XMLSchema#string"
}, {
"@id": "http:\/\/www.utkislandora8testing.com\/taxonomy\/term\/31"
}, {
"@id": "http:\/\/www.utkislandora8testing.com\/taxonomy\/term\/32"
}, {
"@id": "http:\/\/www.utkislandora8testing.com\/taxonomy\/term\/33"
}, {
"@id": "http:\/\/www.utkislandora8testing.com\/taxonomy\/term\/34"
}],
"http:\/\/pcdm.org\/models#memberOf": [{
"@id": "http:\/\/www.utkislandora8testing.com\/node\/2"
}],
"http:\/\/www.w3.org\/2004\/02\/skos\/core#note": [{
"@value": "<p>Mrs. A. C. Bruner donated this collection to the University of Tennessee. Creation dates were inferred from the dates associated with the archival collection and the activity dates of the Jim Thompson Company.<\/p>\r\n",
"@language": "en"
}],
"http:\/\/purl.org\/dc\/terms\/type": [{
"@id": "http:\/\/www.utkislandora8testing.com\/taxonomy\/term\/12"
}],
"http:\/\/schema.org\/sameAs": [{
"@id": "http:\/\/www.utkislandora8testing.com\/node\/3"
}]
}, {
"@id": "http:\/\/www.utkislandora8testing.com\/user\/1",
"@type": ["http:\/\/schema.org\/Person"]
}, {
"@id": ["http:\/\/www.utkislandora8testing.com\/taxonomy\/term\/34", "http:\/\/www.utkislandora8testing.com\/taxonomy\/term\/34"],
"@type": ["http:\/\/purl.org\/dc\/terms\/Location", "http:\/\/schema.org\/Place", "http:\/\/purl.org\/dc\/terms\/Location", "http:\/\/schema.org\/Place"]
}, {
"@id": "http:\/\/www.utkislandora8testing.com\/node\/2",
"@type": ["http:\/\/pcdm.org\/models#Object"]
}, {
"@id": "http:\/\/www.utkislandora8testing.com\/taxonomy\/term\/12",
"@type": ["http:\/\/schema.org\/Thing"]
}, {
"@id": "http:\/\/www.utkislandora8testing.com\/taxonomy\/term\/31",
"@type": ["http:\/\/schema.org\/Thing"]
}, {
"@id": "http:\/\/www.utkislandora8testing.com\/taxonomy\/term\/32",
"@type": ["http:\/\/schema.org\/Thing"]
}, {
"@id": "http:\/\/www.utkislandora8testing.com\/taxonomy\/term\/33",
"@type": ["http:\/\/schema.org\/Thing"]
}]
}
An example of a custom RDF Mapping
Below is an example of an RDF mapping as a .yml (YAML) file. It is the current version of the RDF mapping of the Repository Item (islandora_object) bundle, provided by islandora_defaults and exportable as rdf.mapping.node.islandora_object.yml).
The top level key types specifies the rdf:type of the resource or content model. field_model, a required field of Islandora objects, also gets mapped to rdf:type through an arcane back-end process. The top level key fieldMappings specifies fields attached to that bundle and their RDF property mappings. One field can be mapped to more than one RDF property. It is a simple flat list. datatype_callback : [needs documentation] mapping_type: rel : [needs documentation]
To see your current RDF for an Islandora Object go to the Synchronize Export Single item page (/admin/config/development/configuration/single/export) and select Configuration type: RDF Mapping Configuration name: node.islandora_object
Looking over this configuration the field_rights
properties is set to dc:rights
. This means currently the rights statement requires that this field needs to be set for every object/node with this rights statement and if the statement changes all of them requires an edit. Although mostly trivial this isn’t very dynamic and doesn’t really follow the principles of linked data.
langcode: en
status: true
dependencies:
config:
- node.type.islandora_object
enforced:
module:
- islandora_defaults
module:
- node
id: node.islandora_object
targetEntityType: node
bundle: islandora_object
types:
- 'pcdm:Object'
fieldMappings:
field_alternative_title:
properties:
- 'dc:alternative'
field_edtf_date:
properties:
- 'dc:date'
datatype_callback:
callable: 'Drupal\controlled_access_terms\EDTFConverter::dateIso8601Value'
field_edtf_date_created:
properties:
- 'dc:created'
datatype_callback:
callable: 'Drupal\controlled_access_terms\EDTFConverter::dateIso8601Value'
field_edtf_date_issued:
properties:
- 'dc:issued'
datatype_callback:
callable: 'Drupal\controlled_access_terms\EDTFConverter::dateIso8601Value'
field_description:
properties:
- 'dc:description'
field_extent:
properties:
- 'dc:extent'
field_identifier:
properties:
- 'dc:identifier'
field_member_of:
properties:
- 'pcdm:memberOf'
mapping_type: rel
field_resource_type:
properties:
- 'dc:type'
mapping_type: rel
field_rights:
properties:
- 'dc:rights'
field_subject:
properties:
- 'dc:subject'
mapping_type: rel
field_weight:
properties:
- 'co:index'
title:
properties:
- 'dc:title'
created:
properties:
- 'schema:dateCreated'
datatype_callback:
callable: 'Drupal\rdf\CommonDataConverter::dateIso8601Value'
changed:
properties:
- 'schema:dateModified'
datatype_callback:
callable: 'Drupal\rdf\CommonDataConverter::dateIso8601Value'
uid:
properties:
- 'schema:author'
mapping_type: rel
How to create a custom vocab
Moving the rights statement in it’s own taxonomy vocabularly adds the ability to show the rights statement and URI link to rightsstatement.org. To accomplish this:
- Make a Taxonomy Vocabulary for “Rights Statement”
- Either migrate or add in each “term” like
In Copyright
giving it an authority link, name and description. - Create a mapping for the “Rights Statement” vocabulary by importing via a single Import configuration page.
- Replace the Rights string field with a with a Rights Entity Reference pointing to the new vocab.
For more information see Structure of an RDF mapping and more on the mapping of RightsStatesments.org mappings see Rightsstatements.org White Paper: Requirements for the Technical Infrastructure for Standardized International Rights Statements
Advanced
A solution for handling rights statement. Using a link field that includes the URI of the rights statement as the link value and the term for the link label with a drop-down list of available rights statement. Another solution with the JSON-LD uses the URL for the rights statement instead of a taxonomy-term surrogate URL. Another option is to create a rights statement taxonomy and then create a formatter that use’s the term’s field_external_authority, or whatever the URI field is, to create a link on the node’s page:
<?php
namespace Drupal\islandora_local\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceFormatterBase;
use Drupal\link\LinkItemInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'LinkedEntityLink'.
*
* @FieldFormatter(
* id = "linked_entity_link",
* label = @Translation("Linked Entity Link"),
* field_types = {
* "entity_reference"
* }
* )
*/
class LinkedEntityLink extends EntityReferenceFormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return [
'link_field' => '',
] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$settings = $this->getSettings();
$link_fields = [];
foreach (\Drupal::entityManager()->getFieldMap() as $entity_type => $fields) {
foreach ($fields as $field_name => $field_info) {
if ($field_info['type'] === 'link') {
$link_fields[$field_name] = $field_name;
}
}
}
$element['link_field'] = [
'#type' => 'select',
'#title' => t('Link Field'),
'#options' => $link_fields,
'#required' => TRUE,
'#default_value' => $settings['link_field'],
];
return $element;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$element = [];
foreach ($this->getEntitiesToView($items, $langcode) as $delta => $entity) {
try {
$field = $entity->get($this->getSetting('link_field'));
if (!empty($field)) {
foreach ($entity->get('field_project_site_url') as $delta2 => $item) {
$link_title = $entity->label();
$url = $this->buildUrl($item);
$element[$delta] = [
'#type' => 'link',
'#title' => $link_title,
];
$element[$delta]['#url'] = $url;
if (!empty($item->_attributes)) {
$element[$delta]['#options'] += [
'attributes' => [],
];
$element[$delta]['#options']['attributes'] += $item->_attributes;
// Unset field item attributes since they have been included in the
// formatter output and should not be rendered in the field template.
unset($item->_attributes);
}
}
}
else {
$element[$delta] = $entity->toLink()->toRenderable();
}
}
catch (\InvalidArgumentException $e) {
\Drupal::logger('islandora_local')->warning('LinkedEntityLink attempted to use an invalid field: ' . $e->getMessage());
$element[$delta] = $entity->toLink()->toRenderable();
}
}
return $element;
}
/**
* Builds the \Drupal\Core\Url object for a link field item.
*
* Stolen from Drupal\link\Plugin\Field\FieldFormatter, which we don't extend.
*
* @param \Drupal\link\LinkItemInterface $item
* The link field item being rendered.
*
* @return \Drupal\Core\Url
* A Url object.
*/
protected function buildUrl(LinkItemInterface $item) {
$url = $item
->getUrl() ?: Url::fromRoute('<none>');
$options = $item->options;
$options += $url
->getOptions();
$url
->setOptions($options);
return $url;
}
}
This can redirect users to the relevant site instead of to the taxonomy term page.
Another solution attaching an attribution to media includes a list of CC licenses in a taxonomy vocabulary that gets bulk-loaded on installation from a YML file. This module is an easy way to bulk load licenses. It needs to specify the short name, long description, image URL and license definition URL in the YML file to include it or create one when the module is installed.