Skip to content

Commit bea2d7b

Browse files
committed
Add EMLAnnotation methods for canonical datasets
- Add methods to add, remove, update, and find sameAs and derivedFrom annotations representing canonical datasets Issue #2542
1 parent 6189421 commit bea2d7b

File tree

3 files changed

+201
-3
lines changed

3 files changed

+201
-3
lines changed

src/js/collections/metadata/eml/EMLAnnotations.js

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ define(["underscore", "backbone", "models/metadata/eml211/EMLAnnotation"], (
55
Backbone,
66
EMLAnnotation,
77
) => {
8+
const SCHEMA_ORG_SAME_AS = "http://www.w3.org/2002/07/owl#sameAs";
9+
const PROV_WAS_DERIVED_FROM = "http://www.w3.org/ns/prov#wasDerivedFrom";
810
/**
911
* @class EMLAnnotations
1012
* @classdesc A collection of EMLAnnotations.
@@ -56,6 +58,135 @@ define(["underscore", "backbone", "models/metadata/eml211/EMLAnnotation"], (
5658
}
5759
this.add(annotation);
5860
},
61+
62+
/**
63+
* Find all annotations with the given propertyURI.
64+
* @param {string} propertyURI The propertyURI to search for.
65+
* @returns {EMLAnnotation[]} An array of EMLAnnotations with the given
66+
* propertyURI.
67+
* @since 0.0.0
68+
*/
69+
findByProperty(propertyURI) {
70+
return this.where({ propertyURI });
71+
},
72+
73+
/**
74+
* Adds canonical dataset annotations to this collection. A canonical
75+
* dataset is the one that is considered the authoritative version; the
76+
* current EML doc being essentially a duplicate version.
77+
* @param {string} sourceId The DOI or URL of the canonical dataset.
78+
* @returns {void}
79+
* @since 0.0.0
80+
*/
81+
addCanonicalDatasetAnnotation(sourceId) {
82+
if (!sourceId) return null;
83+
// TODO: Check that sourceId is a valid DOI or URL
84+
85+
// TODO: Check that there is not already a canonical dataset annotation
86+
// before adding a new one, since there should only be one.
87+
return this.add([
88+
{
89+
propertyLabel: "derivedFrom",
90+
propertyURI: PROV_WAS_DERIVED_FROM,
91+
valueLabel: sourceId,
92+
valueURI: sourceId,
93+
},
94+
{
95+
propertyLabel: "sameAs",
96+
propertyURI: SCHEMA_ORG_SAME_AS,
97+
valueLabel: sourceId,
98+
valueURI: sourceId,
99+
},
100+
]);
101+
},
102+
103+
/**
104+
* Find the annotations that make up the canonical dataset annotation. A
105+
* canonical dataset is identified by having both a "derivedFrom" and a
106+
* "sameAs" annotation with the same DOI or URL for the valueURI.
107+
* @returns {object} An object with the derivedFrom and sameAs
108+
* annotations.
109+
* @since 0.0.0
110+
*/
111+
findCanonicalDatasetAnnotation() {
112+
// There must be at least one derivedFrom and one sameAs annotation
113+
// for this to have a canonical dataset annotation
114+
if (!this.length) return null;
115+
const derivedFrom = this.findByProperty(PROV_WAS_DERIVED_FROM);
116+
if (!derivedFrom?.length) return null;
117+
const sameAs = this.findByProperty(SCHEMA_ORG_SAME_AS);
118+
if (!sameAs?.length) return null;
119+
120+
// Find all pairs that have matching valueURIs
121+
const pairs = [];
122+
derivedFrom.forEach((derived) => {
123+
sameAs.forEach((same) => {
124+
if (derived.get("valueURI") === same.get("valueURI")) {
125+
// TODO? Check that the URI is a valid DOI or URL
126+
pairs.push({ derived, same, uri: derived.get("valueURI") });
127+
}
128+
});
129+
});
130+
131+
// If there are multiple pairs, we cannot determine which is the
132+
// canonical dataset.
133+
if (pairs.length > 1 || !pairs.length) return null;
134+
135+
// There is only one pair, so return it
136+
return pairs[0];
137+
},
138+
139+
/**
140+
* Updates the canonical dataset annotations to have the given ID. If
141+
* there is no canonical dataset annotation, one is added. If the ID is a
142+
* falsy value, the canonical dataset annotation is removed.
143+
* @param {string} newSourceId The DOI or URL of the canonical dataset.
144+
* @returns {object} An object with the derivedFrom and sameAs annotations
145+
* if the canonical dataset annotations were updated.
146+
* @since 0.0.0
147+
*/
148+
updateCanonicalDataset(newSourceId) {
149+
if (!newSourceId) {
150+
this.removeCanonicalDatasetAnnotation();
151+
return null;
152+
}
153+
const canonical = this.findCanonicalDatasetAnnotation();
154+
if (!canonical) {
155+
return this.addCanonicalDatasetAnnotation(newSourceId);
156+
}
157+
158+
const { derived, same, uri } = canonical;
159+
if (uri === newSourceId) return null;
160+
161+
derived.set("valueURI", newSourceId);
162+
derived.set("valueLabel", newSourceId);
163+
same.set("valueURI", newSourceId);
164+
same.set("valueLabel", newSourceId);
165+
166+
return [derived, same];
167+
},
168+
169+
/**
170+
* Removes the canonical dataset annotations from this collection.
171+
* @returns {EMLAnnotation[]} The canonical dataset annotations that were
172+
* removed.
173+
* @since 0.0.0
174+
*/
175+
removeCanonicalDatasetAnnotation() {
176+
const canonical = this.findCanonicalDatasetAnnotation();
177+
if (!canonical) return null;
178+
return this.remove([canonical.derived, canonical.same]);
179+
},
180+
181+
/**
182+
* Returns the URI of the canonical dataset.
183+
* @returns {string} The URI of the canonical dataset.
184+
* @since 0.0.0
185+
*/
186+
getCanonicalURI() {
187+
const canonical = this.findCanonicalDatasetAnnotation();
188+
return canonical?.uri;
189+
},
59190
},
60191
);
61192

src/js/models/metadata/eml211/EMLAnnotation.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
2323
},
2424

2525
initialize: function (attributes, opions) {
26-
this.on("change", this.trickleUpChange);
26+
this.stopListening(this, "change", this.trickleUpChange);
27+
this.listenTo(this, "change", this.trickleUpChange);
2728
},
2829

2930
parse: function (attributes, options) {
@@ -175,7 +176,7 @@ define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
175176

176177
/* Let the top level package know of attribute changes from this object */
177178
trickleUpChange: function () {
178-
MetacatUI.rootDataPackage.packageModel.set("changed", true);
179+
MetacatUI.rootDataPackage.packageModel?.set("changed", true);
179180
},
180181
},
181182
);

test/js/specs/unit/collections/metadata/eml/EMLAnnotations.spec.js

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ define([
77
const should = chai.should();
88
const expect = chai.expect;
99

10-
describe("Accordion Test Suite", () => {
10+
const SCHEMA_ORG_SAME_AS = "http://www.w3.org/2002/07/owl#sameAs";
11+
const PROV_WAS_DERIVED_FROM = "http://www.w3.org/ns/prov#wasDerivedFrom";
12+
13+
describe("EML Annotations Test Suite", () => {
1114
const state = cleanState(() => {
1215
const annotations = new EMLAnnotations({
1316
propertyLabel: "Property Label",
@@ -45,5 +48,68 @@ define([
4548
state.annotations.remove(state.annotations.at(0));
4649
state.annotations.length.should.equal(0);
4750
});
51+
52+
it("finds annotations by property", () => {
53+
state.annotations
54+
.findByProperty("http://example.com/property")
55+
.length.should.equal(1);
56+
});
57+
58+
it("adds canonical dataset annotations", () => {
59+
const annotations =
60+
state.annotations.addCanonicalDatasetAnnotation("http://example.com");
61+
state.annotations.length.should.equal(3);
62+
annotations[0].get("propertyURI").should.equal(PROV_WAS_DERIVED_FROM);
63+
annotations[0].get("valueURI").should.equal("http://example.com");
64+
annotations[1].get("propertyURI").should.equal(SCHEMA_ORG_SAME_AS);
65+
annotations[1].get("valueURI").should.equal("http://example.com");
66+
});
67+
68+
it("finds canonical dataset annotations", () => {
69+
state.annotations.addCanonicalDatasetAnnotation("http://example.com");
70+
const annotations = state.annotations.findCanonicalDatasetAnnotation();
71+
annotations.should.have.property("derived");
72+
annotations.should.have.property("same");
73+
annotations.derived.get("valueURI").should.equal("http://example.com");
74+
annotations.same.get("valueURI").should.equal("http://example.com");
75+
annotations.derived
76+
.get("propertyURI")
77+
.should.equal(PROV_WAS_DERIVED_FROM);
78+
annotations.same.get("propertyURI").should.equal(SCHEMA_ORG_SAME_AS);
79+
});
80+
81+
it("updates canonical dataset annotations", () => {
82+
state.annotations.addCanonicalDatasetAnnotation("http://example.com");
83+
state.annotations.updateCanonicalDataset("http://newexample.com");
84+
state.annotations.getCanonicalURI().should.equal("http://newexample.com");
85+
});
86+
87+
it("removes canonical dataset annotations", () => {
88+
state.annotations.addCanonicalDatasetAnnotation("http://example.com");
89+
state.annotations.removeCanonicalDatasetAnnotation();
90+
state.annotations.length.should.equal(1);
91+
});
92+
93+
it("gets the URI of the canonical dataset", () => {
94+
state.annotations.addCanonicalDatasetAnnotation("http://example.com");
95+
state.annotations.getCanonicalURI().should.equal("http://example.com");
96+
});
97+
98+
it("adds annotations if they didn't exist when updating", () => {
99+
state.annotations.updateCanonicalDataset("http://example.com");
100+
state.annotations.length.should.equal(3);
101+
});
102+
103+
it("removes canonical dataset annotations if the ID is falsy", () => {
104+
state.annotations.addCanonicalDatasetAnnotation("http://example.com");
105+
state.annotations.updateCanonicalDataset("");
106+
state.annotations.length.should.equal(1);
107+
});
108+
109+
it("does not update canonical dataset annotations if the ID is the same", () => {
110+
state.annotations.addCanonicalDatasetAnnotation("http://example.com");
111+
state.annotations.updateCanonicalDataset("http://example.com");
112+
state.annotations.length.should.equal(3);
113+
});
48114
});
49115
});

0 commit comments

Comments
 (0)