"use strict";
define([
"jquery",
"underscore",
"backbone",
"rdflib",
"uuid",
"md5",
"collections/SolrResults",
"models/filters/Filter",
"models/DataONEObject",
"models/metadata/ScienceMetadata",
"models/metadata/eml211/EML211",
], (
$,
_,
Backbone,
rdf,
uuid,
md5,
SolrResults,
Filter,
DataONEObject,
ScienceMetadata,
EML211,
) => {
/**
* @class DataPackage
* @classdesc A DataPackage represents a hierarchical collection of packages,
* metadata, and data objects, modeling an OAI-ORE RDF graph.
* @classcategory Collections
* @name DataPackage
* @augments Backbone.Collection
* @class
*/
const DataPackage = Backbone.Collection.extend(
/** @lends DataPackage.prototype */ {
/**
* The name of this type of collection
* @type {string}
*/
type: "DataPackage",
/**
* The package identifier
* @type {string}
*/
id: null,
/**
* The type of the object (DataPackage, Metadata, Data) Simple queue to
* enqueue file transfers. Use push() and shift() to add and remove items.
* If this gets to large/slow, possibly switch to
* http://code.stephenmorley.org/javascript/queues/
* @type {DataPackage|Metadata|Data[]}
*/
transferQueue: [],
/**
* A flag used for the package's edit status. Can be set to false to
* 'lock' the package
* @type {boolean}
*/
editable: true,
/**
* The RDF graph representing this data package
* @type {RDFGraph}
*/
dataPackageGraph: null,
/**
* A DataONEObject representing the resource map itself
* @type {DataONEObject}
*/
packageModel: null,
/**
* The science data identifiers associated with this data package (from
* cito:documents), mapped to the science metadata identifier that
* documents it. Not to be changed after initial fetch - this is to keep
* track of the relationships in their original state
* @type {object}
*/
originalIsDocBy: {},
/**
* An array of ids that are aggregated in the resource map on the server.
* Taken from the original RDF XML that was fetched from the server. Used
* for comparing the original aggregation with the aggregation of this
* collection.
* @type {string[]}
*/
originalMembers: [],
/**
* Used to keep the collection sorted by model "sortOrder". The three
* model types are ordered as: Metadata: 1; Data: 2; DataPackage: 3. See
* getMember(). We do this so that Metadata get rendered first, and Data
* are rendered as DOM siblings of the Metadata rows of the DataPackage
* table.
* @type {string}
*/
comparator: "sortOrder",
/**
* The nesting level in a data package hierarchy
* @type {number}
*/
nodeLevel: 0,
/**
* The SolrResults collection associated with this DataPackage. This can
* be used to fetch the package from Solr by passing the 'fromIndex'
* option to fetch().
* @type {SolrResults}
*/
solrResults: new SolrResults(),
/**
* A Filter model that should filter the Solr index for only the objects
* aggregated by this package.
* @type {Filter}
*/
filterModel: null,
/**
* Namespaces used in the RDF XML. The key is the prefix and the value is
* the namespace URI.
* @type {object}
*/
namespaces: {
RDF: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
FOAF: "http://xmlns.com/foaf/0.1/",
OWL: "http://www.w3.org/2002/07/owl#",
DC: "http://purl.org/dc/elements/1.1/",
ORE: "http://www.openarchives.org/ore/terms/",
DCTERMS: "http://purl.org/dc/terms/",
CITO: "http://purl.org/spar/cito/",
XSD: "http://www.w3.org/2001/XMLSchema#",
PROV: "http://www.w3.org/ns/prov#",
PROVONE: "http://purl.dataone.org/provone/2015/01/15/ontology#",
},
/**
* Package members that are sources in provenance relationships.
* @type {DataONEObject[]}
*/
sources: [],
/**
* Package members that are derivations in provenance relationships.
* @type {DataONEObject[]}
*/
derivations: [],
/**
* Set to "complete" to signal that all prov queries have finished
* @type {string|null}
*/
provenanceFlag: null,
/**
* Contains provenance relationships added or deleted to this
* DataONEObject. Each entry is [operation ('add' or 'delete'), prov field
* name, object id], i.e. ['add', 'prov_used', 'urn:uuid:5678']
* @type {string[][]}
*/
provEdits: [],
/**
* The number of models that have been updated during the current save().
* This is reset to zero after the current save() is complete.
* @type {number}
*/
numSaves: 0,
/** @inheritdoc */
initialize(_models, options = {}) {
// Create an rdflib reference
this.rdf = rdf;
// Create an initial RDF graph
this.dataPackageGraph = this.rdf.graph();
// Set the id or create a new one
this.id = options.id || `resource_map_urn:uuid:${uuid.v4()}`;
const packageModelAttrs = options.packageModelAttrs || {};
if (typeof options.packageModel !== "undefined") {
// use the given package model
this.packageModel = new DataONEObject(options.packageModel);
} else {
// Create a DataONEObject to represent this resource map
this.packageModel = new DataONEObject(
_.extend(packageModelAttrs, {
formatType: "RESOURCE",
type: "DataPackage",
formatId: "http://www.openarchives.org/ore/terms",
childPackages: {},
id: this.id,
latestVersion: this.id,
}),
);
}
this.id = this.packageModel.id;
// Create a Filter for this DataPackage using the id
this.filterModel = new Filter({
fields: ["resourceMap"],
values: [this.id],
matchSubstring: false,
});
// If the id is ever changed, update the id in the Filter
this.listenTo(this.packageModel, "change:id", () => {
this.filterModel.set("values", [this.packageModel.get("id")]);
});
this.on("add", this.handleAdd);
this.on("add", this.triggerComplete);
this.on("successSaving", this.updateRelationships);
return this;
},
/**
* Build the DataPackage URL based on the
* MetacatUI.appModel.objectServiceUrl and id or seriesid
* @param {object} [options] - Optional options for this URL
* @param {boolean} [options.update] - If true, this URL will be for
* updating the package
* @returns {string} The URL for this DataPackage
*/
url(options) {
if (options && options.update) {
return (
MetacatUI.appModel.get("objectServiceUrl") +
(encodeURIComponent(this.packageModel.get("oldPid")) ||
encodeURIComponent(this.packageModel.get("seriesid")))
);
}
// URL encode the id or seriesId
const encodedId =
encodeURIComponent(this.packageModel.get("id")) ||
encodeURIComponent(this.packageModel.get("seriesid"));
// Use the object service URL if it is available (when pointing to a MN)
if (MetacatUI.appModel.get("objectServiceUrl")) {
return MetacatUI.appModel.get("objectServiceUrl") + encodedId;
}
// Otherwise, use the resolve service URL (when pointing to a CN)
return MetacatUI.appModel.get("resolveServiceUrl") + encodedId;
},
/**
* The DataPackage collection stores DataPackages and DataONEObjects,
* including Metadata and Data objects. Return the correct model based on
* the type
* @param {object} attrs - The attributes of the model
* @param {object} options - Options to pass to the instantiated model
* @returns {DataONEObject|ScienceMetadata|EML211|DataPackage} The model
*/
// eslint-disable-next-line object-shorthand, func-names
model: function (attrs, options) {
switch (attrs.formatid) {
case "http://www.openarchives.org/ore/terms":
return new DataPackage(null, { packageModel: attrs }); // TODO: is this correct?
case "eml://ecoinformatics.org/eml-2.0.0":
return new EML211(attrs, options);
case "eml://ecoinformatics.org/eml-2.0.1":
return new EML211(attrs, options);
case "eml://ecoinformatics.org/eml-2.1.0":
return new EML211(attrs, options);
case "eml://ecoinformatics.org/eml-2.1.1":
return new EML211(attrs, options);
case "-//ecoinformatics.org//eml-access-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-access-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-attribute-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-attribute-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-constraint-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-constraint-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-coverage-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-coverage-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-dataset-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-dataset-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-distribution-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-distribution-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-entity-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-entity-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-literature-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-literature-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-party-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-party-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-physical-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-physical-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-project-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-project-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-protocol-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-protocol-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-resource-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-resource-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-software-2.0.0beta4//EN":
return new ScienceMetadata(attrs, options);
case "-//ecoinformatics.org//eml-software-2.0.0beta6//EN":
return new ScienceMetadata(attrs, options);
case "FGDC-STD-001-1998":
return new ScienceMetadata(attrs, options);
case "FGDC-STD-001.1-1999":
return new ScienceMetadata(attrs, options);
case "FGDC-STD-001.2-1999":
return new ScienceMetadata(attrs, options);
case "INCITS-453-2009":
return new ScienceMetadata(attrs, options);
case "ddi:codebook:2_5":
return new ScienceMetadata(attrs, options);
case "http://datacite.org/schema/kernel-3.0":
return new ScienceMetadata(attrs, options);
case "http://datacite.org/schema/kernel-3.1":
return new ScienceMetadata(attrs, options);
case "http://datadryad.org/profile/v3.1":
return new ScienceMetadata(attrs, options);
case "http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd":
return new ScienceMetadata(attrs, options);
case "http://ns.dataone.org/metadata/schema/onedcx/v1.0":
return new ScienceMetadata(attrs, options);
case "http://purl.org/dryad/terms/":
return new ScienceMetadata(attrs, options);
case "http://purl.org/ornl/schema/mercury/terms/v1.0":
return new ScienceMetadata(attrs, options);
case "http://rs.tdwg.org/dwc/xsd/simpledarwincore/":
return new ScienceMetadata(attrs, options);
case "http://www.cuahsi.org/waterML/1.0/":
return new ScienceMetadata(attrs, options);
case "http://www.cuahsi.org/waterML/1.1/":
return new ScienceMetadata(attrs, options);
case "http://www.esri.com/metadata/esriprof80.dtd":
return new ScienceMetadata(attrs, options);
case "http://www.icpsr.umich.edu/DDI":
return new ScienceMetadata(attrs, options);
case "http://www.isotc211.org/2005/gmd":
return new ScienceMetadata(attrs, options);
case "http://www.isotc211.org/2005/gmd-noaa":
return new ScienceMetadata(attrs, options);
case "http://www.loc.gov/METS/":
return new ScienceMetadata(attrs, options);
case "http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2":
return new ScienceMetadata(attrs, options);
default:
return new DataONEObject(attrs, options);
}
},
/**
* Fetches member models in batches to avoid fetching all members
* simultaneously.
* @param {Backbone.Model[]} models The array of member models to fetch.
* @param {number} [batchSize] - The number of models to fetch in each
* batch.
* @param {number} [timeout] -The timeout for each fetch request in
* milliseconds. If set to anything other than a positive number greater
* than 0, the fetch will never timeout.
* @param {number} [maxRetries] - The maximum number of retries for each
* fetch request.
* @returns {Promise} A promise that resolves when all models have been
* fetched.
* @since 2.32.0
*/
async fetchMemberModels(
models,
batchSize = 10,
timeout = 5000,
maxRetries = 3,
) {
const numModels = models.length;
// If batchSize is 0, fetch everything at once
const effectiveBatchSize = batchSize || numModels;
// If timeout is 0, falsey, or not a positive number, then fetch without
// a timeout (i.e., wait indefinitely)
const effectiveTimeout = timeout;
// Loading count is used to show users progress of fetching
this.updateLoadingCount(numModels);
// Models will be fetched asynchonously within batches; batches are
// processed sequentially
for (let i = 0; i < numModels; i += effectiveBatchSize) {
const batch = models.slice(i, i + effectiveBatchSize);
// Fetch all models in the batch
const fetchPromises = batch.map((memberModel) =>
this.fetchMemberModel(memberModel, maxRetries, effectiveTimeout),
);
// Await the entire batch of fetches to complete before moving to the
// next batch
/* eslint-disable-next-line no-await-in-loop */
const results = await Promise.allSettled(fetchPromises);
// Handle any rejections
const errors = results.filter((r) => r.status === "rejected");
if (errors.length > 0) {
this.handleMemberFetchError(batch, errors);
}
const modelsProcessed = i + effectiveBatchSize;
this.updateLoadingCount(Math.max(0, numModels - modelsProcessed));
}
this.triggerComplete();
return this;
},
async fetchMemberModel(memberModel, maxRetries, effectiveTimeout) {
try {
// First wait for the model data to be fetched & synced
await this.fetchWithRetryAndTimeout(
memberModel,
maxRetries,
effectiveTimeout,
);
// Make sure the model is the correct type & merged into the
// collection. Fetch again if type changed.
const newModel = await this.updateMemberModelType(
memberModel,
maxRetries,
effectiveTimeout,
);
return newModel;
} catch (err) {
this.handleMemberFetchError([memberModel], [err]);
return null;
}
},
/**
* Fetch a model with a timeout and a maximum number of retries. Fetch a
* model with a timeout, aborting the fetch if it takes too long.
* @param {DataONEObject} memberModel - The model to fetch
* @param {number} maxRetries - The maximum number of retries
* @param {number} timeout - The timeout in milliseconds. If set to
* anything other than a positive number greater than 0, the fetch will
* not have a timeout (i.e., will wait indefinitely)
* @param {number} [attempt] - The current attempt number
* @returns {DataONEObject} The fetched model
* @since 2.32.1
*/
async fetchWithRetryAndTimeout(
memberModel,
maxRetries,
timeout,
attempt = 0,
) {
let timerId;
try {
// Kick off the real fetch plus a timeout
const { fetchPromise, xhrRef } = this.fetchPromise(memberModel);
// if timeout is not a number > 0, then wait for fetch indefinitely
if (typeof timeout !== "number" || timeout <= 0) {
return await fetchPromise;
}
const timerPromise = new Promise((_resolve, reject) => {
timerId = setTimeout(() => {
if (xhrRef?.abort) {
xhrRef.abort();
}
reject(new Error("Fetch timed out"));
}, timeout);
});
// Wait for whichever finishes first
return await Promise.race([fetchPromise, timerPromise]);
} catch (err) {
// Retry if we still have attempts left
if (attempt >= maxRetries - 1) {
throw err;
}
// Recursively call ourselves with an incremented attempt count
return this.fetchWithRetryAndTimeout.call(
this,
memberModel,
maxRetries,
timeout,
attempt + 1,
);
} finally {
if (timerId) {
clearTimeout(timerId);
}
}
},
/**
* Handle errors that occur when fetching member models
* @param {DataONEObject[]} failedModels - The models that were being
* fetched
* @param {Error[]} errors - The errors that occurred
* @since 2.32.1
*/
handleMemberFetchError(failedModels, errors) {
failedModels.forEach((model, i) => {
let error =
errors?.[i] || errors?.[errors.length - 1] || "Fetch failed";
if (error.message === "abort") {
error = "This file took too long to load. Please try again.";
}
model.set("synced", false);
if (!model.get("errorMessage")) {
model.set("errorMessage", error.message || error);
}
});
},
/**
* Sets the numLoadingFileMetadata attribute on the package model
* @param {number} num - The number of models that are currently being
* fetched
* @since 2.32.1
*/
updateLoadingCount(num) {
this.packageModel.set("numLoadingFileMetadata", num);
},
/**
* After a member model is fetched, determine whether it needs to:
* 1) be merged into the collection (the type did NOT change)
* 2) replace the old model (the type changed to DataPackage)
* 3) be fetched again, waited for, and then replaced in the collection
* (the type from the server is different from the type in the
* collection) Then resolve the promise when the model is fully
* handled.
* @param {DataONEObject} memberModel - The model to potentially replace
* @param {number} maxRetries - The maximum number of retries for each
* fetch request
* @param {number} timeout - The timeout for each fetch request in
* milliseconds. If set to anything other than a positive number greater
* than 0, the fetch will never timeout
* @returns {DataONEObject} The updated model
* @since 2.32.1
*/
async updateMemberModelType(memberModel, maxRetries, timeout) {
const newMemberModel = this.getMember(memberModel);
// 1) If the type did NOT change, just merge the new model
if (memberModel.type === newMemberModel.type) {
newMemberModel.set("synced", true);
this.add(newMemberModel, { merge: true });
if (newMemberModel.type === "EML") {
this.trigger("add:EML");
}
return newMemberModel;
}
// 2) If it changed to "DataPackage", replace the old model directly
if (newMemberModel.type === "DataPackage") {
memberModel.trigger("replace", newMemberModel);
return newMemberModel;
}
// 3) Otherwise (type changed but NOT to DataPackage), we fetch the
// newMemberModel, wait for its sync, then replace in the collection.
newMemberModel.set("synced", false);
try {
await this.fetchWithRetryAndTimeout(
newMemberModel,
maxRetries,
timeout,
);
this.remove(memberModel);
this.add(newMemberModel);
newMemberModel.set("synced", true);
memberModel.trigger("replace", newMemberModel);
if (newMemberModel.type === "EML") {
this.trigger("add:EML");
}
return newMemberModel;
} catch (fetchErr) {
this.handleMemberFetchError([memberModel], [fetchErr]);
return null;
}
},
/**
* Fetch a model using Backbone's fetch method but return a promise that
* resolves when the fetch is complete, along with the XHR object
* @param {DataONEObject} model - The model to fetch
* @returns {object} An object with a promise and an XHR reference
* @since 2.32.1
*/
fetchPromise(model) {
let xhrRef;
const fetchPromise = new Promise((resolve, reject) => {
xhrRef = model.fetch({
success: () => {
this.listenToOnce(model, "sync", () => {
resolve(model);
});
},
error: (m, response) => {
reject(new Error(response?.statusText || "Model fetch failed"));
},
});
});
return { fetchPromise, xhrRef };
},
/**
* Overload fetch calls for a DataPackage
*
* This fetch function will fetch the resource map RDF XML for this
* package
*
* + Example 1: `this.fetch();`
* + Example 2: `this.fetch({fetchModels: false});`
* + Example 3: `this.fetch({fromIndex: true});`
* + Example 4:
* ```
* this.fetch()
* .then(function() {
* console.log("Fetch complete!");
* })
* .catch(function() {
* console.log("Fetch failed!");
* });
* ```
* @param {object} [sourceOptions] - Optional options for this fetch that get
* sent with the XHR request
* @property {boolean} fetchModels - If false, this fetch will not fetch
* each model in the collection. It will only get the resource map
* object.
* @property {boolean} fromIndex - If true, the collection will be
* fetched from Solr rather than fetching the system metadata of each
* model. Useful when you only need to retrieve limited information about
* each package member. Set query-specific parameters on the
* `solrResults` SolrResults set on this collection.
* @returns {Promise} A promise that resolves when the fetch is complete
*/
fetch(sourceOptions) {
const options = sourceOptions || {};
return new Promise((resolve, reject) => {
// Fetch the system metadata for this resource map
this.packageModel.fetch();
if (typeof options === "object") {
// If the fetchModels property is set to false,
if (options.fetchModels === false) {
// Save the property to the Collection itself so it is accessible
// in other functions
this.fetchModels = false;
// Remove the property from the options Object since we don't want
// to send it with the XHR
delete options.fetchModels;
this.once("reset", () => {
this.triggerComplete();
resolve();
});
}
// If the fetchFromIndex property is set to true
else if (options.fromIndex) {
this.fetchFromIndex();
resolve();
return;
}
}
// Set some custom fetch options
const fetchOptions = _.extend({ dataType: "text" }, options);
const thisPackage = this;
// Function to retry fetching with user login details if the initial
// fetch fails
const retryFetch = () => {
// Add the authorization options
const authFetchOptions = _.extend(
fetchOptions,
MetacatUI.appUserModel.createAjaxSettings(),
);
// Fetch the resource map RDF XML with user login details
return Backbone.Collection.prototype.fetch
.call(thisPackage, authFetchOptions)
.fail(() => {
thisPackage.trigger("fetchFailed", thisPackage);
reject();
});
};
// Fetch the resource map RDF XML
Backbone.Collection.prototype.fetch
.call(this, fetchOptions)
.done(() => resolve())
.fail(() => {
// If the initial fetch fails, retry with user login details
retryFetch()
.done(() => resolve())
.fail(() => reject());
});
});
},
/**
* Deserialize a Package from OAI-ORE RDF XML
* @param {string} response - The RDF/XML string to parse
* @param {object} _options - Options for parsing the RDF/XML
* @returns {DataPackage[]} - An array of models that were parsed from the
* RDF/XML
*/
parse(response, _options) {
// Save the raw XML in case it needs to be used later
this.objectXML = response; // TODO: this isn't really objectXML, it's a string of RDF/XML
let responseStr = response;
const ORE = this.rdf.Namespace(this.namespaces.ORE);
const CITO = this.rdf.Namespace(this.namespaces.CITO);
const PROV = this.rdf.Namespace(this.namespaces.PROV);
// The following are not used: const XSD =
// this.rdf.Namespace(this.namespaces.XSD); const RDF =
// this.rdf.Namespace(this.namespaces.RDF); const FOAF =
// this.rdf.Namespace(this.namespaces.FOAF); const OWL =
// this.rdf.Namespace(this.namespaces.OWL); const DC =
// this.rdf.Namespace(this.namespaces.DC); const DCTERMS =
// this.rdf.Namespace(this.namespaces.DCTERMS);
let memberStatements = [];
let atLocationStatements = []; // array to store atLocation statements
let memberURIParts;
let memberPIDStr;
let memberPID;
let memberPIDs = [];
let memberModel;
let documentsStatements;
let objectParts;
let objectPIDStr;
let objectPID;
let objectAtLocationValue;
let scimetaID; // documentor
let scidataID; // documentee
const models = []; // the models returned by parse()
try {
// First, make sure we are only using one CN Base URL in the RDF or
// the RDF parsing will fail.
const cnResolveUrl = MetacatUI.appModel.get("resolveServiceUrl");
const cnURLs = _.uniq(
responseStr.match(
/cn\S+\.test\.dataone\.org\/cn\/v\d\/resolve|cn\.dataone\.org\/cn\/v\d\/resolve/g,
),
);
if (cnURLs.length > 1) {
responseStr = responseStr.replace(
/cn\S+\.test\.dataone\.org\/cn\/v\d\/resolve|cn\.dataone\.org\/cn\/v\d\/resolve/g,
cnResolveUrl.substring(cnResolveUrl.indexOf("https://") + 8),
);
}
this.rdf.parse(
responseStr,
this.dataPackageGraph,
this.url(),
"application/rdf+xml",
);
// List the package members
memberStatements = this.dataPackageGraph.statementsMatching(
undefined,
ORE("aggregates"),
undefined,
undefined,
);
// Get system metadata for each member to eval the formatId
memberStatements.forEach((memberStatement) => {
memberURIParts = memberStatement.object.value.split("/");
memberPIDStr = _.last(memberURIParts);
memberPID = decodeURIComponent(memberPIDStr);
if (memberPID) memberPIDs.push(memberPID);
// TODO: Test passing merge:true when adding a model and this if
// statement may not be necessary Create a DataONEObject model to
// represent this collection member and add to the collection
if (!_.contains(this.pluck("id"), memberPID)) {
memberModel = new DataONEObject({
id: memberPID,
resourceMap: [this.packageModel.get("id")],
collections: [this],
});
models.push(memberModel);
}
// If the model already exists, add this resource map ID to it's
// list of resource maps
else {
memberModel = this.get(memberPID);
models.push(memberModel);
let rMaps = memberModel.get("resourceMap");
if (
rMaps &&
Array.isArray(rMaps) &&
!_.contains(rMaps, this.packageModel.get("id"))
)
rMaps.push(this.packageModel.get("id"));
else if (rMaps && !Array.isArray(rMaps))
rMaps = [rMaps, this.packageModel.get("id")];
else rMaps = [this.packageModel.get("id")];
}
});
// Save the list of original ids
this.originalMembers = memberPIDs;
// Get the isDocumentedBy relationships
documentsStatements = this.dataPackageGraph.statementsMatching(
undefined,
CITO("documents"),
undefined,
undefined,
);
const sciMetaPids = [];
documentsStatements.forEach((documentsStatement) => {
// Extract and URI-decode the metadata pid
scimetaID = decodeURIComponent(
_.last(documentsStatement.subject.value.split("/")),
);
sciMetaPids.push(scimetaID);
// Extract and URI-decode the data pid
scidataID = decodeURIComponent(
_.last(documentsStatement.object.value.split("/")),
);
// Store the isDocumentedBy relationship
if (typeof this.originalIsDocBy[scidataID] === "undefined")
this.originalIsDocBy[scidataID] = [scimetaID];
else if (
Array.isArray(this.originalIsDocBy[scidataID]) &&
!_.contains(this.originalIsDocBy[scidataID], scimetaID)
)
this.originalIsDocBy[scidataID].push(scimetaID);
else
this.originalIsDocBy[scidataID] = _.uniq([
this.originalIsDocBy[scidataID],
scimetaID,
]);
// Find the model in this collection for this data object var
// dataObj = this.get(scidataID);
const dataObj = _.find(models, (m) => m.get("id") === scidataID);
if (dataObj) {
// Get the isDocumentedBy field
let isDocBy = dataObj.get("isDocumentedBy");
if (
isDocBy &&
Array.isArray(isDocBy) &&
!_.contains(isDocBy, scimetaID)
)
isDocBy.push(scimetaID);
else if (isDocBy && !Array.isArray(isDocBy))
isDocBy = [isDocBy, scimetaID];
else isDocBy = [scimetaID];
// Set the isDocumentedBy field
dataObj.set("isDocumentedBy", isDocBy);
}
});
// Save the list of science metadata pids
this.sciMetaPids = sciMetaPids;
// Parse atLocation
const atLocationObject = {};
atLocationStatements = this.dataPackageGraph.statementsMatching(
undefined,
PROV("atLocation"),
undefined,
undefined,
);
const ref = this;
// Get atLocation information for each statement in the resourceMap
_.each(
atLocationStatements,
(atLocationStatement) => {
objectParts = atLocationStatement.subject.value.split("/");
objectPIDStr = _.last(objectParts);
objectPID = decodeURIComponent(objectPIDStr);
objectAtLocationValue = atLocationStatement.object.value;
atLocationObject[objectPID] = ref.getAbsolutePath(
objectAtLocationValue,
);
},
this,
);
this.atLocationObject = atLocationObject;
// Put the science metadata pids first
memberPIDs = _.difference(memberPIDs, sciMetaPids);
_.each(_.uniq(sciMetaPids), (id) => {
memberPIDs.unshift(id);
});
// Don't fetch each member model if the fetchModels property on this
// Collection is set to false
if (this.fetchModels !== false) {
// Start fetching member models
this.fetchMemberModels.call(
this,
models,
MetacatUI.appModel.get("batchSizeFetch"),
MetacatUI.appModel.get("fileDownloadTimeout") || 0,
);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error parsing the RDF/XML", error);
}
// trigger complete if fetchModel is false and this is the only object
// in the package
if (this.fetchModels === false && models.length === 1)
this.triggerComplete();
return models;
},
/**
* Parse the provenance relationships from the RDF graph, after all
* DataPackage members have been fetched, as the prov info will be stored
* in them.
*/
parseProv() {
try {
// Now run the SPARQL queries for the provenance relationships
const provQueries = [];
// result: pidValue, wasDerivedFromValue (prov_wasDerivedFrom)
provQueries.prov_wasDerivedFrom =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"SELECT ?pid ?prov_wasDerivedFrom \n" +
"WHERE { \n" +
"?derived_data prov:wasDerivedFrom ?primary_data . \n" +
"?derived_data dcterms:identifier ?pid . \n" +
"?primary_data dcterms:identifier ?prov_wasDerivedFrom . \n" +
"} \n" +
"]]>";
// result: pidValue, generatedValue (prov_generated)
provQueries.prov_generated =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"SELECT ?pid ?prov_generated \n" +
"WHERE { \n" +
"?result prov:wasGeneratedBy ?activity . \n" +
"?activity prov:qualifiedAssociation ?association . \n" +
"?association prov:hadPlan ?program . \n" +
"?result dcterms:identifier ?prov_generated . \n" +
"?program dcterms:identifier ?pid . \n" +
"} \n" +
"]]>";
// result: pidValue, wasInformedByValue (prov_wasInformedBy)
provQueries.prov_wasInformedBy =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"SELECT ?pid ?prov_wasInformedBy \n" +
"WHERE { \n" +
"?activity prov:wasInformedBy ?previousActivity . \n" +
"?activity dcterms:identifier ?pid . \n" +
"?previousActivity dcterms:identifier ?prov_wasInformedBy . \n" +
"} \n" +
"]]> \n";
// result: pidValue, usedValue (prov_used)
provQueries.prov_used =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"SELECT ?pid ?prov_used \n" +
"WHERE { \n" +
"?activity prov:used ?data . \n" +
"?activity prov:qualifiedAssociation ?association . \n" +
"?association prov:hadPlan ?program . \n" +
"?program dcterms:identifier ?pid . \n" +
"?data dcterms:identifier ?prov_used . \n" +
"} \n" +
"]]> \n";
// result: pidValue, programPidValue (prov_generatesByProgram)
provQueries.prov_generatedByProgram =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"SELECT ?pid ?prov_generatedByProgram \n" +
"WHERE { \n" +
"?derived_data prov:wasGeneratedBy ?execution . \n" +
"?execution prov:qualifiedAssociation ?association . \n" +
"?association prov:hadPlan ?program . \n" +
"?program dcterms:identifier ?prov_generatedByProgram . \n" +
"?derived_data dcterms:identifier ?pid . \n" +
"} \n" +
"]]> \n";
// result: pidValue, executionPidValue
provQueries.prov_generatedByExecution =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"SELECT ?pid ?prov_generatedByExecution \n" +
"WHERE { \n" +
"?derived_data prov:wasGeneratedBy ?execution . \n" +
"?execution dcterms:identifier ?prov_generatedByExecution . \n" +
"?derived_data dcterms:identifier ?pid . \n" +
"} \n" +
"]]> \n";
// result: pidValue, pid (prov_generatedByProgram)
provQueries.prov_generatedByUser =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"SELECT ?pid ?prov_generatedByUser \n" +
"WHERE { \n" +
"?derived_data prov:wasGeneratedBy ?execution . \n" +
"?execution prov:qualifiedAssociation ?association . \n" +
"?association prov:agent ?prov_generatedByUser . \n" +
"?derived_data dcterms:identifier ?pid . \n" +
"} \n" +
"]]> \n";
// results: pidValue, programPidValue (prov_usedByProgram)
provQueries.prov_usedByProgram =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"SELECT ?pid ?prov_usedByProgram \n" +
"WHERE { \n" +
"?execution prov:used ?primary_data . \n" +
"?execution prov:qualifiedAssociation ?association . \n" +
"?association prov:hadPlan ?program . \n" +
"?program dcterms:identifier ?prov_usedByProgram . \n" +
"?primary_data dcterms:identifier ?pid . \n" +
"} \n" +
"]]> \n";
// results: pidValue, executionIdValue (prov_usedByExecution)
provQueries.prov_usedByExecution =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"SELECT ?pid ?prov_usedByExecution \n" +
"WHERE { \n" +
"?execution prov:used ?primary_data . \n" +
"?primary_data dcterms:identifier ?pid . \n" +
"?execution dcterms:identifier ?prov_usedByExecution . \n" +
"} \n" +
"]]> \n";
// results: pidValue, pid (prov_usedByUser)
provQueries.prov_usedByUser =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"SELECT ?pid ?prov_usedByUser \n" +
"WHERE { \n" +
"?execution prov:used ?primary_data . \n" +
"?execution prov:qualifiedAssociation ?association . \n" +
"?association prov:agent ?prov_usedByUser . \n" +
"?primary_data dcterms:identifier ?pid . \n" +
"} \n" +
"]]> \n";
// results: pidValue, executionIdValue (prov_wasExecutedByExecution)
provQueries.prov_wasExecutedByExecution =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"SELECT ?pid ?prov_wasExecutedByExecution \n" +
"WHERE { \n" +
"?execution prov:qualifiedAssociation ?association . \n" +
"?association prov:hadPlan ?program . \n" +
"?execution dcterms:identifier ?prov_wasExecutedByExecution . \n" +
"?program dcterms:identifier ?pid . \n" +
"} \n" +
"]]> \n";
// results: pidValue, pid (prov_wasExecutedByUser)
provQueries.prov_wasExecutedByUser =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"SELECT ?pid ?prov_wasExecutedByUser \n" +
"WHERE { \n" +
"?execution prov:qualifiedAssociation ?association . \n" +
"?association prov:hadPlan ?program . \n" +
"?association prov:agent ?prov_wasExecutedByUser . \n" +
"?program dcterms:identifier ?pid . \n" +
"} \n" +
"]]> \n";
// results: pidValue, derivedDataPidValue (prov_hasDerivations)
provQueries.prov_hasDerivations =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"PREFIX cito: <http://purl.org/spar/cito/> \n" +
"SELECT ?pid ?prov_hasDerivations \n" +
"WHERE { \n" +
"?derived_data prov:wasDerivedFrom ?source_data . \n" +
"?source_data dcterms:identifier ?pid . \n" +
"?derived_data dcterms:identifier ?prov_hasDerivations . \n" +
"} \n" +
"]]> \n";
// results: pidValue, pid (prov_instanceOfClass)
provQueries.prov_instanceOfClass =
"<![CDATA[ \n" +
"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> \n" +
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> \n" +
"PREFIX owl: <http://www.w3.org/2002/07/owl#> \n" +
"PREFIX prov: <http://www.w3.org/ns/prov#> \n" +
"PREFIX provone: <http://purl.dataone.org/provone/2015/01/15/ontology#> \n" +
"PREFIX ore: <http://www.openarchives.org/ore/terms/> \n" +
"PREFIX dcterms: <http://purl.org/dc/terms/> \n" +
"SELECT ?pid ?prov_instanceOfClass \n" +
"WHERE { \n" +
"?subject rdf:type ?prov_instanceOfClass . \n" +
"?subject dcterms:identifier ?pid . \n" +
"} \n" +
"]]> \n";
// These are the provenance fields that are currently searched for in
// the provenance queries, but not all of these fields are displayed
// by any view. Note: this list is different than the prov list
// returned by MetacatUI.appSearchModel.getProvFields()
this.provFields = [
"prov_wasDerivedFrom",
"prov_generated",
"prov_wasInformedBy",
"prov_used",
"prov_generatedByProgram",
"prov_generatedByExecution",
"prov_generatedByUser",
"prov_usedByProgram",
"prov_usedByExecution",
"prov_usedByUser",
"prov_wasExecutedByExecution",
"prov_wasExecutedByUser",
"prov_hasDerivations",
"prov_instanceOfClass",
];
// Process each SPARQL query
const keys = Object.keys(provQueries);
this.queriesToRun = keys.length;
// Bind the onResult and onDone functions to the model so they can be
// called out of context
this.onResult = _.bind(this.onResult, this);
this.onDone = _.bind(this.onDone, this);
// Run queries for all provenance fields. Each query may have multiple
// solutions and each solution will trigger a callback to the
// 'onResult' function. When each query has completed, the 'onDone'
// function is called for that query.
for (let iquery = 0; iquery < keys.length; iquery += 1) {
const eq = rdf.SPARQLToQuery(
provQueries[keys[iquery]],
false,
this.dataPackageGraph,
);
this.dataPackageGraph.query(
eq,
this.onResult,
this.url(),
this.onDone,
);
}
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error parsing the provenance relationships", error);
}
},
/**
* The return values have to be extracted from the result.
* @param {object} result - The result of the SPARQL query
* @param {string} name - The name of the field to extract
* @returns {string} - The value of the result
*/
getValue(result, name) {
const res = result[name];
// The result is of type 'NamedNode', just return the string value
if (res) {
return res.value;
}
return " ";
},
/**
* This callback is called for every query solution of the SPARQL queries.
* One query may result in multple queries solutions and calls to this
* function. Each query result returns two pids, i.e. pid: 1234
* prov_generated: 5678, which corresponds to the RDF triple '5678
* wasGeneratedBy 1234', or the DataONE solr document for pid '1234', with
* the field prov_generated: 5678.
* @param {object} result - The result of the SPARQL query
* @example
* // The result can look like this:
* [?pid: t, ?prov_wasDerivedFrom: t, ?primary_data: t, ?derived_data: t]
* ?derived_data : t {termType: "NamedNode", value: "https://cn-stage.test.dataone.org/cn/v2/resolve/urn%3Auuid%3Adbbb9a2e-af64-452a-b7b9-122861a5dbb2"}
* ?pid : t {termType: "Literal", value: "urn:uuid:dbbb9a2e-af64-452a-b7b9-122861a5dbb2", datatype: t}
* ?primary_data : t {termType: "NamedNode", value: "https://cn-stage.test.dataone.org/cn/v2/resolve/urn%3Auuid%3Aaae9d025-a331-4c3a-b399-a8ca0a2826ef"}
* ?prov_wasDerivedFrom : t {termType: "Literal", value: "urn:uuid:aae9d025-a331-4c3a-b399-a8ca0a2826ef", datatype: t}]
*/
onResult(result) {
const currentPid = this.getValue(result, "?pid");
let resval;
// If there is a solution for this query, assign the value to the prov
// field attribute (e.g. "prov_generated") of the package member (a
// DataONEObject) with id = '?pid'
if (typeof currentPid !== "undefined" && currentPid !== " ") {
let currentMember = null;
let fieldName = null;
let vals = [];
let resultMember = null;
currentMember = this.find((model) => model.get("id") === currentPid);
if (typeof currentMember === "undefined") {
return;
}
// Search for a provenenace field value (i.e. 'prov_wasDerivedFrom')
// that was returned from the query. The current prov queries all
// return one prov field each (see this.provFields). Note:
// dataPackage.provSources and dataPackage.provDerivations are
// accumulators for the entire DataPackage. member.sources and
// member.derivations are accumulators for each package member, and
// are used by functions such as ProvChartView().
for (let iFld = 0; iFld < this.provFields.length; iFld += 1) {
fieldName = this.provFields[iFld];
resval = `?${fieldName}`;
// The pid corresponding to the object of the RDF triple, with the
// predicate of 'prov_generated', 'prov_used', etc. getValue returns
// a string value.
const provFieldResult = this.getValue(result, resval);
if (provFieldResult !== " ") {
// Find the Datapacakge member for the result 'pid' and add the
// result prov_* value to it. This is the package member that is
// the 'subject' of the prov relationship. The 'resultMember'
// could be in the current package, or could be in another
// 'related' package.
resultMember = this.find(
(model) => model.get("id") === provFieldResult,
);
if (typeof resultMember !== "undefined") {
// If this prov field is a 'source' field, add it to 'sources'
if (currentMember.isSourceField(fieldName)) {
const packageMember = this.sources.find(
(source) => source.id === provFieldResult,
);
const matchingMember = currentMember
.get("provSources")
.find((source) => source.id === provFieldResult);
if (!packageMember) {
this.sources.push(resultMember);
}
// Only add the result member if it has not already been
// added.
if (!matchingMember) {
vals = currentMember.get("provSources");
vals.push(resultMember);
currentMember.set("provSources", vals);
}
} else if (currentMember.isDerivationField(fieldName)) {
const derivation = this.derivations.find(
(source) => source.id === provFieldResult,
);
const matchingDerivation = currentMember
.get("provDerivations")
.find((source) => source.id === provFieldResult);
// If this prov field is a 'derivation' field, add it to
// 'derivations'
if (!derivation) {
this.derivations.push(resultMember);
}
if (!matchingDerivation) {
vals = currentMember.get("provDerivations");
vals.push(resultMember);
currentMember.set("provDerivations", vals);
}
}
// Get the existing values for this prov field in the package
// member
vals = currentMember.get(fieldName);
// Push this result onto the prov file list if it is not there,
// i.e.
if (!_.contains(vals, resultMember)) {
vals.push(resultMember);
currentMember.set(fieldName, vals);
}
// provFieldValues = _.uniq(provFieldValues); Add the current
// prov valid (a pid) to the current value in the member
// currentMember.set(fieldName, provFieldValues);
// this.add(currentMember, { merge: true });
} else {
// The query result field is not the identifier of a packge
// member, so it may be the identifier of another 'related'
// package, or it may be a string value that is the object of a
// prov relationship, i.e. for 'prov_instanceOfClass' ==
// 'http://purl.dataone.org/provone/2015/01/15/ontology#Data',
// so add the value to the current member.
vals = currentMember.get(fieldName);
if (!_.contains(vals, provFieldResult)) {
vals.push(provFieldResult);
currentMember.set(fieldName, vals);
}
}
}
}
}
},
/** This callback is called when all queries have finished. */
onDone() {
if (this.queriesToRun > 1) {
this.queriesToRun -= 1;
} else {
// Signal that all prov queries have finished
this.provenanceFlag = "complete";
this.trigger("queryComplete");
}
},
/**
* Use the DataONEObject parseSysMeta() function
* @param {object} sysMeta - The system metadata object to parse
* @returns {object} The parsed system metadata object
*/
parseSysMeta(sysMeta) {
return DataONEObject.parseSysMeta.call(this, sysMeta);
},
/**
* Overwrite the Backbone.Collection.sync() function to set custom options
* @param {object} [options] - Options for this DataPackage save
* @param {boolean} [options.sysMetaOnly] - If true, only the system
* metadata of this Package will be saved.
* @param {boolean} [options.resourceMapOnly] - If true, only the Resource
* Map/Package object will be saved. Metadata and Data objects aggregated
* by the package will be skipped.
*/
save(options = {}) {
this.packageModel.set("uploadStatus", "p");
let mapXML = null;
const collection = this;
let sysMetaToUpdate = [];
// Get the system metadata first if we haven't retrieved it yet
if (!this.packageModel.get("sysMetaXML")) {
this.packageModel.fetch({
success() {
collection.save(options);
},
});
return;
}
// If we want to update the system metadata only, then update via the
// DataONEObject model and exit
if (options.sysMetaOnly) {
this.packageModel.save(null, options);
return;
}
if (options.resourceMapOnly !== true) {
// Sort the models in the collection so the metadata is saved first
const metadataModels = this.where({ type: "Metadata" });
const dataModels = _.difference(this.models, metadataModels);
const sortedModels = _.union(metadataModels, dataModels);
const modelsInProgress = _.filter(
sortedModels,
(m) =>
m.get("uploadStatus") === "p" ||
m.get("sysMetaUploadStatus") === "p",
);
const modelsToBeSaved = _.filter(
sortedModels,
(m) =>
// Models should be saved if they are in the save queue, had an
// error saving earlier, or they are Science Metadata model that
// is NOT already in progress
(m.get("type") === "Metadata" && m.get("uploadStatus") === "q") ||
(m.get("type") === "Data" &&
m.get("hasContentChanges") &&
m.get("uploadStatus") !== "p" &&
m.get("uploadStatus") !== "c" &&
m.get("uploadStatus") !== "e") ||
(m.get("type") === "Metadata" &&
m.get("uploadStatus") !== "p" &&
m.get("uploadStatus") !== "c" &&
m.get("uploadStatus") !== "e" &&
m.get("uploadStatus") !== null),
);
// Get an array of data objects whose system metadata should be
// updated.
sysMetaToUpdate = _.reject(
dataModels,
(m) =>
// Find models that don't have any content changes to save, and
// whose system metadata is not already saving
!m.hasUpdates() ||
m.get("hasContentChanges") ||
m.get("sysMetaUploadStatus") === "p" ||
m.get("sysMetaUploadStatus") === "c" ||
m.get("sysMetaUploadStatus") === "e",
);
// First quickly validate all the models before attempting to save any
const allValid = _.every(modelsToBeSaved, (m) => {
if (m.isValid()) {
m.trigger("valid");
return true;
}
return false;
});
// If at least once model to be saved is invalid, or the metadata
// failed to save, cancel the save.
if (
!allValid ||
_.contains(
_.map(metadataModels, (model) => model.get("uploadStatus")),
"e",
)
) {
this.packageModel.set("changed", false);
this.packageModel.set("uploadStatus", "q");
this.trigger("cancelSave");
return;
}
// If we are saving at least one model in this package, then serialize
// the Resource Map RDF XML
if (modelsToBeSaved.length) {
try {
// Set a new id and keep our old id
if (!this.packageModel.isNew()) {
// Update the identifier for this object
this.packageModel.updateID();
}
// Create the resource map XML
mapXML = this.serialize();
} catch (serializationException) {
// If serialization failed, revert back to our old id
this.packageModel.resetID();
// Cancel the save and show an error message
this.packageModel.set("changed", false);
this.packageModel.set("uploadStatus", "q");
this.trigger(
"errorSaving",
`There was a Javascript error during the serialization process: ${serializationException}`,
);
return;
}
}
// First save all the models of the collection, if needed
modelsToBeSaved.forEach((model) => {
// If the model is saved successfully, start this save function
// again
this.stopListening(model, "successSaving", this.save);
this.listenToOnce(model, "successSaving", this.save);
// If the model fails to save, start this save function
this.stopListening(model, "errorSaving", this.save);
this.listenToOnce(model, "errorSaving", this.save);
// If the model fails to save, start this save function
this.stopListening(model, "cancelSave", this.save);
this.listenToOnce(model, "cancelSave", this.save);
// Save the model and watch for fails
model.save();
// Add it to the list of models in progress
modelsInProgress.push(model);
this.numSaves += 1;
});
// Save the system metadata of all the Data objects
sysMetaToUpdate.forEach((dataModel) => {
// When the sytem metadata has been saved, save this resource map
this.listenTo(dataModel, "change:sysMetaUploadStatus", this.save);
// Update the system metadata
dataModel.updateSysMeta();
// Add it to the list of models in progress
modelsInProgress.push(dataModel);
this.numSaves += 1;
});
// If there are still models in progress of uploading, then exit. (We
// will return when they are synced to upload the resource map)
if (modelsInProgress.length) return;
}
// If we are saving the resource map object only, and there are changes
// to save, serialize the RDF XML
else if (this.needsUpdate()) {
try {
// Set a new id and keep our old id
if (!this.packageModel.isNew()) {
// Update the identifier for this object
this.packageModel.updateID();
}
// Create the resource map XML
mapXML = this.serialize();
} catch (serializationException) {
// If serialization failed, revert back to our old id
this.packageModel.resetID();
// Cancel the save and show an error message
this.packageModel.set("changed", false);
this.packageModel.set("uploadStatus", "q");
this.trigger(
"errorSaving",
`There was a Javascript error during the serialization process: ${serializationException}`,
);
return;
}
}
// If we are saving the resource map object only, and there are no
// changes to save, exit the function
else if (!this.needsUpdate()) {
return;
}
// If no models were saved and this package has no changes, we can exit
// without saving the resource map
if (this.numSaves < 1 && !this.needsUpdate()) {
this.numSaves = 0;
this.packageModel.set(
"uploadStatus",
this.packageModel.defaults().uploadStatus,
);
this.trigger("successSaving", this);
return;
}
// Reset the number of models saved since they should all be completed
// by now
this.numSaves = 0;
// Determine the HTTP request type
let requestType;
if (this.packageModel.isNew()) {
requestType = "POST";
} else {
requestType = "PUT";
}
// Create a FormData object to send data with the XHR
const formData = new FormData();
// Add the identifier to the XHR data
if (this.packageModel.isNew()) {
formData.append("pid", this.packageModel.get("id"));
} else {
// Add the ids to the form data
formData.append("newPid", this.packageModel.get("id"));
formData.append("pid", this.packageModel.get("oldPid"));
}
// Do a fresh re-serialization of the RDF XML, in case any pids in the
// package have changed. The hope is that any errors during the
// serialization process have already been caught during the first
// serialization above
try {
mapXML = this.serialize();
} catch (serializationException) {
// Cancel the save and show an error message
this.packageModel.set("changed", false);
this.packageModel.set("uploadStatus", "q");
this.trigger(
"errorSaving",
`There was a Javascript error during the serialization process: ${serializationException}`,
);
return;
}
// Make a Blob object from the serialized RDF XML
const mapBlob = new Blob([mapXML], { type: "application/xml" });
// Get the size of the new resource map
this.packageModel.set("size", mapBlob.size);
// Get the new checksum of the resource map
const checksum = md5(mapXML);
this.packageModel.set("checksum", checksum);
this.packageModel.set("checksumAlgorithm", "MD5");
// Set the file name based on the id
this.packageModel.set(
"fileName",
`${this.packageModel
.get("id")
.replace(/[^a-zA-Z0-9]/g, "_")}.rdf.xml`,
);
// Create the system metadata
const sysMetaXML = this.packageModel.serializeSysMeta();
// Send the system metadata
const xmlBlob = new Blob([sysMetaXML], {
type: "application/xml",
});
// Add the object XML and System Metadata XML to the form data Append
// the system metadata first, so we can take advantage of Metacat's
// streaming multipart handler
formData.append("sysmeta", xmlBlob, "sysmeta");
formData.append("object", mapBlob);
const requestSettings = {
url: this.packageModel.isNew()
? this.url()
: this.url({ update: true }),
type: requestType,
cache: false,
contentType: false,
processData: false,
data: formData,
success(_response) {
// Update the object XML
collection.objectXML = mapXML;
collection.packageModel.set(
"sysMetaXML",
collection.packageModel.serializeSysMeta(),
);
// Reset the upload status for all members
_.each(collection.where({ uploadStatus: "c" }), (m) => {
m.set("uploadStatus", m.defaults().uploadStatus);
});
// Reset oldPid to null so we know we need to update the ID in the
// future
collection.packageModel.set("oldPid", null);
// Reset the upload status for the package
collection.packageModel.set(
"uploadStatus",
collection.packageModel.defaults().uploadStatus,
);
// Reset the content changes status
collection.packageModel.set("hasContentChanges", false);
// This package is no longer new, so mark it as such
collection.packageModel.set("isNew", false);
collection.trigger("successSaving", collection);
collection.packageModel.fetch({ merge: true });
_.each(sysMetaToUpdate, (dataModel) => {
dataModel.set("sysMetaUploadStatus", "c");
});
},
error(data) {
// Reset the id back to its original state
collection.packageModel.resetID();
// Reset the upload status for all members
_.each(collection.where({ uploadStatus: "c" }), (m) => {
m.set("uploadStatus", m.defaults().uploadStatus);
});
// When there is no network connection (status === 0), there will be
// no response text
let parsedResponse =
"There was a network issue that prevented this file from uploading. " +
"Make sure you are connected to a reliable internet connection.";
if (data.status !== 408 && data.status !== 0) {
parsedResponse = $(data.responseText).not("style, title").text();
}
// Save the error message in the model
collection.packageModel.set("errorMessage", parsedResponse);
// Reset the upload status for the package
collection.packageModel.set("uploadStatus", "e");
collection.trigger("errorSaving", parsedResponse);
// Track this error in our analytics
MetacatUI.analytics?.trackException(
`DataPackage save error: ${parsedResponse}`,
collection.packageModel.get("id"),
true,
);
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},
/**
* When a data package member updates, we evaluate it for its formatid,
* and update it appropriately if it is not a data object only
* @param {Backbone.Model} context - The model that was updated
* @returns {Backbone.Model} The updated model
*/
getMember(context) {
let memberModel = {};
switch (context.get("formatId")) {
case "http://www.openarchives.org/ore/terms":
context.attributes.id = context.id;
context.attributes.type = "DataPackage";
context.attributes.childPackages = {};
memberModel = new DataPackage(null, {
packageModel: context.attributes,
});
this.packageModel.get("childPackages")[
memberModel.packageModel.id
] = memberModel;
break;
case "eml://ecoinformatics.org/eml-2.0.0":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new EML211(context.attributes);
break;
case "eml://ecoinformatics.org/eml-2.0.1":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new EML211(context.attributes);
break;
case "eml://ecoinformatics.org/eml-2.1.0":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new EML211(context.attributes);
break;
case "eml://ecoinformatics.org/eml-2.1.1":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new EML211(context.attributes);
break;
case "https://eml.ecoinformatics.org/eml-2.2.0":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new EML211(context.attributes);
break;
case "-//ecoinformatics.org//eml-access-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-access-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-attribute-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-attribute-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-constraint-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-constraint-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-coverage-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-coverage-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-dataset-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-dataset-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-distribution-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-distribution-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-entity-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-entity-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-literature-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-literature-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-party-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-party-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-physical-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-physical-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-project-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-project-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-protocol-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-protocol-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-resource-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-resource-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-software-2.0.0beta4//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "-//ecoinformatics.org//eml-software-2.0.0beta6//EN":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "FGDC-STD-001-1998":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "FGDC-STD-001.1-1999":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "FGDC-STD-001.2-1999":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "INCITS-453-2009":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "ddi:codebook:2_5":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://datacite.org/schema/kernel-3.0":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://datacite.org/schema/kernel-3.1":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://datadryad.org/profile/v3.1":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://ns.dataone.org/metadata/schema/onedcx/v1.0":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://purl.org/dryad/terms/":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://purl.org/ornl/schema/mercury/terms/v1.0":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://rs.tdwg.org/dwc/xsd/simpledarwincore/":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://www.cuahsi.org/waterML/1.0/":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://www.cuahsi.org/waterML/1.1/":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://www.esri.com/metadata/esriprof80.dtd":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://www.icpsr.umich.edu/DDI":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://www.isotc211.org/2005/gmd":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://www.isotc211.org/2005/gmd-noaa":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://www.loc.gov/METS/":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
case "http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2":
context.set({ type: "Metadata", sortOrder: 1 });
memberModel = new ScienceMetadata(context.attributes);
break;
default:
// For other data formats, keep just the DataONEObject sysmeta
context.set({ type: "Data", sortOrder: 2 });
memberModel = context;
}
if (memberModel.type === "DataPackage") {
// We have a nested collection
memberModel.packageModel.set(
"nodeLevel",
this.packageModel.get("nodeLevel") + 1,
);
} else {
// We have a model
memberModel.set("nodeLevel", this.packageModel.get("nodeLevel")); // same level for all members
}
return memberModel;
},
/**
* Trigger the complete event if all models have been fetched
* @param {Backbone.Model} model - The model that was fetched
*/
triggerComplete(model) {
// If the last fetch did not fetch the models of the collection, then
// mark as complete now.
if (this.fetchModels === false) {
// Delete the fetchModels property since it is set only once per
// fetch.
delete this.fetchModels;
this.trigger("complete", this);
return;
}
// Check if the collection is done being retrieved
const notSynced = this.reject(
(m) => m.get("synced") || m.get("id") === model?.get("id"),
);
// If there are any models that are not synced yet, the collection is
// not complete
if (notSynced.length > 0) {
return;
}
// If the number of models in this collection does not equal the number
// of objects referenced in the RDF XML, the collection is not complete
if (this.originalMembers.length > this.length) return;
this.sort();
this.trigger("complete", this);
},
/**
* Accumulate edits that are made to the provenance relationships via the
* ProvChartView. these edits are accumulated here so that they are
* available to any package member or view.
* @param {string} operation - The operation performed on the relationship
* (add or delete)
* @param {string} subject - The subject of the relationship
* @param {string} predicate - The predicate of the relationship
* @param {string} object - The object of the relationship
*/
recordProvEdit(operation, subject, predicate, object) {
if (!this.provEdits.length) {
this.provEdits = [[operation, subject, predicate, object]];
} else {
// First check if the edit already exists in the list. If yes, then
// don't add it again! This could occur if an edit icon was clicked
// rapidly before it is dismissed.
const editFound = _.find(
this.provEdits,
(edit) =>
edit[0] === operation &&
edit[1] === subject &&
edit[2] === predicate &&
edit[3] === object,
);
if (typeof editFound !== "undefined") {
return;
}
// If this is a delete operation, then check if a matching operation
// is in the edit list (i.e. the user may have changed their mind, and
// they just want to cancel an edit). If yes, then just delete the
// matching add edit request
const editListSize = this.provEdits.length;
const oppositeOp = operation === "delete" ? "add" : "delete";
this.provEdits = _.reject(this.provEdits, (edit) => {
const editOperation = edit[0];
const editSubjectId = edit[1];
const editPredicate = edit[2];
const editObject = edit[3];
if (
editOperation === oppositeOp &&
editSubjectId === subject &&
editPredicate === predicate &&
editObject === object
) {
return true;
}
return false;
});
// If we cancelled out edit containing inverse of the current edit
// then the edit list will now be one edit shorter. Test for this and
// only save the current edit if we didn't remove the inverse.
if (editListSize >= this.provEdits.length) {
this.provEdits.push([operation, subject, predicate, object]);
}
}
},
/**
* Check if there are any provenance edits pending
* @returns {boolean} Returns true if the prov edits list is not empty,
* otherwise false.
*/
provEditsPending() {
if (this.provEdits.length) return true;
return false;
},
/**
* If provenance relationships have been modified by the provenance editor
* (in ProvChartView), then update the ORE Resource Map and save it to the
* server.
*/
saveProv() {
const graph = this.dataPackageGraph;
const rdfRef = this.rdf;
const { provEdits } = this;
if (!provEdits.length) {
return;
}
const RDF = rdfRef.Namespace(this.namespaces.RDF);
const PROV = rdfRef.Namespace(this.namespaces.PROV);
const PROVONE = rdfRef.Namespace(this.namespaces.PROVONE);
// The following are not used: const DCTERMS =
// rdfRef.Namespace(this.namespaces.DCTERMS); const CITO =
// rdfRef.Namespace(this.namespaces.CITO); const XSD =
// rdfRef.Namespace(this.namespaces.XSD);
// Check if this package member had provenance relationships added or
// deleted by the provenance editor functionality of the ProvChartView
provEdits.forEach((edit) => {
const [operation, subject, predicate, object] = edit;
// The predicates of the provenance edits recorded by the
// ProvChartView indicate which W3C PROV relationship has been
// recorded. First check if this relationship alread exists in the RDF
// graph. See DataPackage.parseProv for a description of how
// relationships from an ORE resource map are parsed and stored in
// DataONEObjects. Here we are reversing the process, so may need The
// representation of the PROVONE data model is simplified in the
// ProvChartView, to aid legibility for users not familiar with the
// details of the PROVONE model. In this simplification, a
// provone:Program has direct inputs and outputs. In the actual model,
// a prov:Execution has inputs and outputs and is connected to a
// program via a prov:association. We must 'expand' the simplified
// provenance updates recorded by the editor into the fully detailed
// representation of the actual model.
let executionId;
let executionNode;
let programId;
let dataNode;
let derivedDataNode;
// var graph = this.dataPackageGraph;
// Create a node for the subject and object
const subjectNode = rdfRef.sym(this.getURIFromRDF(subject));
const objectNode = rdfRef.sym(this.getURIFromRDF(object));
switch (predicate) {
case "prov_wasDerivedFrom":
derivedDataNode = subjectNode;
dataNode = objectNode;
if (operation === "add") {
this.addToGraph(dataNode, RDF("type"), PROVONE("Data"));
this.addToGraph(derivedDataNode, RDF("type"), PROVONE("Data"));
this.addToGraph(
derivedDataNode,
PROV("wasDerivedFrom"),
dataNode,
);
} else {
graph.removeMatches(
derivedDataNode,
PROV("wasDerivedFrom"),
dataNode,
);
this.removeIfLastProvRef(
dataNode,
RDF("type"),
PROVONE("Data"),
);
this.removeIfLastProvRef(
derivedDataNode,
RDF("type"),
PROVONE("Data"),
);
}
break;
case "prov_generatedByProgram":
programId = object;
dataNode = subjectNode;
if (operation === "add") {
// 'subject' is the program id, which is a simplification of the
// PROVONE model for display. In the PROVONE model, execution
// 'uses' and input, and is associated with a program.
executionId = this.addProgramToGraph(programId);
// executionNode = rdfRef.sym(cnResolveUrl +
// encodeURIComponent(executionId));
executionNode = this.getExecutionNode(executionId);
this.addToGraph(dataNode, RDF("type"), PROVONE("Data"));
this.addToGraph(
dataNode,
PROV("wasGeneratedBy"),
executionNode,
);
} else {
executionId = this.getExecutionId(programId);
executionNode = this.getExecutionNode(executionId);
graph.removeMatches(
dataNode,
PROV("wasGeneratedBy"),
executionNode,
);
this.removeProgramFromGraph(programId);
this.removeIfLastProvRef(
dataNode,
RDF("type"),
PROVONE("Data"),
);
}
break;
case "prov_usedByProgram":
programId = object;
dataNode = subjectNode;
if (operation === "add") {
// 'subject' is the program id, which is a simplification of the
// PROVONE model for display. In the PROVONE model, execution
// 'uses' and input, and is associated with a program.
executionId = this.addProgramToGraph(programId);
// executionNode = rdfRef.sym(cnResolveUrl +
// encodeURIComponent(executionId));
executionNode = this.getExecutionNode(executionId);
this.addToGraph(dataNode, RDF("type"), PROVONE("Data"));
this.addToGraph(executionNode, PROV("used"), dataNode);
} else {
executionId = this.getExecutionId(programId);
executionNode = this.getExecutionNode(executionId);
graph.removeMatches(executionNode, PROV("used"), dataNode);
this.removeProgramFromGraph(programId);
this.removeIfLastProvRef(
dataNode,
RDF("type"),
PROVONE("Data"),
);
}
break;
case "prov_hasDerivations":
dataNode = subjectNode;
derivedDataNode = objectNode;
if (operation === "add") {
this.addToGraph(dataNode, RDF("type"), PROVONE("Data"));
this.addToGraph(derivedDataNode, RDF("type"), PROVONE("Data"));
this.addToGraph(
derivedDataNode,
PROV("wasDerivedFrom"),
dataNode,
);
} else {
graph.removeMatches(
derivedDataNode,
PROV("wasDerivedFrom"),
dataNode,
);
this.removeIfLastProvRef(
dataNode,
RDF("type"),
PROVONE("Data"),
);
this.removeIfLastProvRef(
derivedDataNode,
RDF("type"),
PROVONE("Data"),
);
}
break;
case "prov_instanceOfClass": {
const classNode = PROVONE(object);
if (operation === "add") {
this.addToGraph(subjectNode, RDF("type"), classNode);
} else {
// Make sure there are no other references to this
this.removeIfLastProvRef(subjectNode, RDF("type"), classNode);
}
break;
}
default:
// Print error if predicate for prov edit not found.
}
});
// When saving provenance only, we only have to save the Resource
// Map/Package object. So we will send the resourceMapOnly flag with
// the save function.
this.save({
resourceMapOnly: true,
});
},
/**
* Add the specified relationship to the RDF graph only if it has not
* already been added.
* @param {object} subject - The subject of the statement to add
* @param {object} predicate - The predicate of the statement to add
* @param {object} object - The object of the statement to add
*/
addToGraph(subject, predicate, object) {
const graph = this.dataPackageGraph;
const statements = graph.statementsMatching(subject, predicate, object);
if (!statements.length) {
graph.add(subject, predicate, object);
}
},
/**
* Remove the statement fromn the RDF graph only if the subject of this
* relationship is not referenced by any other provenance relationship,
* i.e. for example, the prov relationship "id rdf:type provone:data" is
* only needed if the subject ('id') is referenced in another
* relationship. Also don't remove it if the subject is in any other prov
* statement, meaning it still references another prov object.
* @param {object} subjectNode - The subject of the statement to remove
* @param {object} predicateNode - The predicate of the statement to
* remove
* @param {object} objectNode - The object of the statement to remove
*/
removeIfLastProvRef(subjectNode, predicateNode, objectNode) {
const graph = this.dataPackageGraph;
const PROV = rdf.Namespace(this.namespaces.PROV);
const PROVONE = rdf.Namespace(this.namespaces.PROVONE);
// PROV namespace value, used to identify PROV statements
const provStr = PROV("").value;
// PROVONE namespace value, used to identify PROVONE statements
const provoneStr = PROVONE("").value;
// Get the statements from the RDF graph that reference the subject of
// the statement to remove.
let statements = graph.statementsMatching(
undefined,
undefined,
subjectNode,
);
let found = statements.find((statement) => {
if (
statement.subject === subjectNode &&
statement.predicate === predicateNode &&
statement.object === objectNode
)
return false;
const pVal = statement.predicate.value;
// Now check if the subject is referenced in a prov statement There is
// another statement that references the subject of the statement to
// remove, so it is still being used and don't remove it.
if (pVal.indexOf(provStr) !== -1) return true;
if (pVal.indexOf(provoneStr) !== -1) return true;
return false;
}, this);
// IF not found in the first test, keep looking.
if (typeof found === "undefined") {
// Get the statements from the RDF where
statements = graph.statementsMatching(
subjectNode,
undefined,
undefined,
);
found = _.find(
statements,
(statement) => {
if (
statement.subject === subjectNode &&
statement.predicate === predicateNode &&
statement.object === objectNode
)
return false;
const pVal = statement.predicate.value;
// Now check if the subject is referenced in a prov statement
if (pVal.indexOf(provStr) !== -1) return true;
if (pVal.indexOf(provoneStr) !== -1) return true;
// There is another statement that references the subject of the
// statement to remove, so it is still being used and don't remove
// it.
return false;
},
this,
);
}
// The specified statement term isn't being used for prov, so remove it.
if (typeof found === "undefined") {
graph.removeMatches(
subjectNode,
predicateNode,
objectNode,
undefined,
);
}
},
/**
* Remove orphaned blank nodes from the model's current graph
*
* This was put in to support replacing package members who are referenced
* by provenance statements, specifically members typed as Programs.
* rdflib.js will throw an error when serializing if any statements in the
* graph have objects that are blank nodes when no other statements in the
* graph have subjects for the same blank node. i.e., blank nodes
* references that aren't defined.
*
* Should be called during a call to serialize() and mutates
* this.dataPackageGraph directly as a side-effect.
*/
removeOrphanedBlankNodes() {
if (!this.dataPackageGraph || !this.dataPackageGraph.statements) {
return;
}
// Collect an array of statements to be removed
const toRemove = [];
this.dataPackageGraph.statements.forEach((statement) => {
if (statement.object.termType !== "BlankNode") {
return;
}
// For this statement, look for other statments about it
let matches = 0;
_.each(this.dataPackageGraph.statements, (other) => {
if (
other.subject.termType === "BlankNode" &&
other.subject.id === statement.object.id
) {
matches += 1;
}
});
// If none are found, add it to our list
if (matches === 0) {
toRemove.push(statement);
}
}, this);
// Remove collected statements
toRemove.forEach((statement) => {
this.dataPackageGraph.removeStatement(statement);
});
},
/**
* Get the execution identifier that is associated with a program id. This
* will either be in the 'prov_wasExecutedByExecution' of the package
* member for the program script, or available by tracing backward in the
* RDF graph from the program node, through the assocation to the related
* execution.
* @param {string} programId - The program identifier
* @returns {string} The execution identifier
*/
getExecutionId(programId) {
const rdfRef = this.rdf;
const graph = this.dataPackageGraph;
let stmts = null;
this.getCnURI();
rdfRef.Namespace(this.namespaces.RDF);
const PROV = rdfRef.Namespace(this.namespaces.PROV);
// Not used: const DCTERMS = rdfRef.Namespace(this.namespaces.DCTERMS);
// const PROVONE = rdfRef.Namespace(this.namespaces.PROVONE);
const member = this.get(programId);
const executionId = member.get("prov_wasExecutedByExecution");
if (executionId.length > 0) {
return executionId[0];
}
const programNode = rdfRef.sym(this.getURIFromRDF(programId));
// Get the executionId from the RDF graph There can be only one plan for
// an association
stmts = graph.statementsMatching(
undefined,
PROV("hadPlan"),
programNode,
);
if (typeof stmts === "undefined") return null;
const associationNode = stmts[0].subject;
// There should be only one execution for this assocation.
stmts = graph.statementsMatching(
undefined,
PROV("qualifiedAssociation"),
associationNode,
);
if (typeof stmts === "undefined") return null;
return stmts[0].subject;
},
/**
* Get the RDF node for an execution that is associated with the execution
* identifier. The execution may have been created in the resource map as
* a 'bare' urn:uuid (no resolveURI), or as a resolve URL, so check for
* both until the id is found.
* @param {string} executionId - The execution identifier
* @returns {object} The RDF node for the execution
*/
getExecutionNode(executionId) {
const rdfRef = this.rdf;
const graph = this.dataPackageGraph;
let stmts = null;
let testNode = null;
this.getCnURI();
let executionNode = null;
// First see if the execution exists in the RDF graph as a 'bare'
// idenfier, i.e. a 'urn:uuid'.
stmts = graph.statementsMatching(
rdfRef.sym(executionId),
undefined,
undefined,
);
if (typeof stmts === "undefined" || !stmts.length) {
// The execution node as urn was not found, look for fully qualified
// version.
testNode = rdfRef.sym(this.getURIFromRDF(executionId));
stmts = graph.statementsMatching(
rdfRef.sym(executionId),
undefined,
undefined,
);
if (typeof stmts === "undefined") {
// Couldn't find the execution, return the standard RDF node value
executionNode = rdfRef.sym(this.getURIFromRDF(executionId));
return executionNode;
}
return testNode;
}
// The executionNode was found in the RDF graph as a urn
executionNode = stmts[0].subject;
return executionNode;
},
/**
* Add a program identifier to the RDF graph and create an execution node
* @param {string} programId - The program identifier
* @returns {string} The execution identifier
*/
addProgramToGraph(programId) {
const rdfRef = this.rdf;
const graph = this.dataPackageGraph;
const RDF = rdfRef.Namespace(this.namespaces.RDF);
const DCTERMS = rdfRef.Namespace(this.namespaces.DCTERMS);
const PROV = rdfRef.Namespace(this.namespaces.PROV);
const PROVONE = rdfRef.Namespace(this.namespaces.PROVONE);
const XSD = rdfRef.Namespace(this.namespaces.XSD);
const member = this.get(programId);
let executionId = member.get("prov_wasExecutedByExecution");
let executionNode = null;
let programNode = null;
let associationNode = null;
this.getCnURI();
if (!executionId.length) {
// This is a new execution, so create new execution and association
// ids
executionId = `urn:uuid:${uuid.v4()}`;
member.set("prov_wasExecutedByExecution", [executionId]);
// Blank node id. RDF validator doesn't like ':' so don't use in the
// id executionNode = rdfRef.sym(cnResolveUrl +
// encodeURIComponent(executionId));
executionNode = this.getExecutionNode(executionId);
// associationId = "_" + uuid.v4();
associationNode = graph.bnode();
} else {
[executionId] = executionId;
// Check if an association exists in the RDF graph for this execution
// id executionNode = rdfRef.sym(cnResolveUrl +
// encodeURIComponent(executionId));
executionNode = this.getExecutionNode(executionId);
// Check if there is an association id for this execution. If this
// execution is newly created (via the editor (existing would be
// parsed from the resmap), then create a new association id.
const stmts = graph.statementsMatching(
executionNode,
PROV("qualifiedAssociation"),
undefined,
);
// IF an associati on was found, then use it, else geneate a new one
// (Associations aren't stored in the )
if (stmts.length) {
associationNode = stmts[0].object;
// associationId = stmts[0].object.value;
} else {
// associationId = "_" + uuid.v4();
associationNode = graph.bnode();
}
}
// associationNode = graph.bnode(associationId); associationNode =
// graph.bnode();
programNode = rdfRef.sym(this.getURIFromRDF(programId));
try {
this.addToGraph(
executionNode,
PROV("qualifiedAssociation"),
associationNode,
);
this.addToGraph(executionNode, RDF("type"), PROVONE("Execution"));
this.addToGraph(
executionNode,
DCTERMS("identifier"),
rdfRef.literal(executionId, undefined, XSD("string")),
);
this.addToGraph(associationNode, PROV("hadPlan"), programNode);
this.addToGraph(programNode, RDF("type"), PROVONE("Program"));
} catch (error) {
// TODO: Handle the error
}
return executionId;
},
/**
* Remove a program identifier from the RDF graph and remove associated
* linkage between the program id and the exection, if the execution is
* not being used by any other statements.
* @param {string} programId - The program identifier
* @returns {boolean} Returns true if the program was removed, otherwise
* false.
*/
removeProgramFromGraph(programId) {
const graph = this.dataPackageGraph;
const rdfRef = this.rdf;
let stmts = null;
this.getCnURI();
const RDF = rdfRef.Namespace(this.namespaces.RDF);
const DCTERMS = rdfRef.Namespace(this.namespaces.DCTERMS);
const PROV = rdfRef.Namespace(this.namespaces.PROV);
const PROVONE = rdfRef.Namespace(this.namespaces.PROVONE);
const XSD = rdfRef.Namespace(this.namespaces.XSD);
let associationNode = null;
const executionId = this.getExecutionId(programId);
if (executionId !== null && executionId !== undefined) return false;
// var executionNode = rdfRef.sym(cnResolveUrl +
// encodeURIComponent(executionId));
const executionNode = this.getExecutionNode(executionId);
const programNode = rdfRef.sym(this.getURIFromRDF(programId));
// In order to remove this program from the graph, we have to first
// determine that nothing else is using the execution that is associated
// with the program (the plan). There may be additional 'used',
// 'geneated', 'qualifiedGeneration', etc. items that may be pointing to
// the execution. If yes, then don't delete the execution or the program
// (the execution's plan).
try {
// Is the program in the graph? If the program is not in the graph,
// then we don't know how to remove the proper execution and
// assocation.
stmts = graph.statementsMatching(undefined, undefined, programNode);
if (typeof stmts === "undefined" || !stmts.length) return false;
// Is anything else linked to this execution?
stmts = graph.statementsMatching(executionNode, PROV("used"));
if (!typeof stmts === "undefined" || stmts.length) return false;
stmts = graph.statementsMatching(
undefined,
PROV("wasGeneratedBy"),
executionNode,
);
if (!typeof stmts === "undefined" || stmts.length) return false;
stmts = graph.statementsMatching(
executionNode,
PROV("qualifiedGeneration"),
undefined,
);
if (!typeof stmts === "undefined" || stmts.length) return false;
stmts = graph.statementsMatching(
undefined,
PROV("wasInformedBy"),
executionNode,
);
if (!typeof stmts === "undefined" || stmts.length) return false;
stmts = graph.statementsMatching(
undefined,
PROV("wasPartOf"),
executionNode,
);
if (!typeof stmts === "undefined" || stmts.length) return false;
// get association
stmts = graph.statementsMatching(
undefined,
PROV("hadPlan"),
programNode,
);
associationNode = stmts[0].subject;
} catch (error) {
// TODO: Handle the error
}
// The execution isn't needed any longer, so remove it and the program.
try {
graph.removeMatches(programNode, RDF("type"), PROVONE("Program"));
graph.removeMatches(associationNode, PROV("hadPlan"), programNode);
graph.removeMatches(
associationNode,
RDF("type"),
PROV("Association"),
);
graph.removeMatches(associationNode, PROV("Agent"), undefined);
graph.removeMatches(executionNode, RDF("type"), PROVONE("Execution"));
graph.removeMatches(
executionNode,
DCTERMS("identifier"),
rdfRef.literal(executionId, undefined, XSD("string")),
);
graph.removeMatches(
executionNode,
PROV("qualifiedAssociation"),
associationNode,
);
} catch (error) {
// TODO: Handle the error
}
return true;
},
/**
* Serialize the DataPackage to OAI-ORE RDF XML
* @returns {string} The serialized RDF/XML
*/
serialize() {
// Create an RDF serializer
const serializer = this.rdf.Serializer();
let oldPidVariations;
let modifiedDate;
let subjectClone;
let predicateClone;
let objectClone;
serializer.store = this.dataPackageGraph;
// Define the namespaces
const ORE = this.rdf.Namespace(this.namespaces.ORE);
// const CITO = this.rdf.Namespace(this.namespaces.CITO);
const DC = this.rdf.Namespace(this.namespaces.DC);
const DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS);
const FOAF = this.rdf.Namespace(this.namespaces.FOAF);
const RDF = this.rdf.Namespace(this.namespaces.RDF);
const XSD = this.rdf.Namespace(this.namespaces.XSD);
// Get the pid of this package - depends on whether we are updating or
// creating a resource map
const pid = this.packageModel.get("id");
const oldPid = this.packageModel.get("oldPid");
let cnResolveUrl = this.getCnURI();
// Get a list of the model pids that should be aggregated by this
// package
let idsFromModel = [];
this.each((packageMember) => {
// If this object isn't done uploading, don't aggregate it. Or if it
// failed to upload, don't aggregate it. But if the system metadata
// failed to update, it can still be aggregated.
if (
packageMember.get("uploadStatus") !== "p" ||
packageMember.get("uploadStatus") !== "e" ||
packageMember.get("sysMetaUploadStatus") === "e"
) {
idsFromModel.push(packageMember.get("id"));
}
});
this.idsToAggregate = idsFromModel;
// Update the pids in the RDF graph only if we are updating the resource
// map with a new pid
if (!this.packageModel.isNew()) {
// Remove all describes/isDescribedBy statements (they'll be rebuilt)
this.dataPackageGraph.removeMany(
undefined,
ORE("describes"),
undefined,
undefined,
undefined,
);
this.dataPackageGraph.removeMany(
undefined,
ORE("isDescribedBy"),
undefined,
undefined,
undefined,
);
// Create variations of the resource map ID using the resolve URL so
// we can always find it in the RDF graph
oldPidVariations = [
oldPid,
encodeURIComponent(oldPid),
cnResolveUrl + oldPid,
cnResolveUrl + encodeURIComponent(oldPid),
this.getURIFromRDF(oldPid),
];
// Using the isAggregatedBy statements, find all the DataONE object
// ids in the RDF graph
const idsFromXML = [];
const identifierStatements = this.dataPackageGraph.statementsMatching(
undefined,
DCTERMS("identifier"),
undefined,
);
_.each(
identifierStatements,
(statement) => {
idsFromXML.push(
statement.object.value,
encodeURIComponent(statement.object.value),
cnResolveUrl + encodeURIComponent(statement.object.value),
cnResolveUrl + statement.object.value,
);
},
this,
);
// Get all the child package ids
const childPackages = this.packageModel.get("childPackages");
if (typeof childPackages === "object") {
idsFromModel = _.union(idsFromModel, Object.keys(childPackages));
}
// Find the difference between the model IDs and the XML IDs to get a
// list of added members
const addedIds = _.without(
_.difference(idsFromModel, idsFromXML),
oldPidVariations,
);
// Start an array to track all the member id variations
const allMemberIds = idsFromModel;
// Add the ids with the CN Resolve URLs
_.each(idsFromModel, (id) => {
allMemberIds.push(
cnResolveUrl + encodeURIComponent(id),
cnResolveUrl + id,
encodeURIComponent(id),
);
});
// Find the identifier statement in the resource map
const idNode = this.rdf.lit(oldPid);
const idStatements = this.dataPackageGraph.statementsMatching(
undefined,
undefined,
idNode,
);
// Change all the resource map identifier literal node in the RDF
// graph
if (idStatements.length) {
const idStatement = idStatements[0];
// Remove the identifier statement
try {
this.dataPackageGraph.remove(idStatement);
} catch (error) {
// TODO: Handle the error
}
// Replace the id in the subject URI with the new id
let newRMapURI = "";
if (idStatement.subject.value.indexOf(oldPid) > -1) {
newRMapURI = idStatement.subject.value.replace(oldPid, pid);
} else if (
idStatement.subject.value.indexOf(encodeURIComponent(oldPid)) > -1
) {
newRMapURI = idStatement.subject.value.replace(
encodeURIComponent(oldPid),
encodeURIComponent(pid),
);
}
// Create resource map nodes for the subject and object
const rMapNode = this.rdf.sym(newRMapURI);
const rMapIdNode = this.rdf.lit(pid);
// Add the triple for the resource map id
this.dataPackageGraph.add(
rMapNode,
DCTERMS("identifier"),
rMapIdNode,
);
}
// Get all the isAggregatedBy statements
const aggByStatements = $.extend(
true,
[],
this.dataPackageGraph.statementsMatching(
undefined,
ORE("isAggregatedBy"),
),
);
// Remove any other isAggregatedBy statements that are not listed as
// members of this model
aggByStatements.forEach((statement) => {
if (!_.contains(allMemberIds, statement.subject.value)) {
this.removeFromAggregation(statement.subject.value);
}
});
// Change all the statements in the RDF where the aggregation is the
// subject, to reflect the new resource map ID
let aggregationNode;
oldPidVariations.forEach((oldPidVar) => {
// Create a node for the old aggregation using this pid variation
aggregationNode = this.rdf.sym(`${oldPidVar}#aggregation`);
const aggregationLitNode = this.rdf.lit(
`${oldPidVar}#aggregation`,
"",
XSD("anyURI"),
);
// Get all the triples where the old aggregation is the subject
const aggregationSubjStatements = _.union(
this.dataPackageGraph.statementsMatching(aggregationNode),
this.dataPackageGraph.statementsMatching(aggregationLitNode),
);
if (aggregationSubjStatements.length) {
aggregationSubjStatements.forEach((statement) => {
// Clone the subject
subjectClone = this.cloneNode(statement.subject);
// Clone the predicate
predicateClone = this.cloneNode(statement.predicate);
// Clone the object
objectClone = this.cloneNode(statement.object);
// Set the subject value to the new aggregation id
subjectClone.value = `${this.getURIFromRDF(pid)}#aggregation`;
// Add a new statement with the new aggregation subject but the
// same predicate and object
this.dataPackageGraph.add(
subjectClone,
predicateClone,
objectClone,
);
});
// Remove the old aggregation statements from the graph
this.dataPackageGraph.removeMany(aggregationNode);
}
// Change all the statements in the RDF where the aggregation is the
// object, to reflect the new resource map ID
const aggregationObjStatements = _.union(
this.dataPackageGraph.statementsMatching(
undefined,
undefined,
aggregationNode,
),
this.dataPackageGraph.statementsMatching(
undefined,
undefined,
aggregationLitNode,
),
);
if (aggregationObjStatements.length) {
aggregationObjStatements.forEach((statement) => {
// Clone the subject, object, and predicate
subjectClone = this.cloneNode(statement.subject);
predicateClone = this.cloneNode(statement.predicate);
objectClone = this.cloneNode(statement.object);
// Set the object to the new aggregation pid
objectClone.value = `${this.getURIFromRDF(pid)}#aggregation`;
// Add the statement with the old subject and predicate but new
// aggregation object
this.dataPackageGraph.add(
subjectClone,
predicateClone,
objectClone,
);
});
// Remove all the old aggregation statements from the graph
this.dataPackageGraph.removeMany(
undefined,
undefined,
aggregationNode,
);
}
// Change all the resource map subject nodes in the RDF graph
const rMapNode = this.rdf.sym(this.getURIFromRDF(oldPid));
const rMapStatements = $.extend(
true,
[],
this.dataPackageGraph.statementsMatching(rMapNode),
);
// then repopulate them with correct values
rMapStatements.forEach((statement) => {
subjectClone = this.cloneNode(statement.subject);
predicateClone = this.cloneNode(statement.predicate);
objectClone = this.cloneNode(statement.object);
// In the case of modified date, reset it to now()
if (predicateClone.value === DC("modified")) {
objectClone.value = new Date().toISOString();
}
// Update the subject to the new pid
subjectClone.value = this.getURIFromRDF(pid);
// Remove the old resource map statement
this.dataPackageGraph.remove(statement);
// Add the statement with the new subject pid, but the same
// predicate and object
this.dataPackageGraph.add(
subjectClone,
predicateClone,
objectClone,
);
});
});
// Add the describes/isDescribedBy statements back in
this.dataPackageGraph.add(
this.rdf.sym(this.getURIFromRDF(pid)),
ORE("describes"),
this.rdf.sym(`${this.getURIFromRDF(pid)}#aggregation`),
);
this.dataPackageGraph.add(
this.rdf.sym(`${this.getURIFromRDF(pid)}#aggregation`),
ORE("isDescribedBy"),
this.rdf.sym(this.getURIFromRDF(pid)),
);
// Add nodes for new package members
addedIds.forEach((id) => this.addToAggregation(id));
} else {
// Create the OAI-ORE graph from scratch
this.dataPackageGraph = this.rdf.graph();
cnResolveUrl = this.getCnURI();
// Create a resource map node
const rMapNode = this.rdf.sym(
this.getURIFromRDF(this.packageModel.id),
);
// Create an aggregation node
const aggregationNode = this.rdf.sym(
`${this.getURIFromRDF(this.packageModel.id)}#aggregation`,
);
// Describe the resource map with a Creator
const creatorNode = this.rdf.blankNode();
const creatorName = this.rdf.lit(
`${MetacatUI.appUserModel.get("firstName") || ""} ${
MetacatUI.appUserModel.get("lastName") || ""
}`,
"",
XSD("string"),
);
this.dataPackageGraph.add(creatorNode, FOAF("name"), creatorName);
this.dataPackageGraph.add(creatorNode, RDF("type"), DCTERMS("Agent"));
this.dataPackageGraph.add(rMapNode, DC("creator"), creatorNode);
// Set the modified date
modifiedDate = this.rdf.lit(
new Date().toISOString(),
"",
XSD("dateTime"),
);
this.dataPackageGraph.add(
rMapNode,
DCTERMS("modified"),
modifiedDate,
);
this.dataPackageGraph.add(rMapNode, RDF("type"), ORE("ResourceMap"));
this.dataPackageGraph.add(
rMapNode,
ORE("describes"),
aggregationNode,
);
const idLiteral = this.rdf.lit(
this.packageModel.id,
"",
XSD("string"),
);
this.dataPackageGraph.add(rMapNode, DCTERMS("identifier"), idLiteral);
// Describe the aggregation
this.dataPackageGraph.add(
aggregationNode,
ORE("isDescribedBy"),
rMapNode,
);
// Aggregate each package member
idsFromModel.forEach((id) => this.addToAggregation(id));
}
// Remove any references to blank nodes not already cleaned up.
// rdflib.js will fail to serialize an IndexedFormula (graph) with
// statements whose object is a blank node when the blank node is not
// the subject of any other statements.
this.removeOrphanedBlankNodes();
const xmlString = serializer.statementsToXML(
this.dataPackageGraph.statements,
);
return xmlString;
},
/**
* Clone an rdflib.js Node by creaing a new node based on the original
* node RDF term type and data type.
* @param {Node} nodeToClone - The node to clone
* @returns {Node} - The cloned node
*/
cloneNode(nodeToClone) {
switch (nodeToClone.termType) {
case "NamedNode":
return this.rdf.sym(nodeToClone.value);
case "Literal":
// Check for the datatype for this literal value, e.g.
// http://www.w3.org/2001/XMLSchema#string"
if (typeof nodeToClone.datatype !== "undefined") {
return this.rdf.literal(
nodeToClone.value,
undefined,
nodeToClone.datatype,
);
}
return this.rdf.literal(nodeToClone.value);
case "BlankNode":
// Blank nodes don't need to be cloned
return nodeToClone; // (this.rdf.blankNode(nodeToClone.value));
case "Collection":
// TODO: construct a list of nodes for this term type.
return this.rdf.list(nodeToClone.value);
default:
// TODO: Handle error `unknown node type to clone:
// ${nodeToClone.termType}`
return null;
}
},
/**
* Adds a new object to the resource map RDF graph
* @param {string} id - The identifier of the object to add
*/
addToAggregation(id) {
// Initialize the namespaces
const ORE = this.rdf.Namespace(this.namespaces.ORE);
const DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS);
const XSD = this.rdf.Namespace(this.namespaces.XSD);
const CITO = this.rdf.Namespace(this.namespaces.CITO);
// Create a node for this object, the identifier, the resource map, and
// the aggregation
const objectNode = this.rdf.sym(this.getURIFromRDF(id));
const rMapURI = this.getURIFromRDF(this.packageModel.get("id"));
this.rdf.sym(rMapURI);
const aggNode = this.rdf.sym(`${rMapURI}#aggregation`);
const idNode = this.rdf.literal(id, undefined, XSD("string"));
let idStatements = [];
let aggStatements = [];
let aggByStatements = [];
let documentsStatements = [];
let isDocumentedByStatements = [];
// Add the statement: this object isAggregatedBy the resource map
// aggregation
aggByStatements = this.dataPackageGraph.statementsMatching(
objectNode,
ORE("isAggregatedBy"),
aggNode,
);
if (aggByStatements.length < 1) {
this.dataPackageGraph.add(objectNode, ORE("isAggregatedBy"), aggNode);
}
// Add the statement: The resource map aggregation aggregates this
// object
aggStatements = this.dataPackageGraph.statementsMatching(
aggNode,
ORE("aggregates"),
objectNode,
);
if (aggStatements.length < 1) {
this.dataPackageGraph.add(aggNode, ORE("aggregates"), objectNode);
}
// Add the statement: This object has the identifier {id} if it isn't
// present
idStatements = this.dataPackageGraph.statementsMatching(
objectNode,
DCTERMS("identifier"),
idNode,
);
if (idStatements.length < 1) {
this.dataPackageGraph.add(objectNode, DCTERMS("identifier"), idNode);
}
// Find the metadata doc that describes this object
const model = this.findWhere({ id });
const isDocBy = model.get("isDocumentedBy");
const documents = model.get("documents");
// Deal with Solr indexing bug where metadata-only packages must
// "document" themselves
if (isDocBy.length === 0 && documents.length === 0) {
documents.push(model.get("id"));
}
// If this object is documented by any metadata...
if (isDocBy && isDocBy.length) {
// Get the ids of all the metadata objects in this package
const metadataInPackage = _.compact(
_.map(this.models, (m) => {
if (m.get("formatType") === "METADATA") return m;
return null;
}),
);
const metadataInPackageIDs = _.each(metadataInPackage, (m) =>
m.get("id"),
);
// Find the metadata IDs that are in this package that also documents
// this data object
let metadataIds = Array.isArray(isDocBy)
? _.intersection(metadataInPackageIDs, isDocBy)
: _.intersection(metadataInPackageIDs, [isDocBy]);
// If this data object is not documented by one of these metadata
// docs, then we should check if it's documented by an obsoleted pid.
// If so, we'll want to change that so it's documented by a current
// metadata.
if (!metadataIds.length) {
for (let i = 0; i < metadataInPackage.length; i += 1) {
// If the previous version of this metadata documents this data,
if (_.contains(isDocBy, metadataInPackage[i].get("obsoletes"))) {
// Save the metadata id for serialization
metadataIds = [metadataInPackage[i].get("id")];
// Exit the for loop
break;
}
}
}
// For each metadata that documents this object, add a
// CITO:isDocumentedBy and CITO:documents statement
metadataIds.forEach((metaId) => {
// Create the named nodes and statements
const dataNode = this.rdf.sym(this.getURIFromRDF(id));
const metadataNode = this.rdf.sym(this.getURIFromRDF(metaId));
const isDocByStatement = this.rdf.st(
dataNode,
CITO("isDocumentedBy"),
metadataNode,
);
const documentsStatement = this.rdf.st(
metadataNode,
CITO("documents"),
dataNode,
);
// Add the statements
documentsStatements = this.dataPackageGraph.statementsMatching(
metadataNode,
CITO("documents"),
dataNode,
);
if (documentsStatements.length < 1) {
this.dataPackageGraph.add(documentsStatement);
}
isDocumentedByStatements = this.dataPackageGraph.statementsMatching(
dataNode,
CITO("isDocumentedBy"),
metadataNode,
);
if (isDocumentedByStatements.length < 1) {
this.dataPackageGraph.add(isDocByStatement);
}
});
}
// If this object documents a data object
if (documents && documents.length) {
// Create a literal node for it
const metadataNode = this.rdf.sym(this.getURIFromRDF(id));
documents.forEach((dataID) => {
// Make sure the id is one that will be aggregated
if (_.contains(this.idsToAggregate, dataID)) {
// Find the identifier statement for this data object
const dataURI = this.getURIFromRDF(dataID);
// Create a data node using the exact way the identifier URI is
// written
const dataNode = this.rdf.sym(dataURI);
// Get the statements for data isDocumentedBy metadata
isDocumentedByStatements =
this.dataPackageGraph.statementsMatching(
dataNode,
CITO("isDocumentedBy"),
metadataNode,
);
// If that statement is not in the RDF already...
if (isDocumentedByStatements.length < 1) {
// Create a statement: This data is documented by this metadata
const isDocByStatement = this.rdf.st(
dataNode,
CITO("isDocumentedBy"),
metadataNode,
);
// Add the "isDocumentedBy" statement
this.dataPackageGraph.add(isDocByStatement);
}
// Get the statements for metadata documents data
documentsStatements = this.dataPackageGraph.statementsMatching(
metadataNode,
CITO("documents"),
dataNode,
);
// If that statement is not in the RDF already...
if (documentsStatements.length < 1) {
// Create a statement: This metadata documents data
const documentsStatement = this.rdf.st(
metadataNode,
CITO("documents"),
dataNode,
);
// Add the "isDocumentedBy" statement
this.dataPackageGraph.add(documentsStatement);
}
}
});
}
},
/**
* Removes an object from the aggregation in the RDF graph
* @param {string} id - The identifier of the object to remove
*/
removeFromAggregation(id) {
let identifier = id;
if (id.indexOf(this.dataPackageGraph.cnResolveUrl) === -1) {
identifier = this.getURIFromRDF(id);
}
// Create a literal node for the removed object
const removedObjNode = this.rdf.sym(identifier);
// Get the statements from the RDF where the removed object is the
// subject or object
const statements = $.extend(
true,
[],
_.union(
this.dataPackageGraph.statementsMatching(
undefined,
undefined,
removedObjNode,
),
this.dataPackageGraph.statementsMatching(removedObjNode),
),
);
// Remove all the statements mentioning this object
try {
this.dataPackageGraph.remove(statements);
} catch (error) {
// TODO: Handle the error
}
},
/**
* Finds the given identifier in the RDF graph and returns the subject URI
* of that statement. This is useful when adding additional statements to
* the RDF graph for an object that already exists in that graph.
* @param {string} id - The identifier to search for
* @returns {string} - The full URI for the given id as it exists in the
* RDF.
*/
getURIFromRDF(id) {
// Exit if no id was given
if (!id) return "";
// Create a literal node with the identifier as the value
const XSD = this.rdf.Namespace(this.namespaces.XSD);
const DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS);
const idNode = this.rdf.literal(id, undefined, XSD("string"));
// Find the identifier statements for the given id
const idStatements = this.dataPackageGraph.statementsMatching(
undefined,
DCTERMS("identifier"),
idNode,
);
// If this object has an identifier statement,
if (idStatements.length > 0) {
// Return the subject of the statement
return idStatements[0].subject.value;
}
return this.getCnURI() + encodeURIComponent(id);
},
/**
* Parses out the CN Resolve URL from the existing statements in the RDF
* or if not found in the RDF, from the app configuration.
* @returns {string} - The CN resolve URL
*/
getCnURI() {
// If the CN resolve URL was already found, return it
if (this.dataPackageGraph.cnResolveUrl) {
return this.dataPackageGraph.cnResolveUrl;
}
if (this.packageModel.get("oldPid")) {
// Find the identifier statement for the resource map in the RDF
// graph
const idNode = this.rdf.lit(this.packageModel.get("oldPid"));
const idStatements = this.dataPackageGraph.statementsMatching(
undefined,
undefined,
idNode,
);
const idStatement = idStatements.length ? idStatements[0] : null;
if (idStatement) {
// Parse the CN resolve URL from the statement subject URI
this.dataPackageGraph.cnResolveUrl =
idStatement.subject.value.substring(
0,
idStatement.subject.value.indexOf(
this.packageModel.get("oldPid"),
),
) ||
idStatement.subject.value.substring(
0,
idStatement.subject.value.indexOf(
encodeURIComponent(this.packageModel.get("oldPid")),
),
);
} else {
this.dataPackageGraph.cnResolveUrl =
MetacatUI.appModel.get("resolveServiceUrl");
}
} else {
this.dataPackageGraph.cnResolveUrl =
MetacatUI.appModel.get("resolveServiceUrl");
}
// Return the CN resolve URL
return this.dataPackageGraph.cnResolveUrl;
},
/**
* Checks if this resource map has had any changes that requires an update
* @returns {boolean} - True if the resource map needs to be updated
*/
needsUpdate() {
// Check for changes to the list of aggregated members
const ids = this.pluck("id");
if (
this.originalMembers.length !== ids.length ||
_.intersection(this.originalMembers, ids).length !== ids.length
)
return true;
// If the provenance relationships have been updated, then the resource
// map needs to be updated.
if (this.provEdits.length) return true;
// Check for changes to the isDocumentedBy relationships
let isDifferent = false;
let i = 0;
// Keep going until we find a difference
while (!isDifferent && i < this.length) {
// Get the original isDocBy relationships from the resource map, and
// the new isDocBy relationships from the models
let isDocBy = this.models[i].get("isDocumentedBy");
const id = this.models[i].get("id");
let origIsDocBy = this.originalIsDocBy[id];
// Make sure they are both formatted as arrays for these checks
isDocBy = _.uniq(
_.flatten(_.compact(Array.isArray(isDocBy) ? isDocBy : [isDocBy])),
);
origIsDocBy = _.uniq(
_.flatten(
_.compact(
Array.isArray(origIsDocBy) ? origIsDocBy : [origIsDocBy],
),
),
);
// Remove the id of this object so metadata can not be
// "isDocumentedBy" itself
isDocBy = _.without(isDocBy, id);
origIsDocBy = _.without(origIsDocBy, id);
// Simply check if they are the same
if (origIsDocBy === isDocBy) {
i += 1;
}
// Are the number of relationships different?
else if (isDocBy.length !== origIsDocBy.length) isDifferent = true;
// Are the arrays the same?
else if (
_.intersection(isDocBy, origIsDocBy).length !== origIsDocBy.length
)
isDifferent = true;
i += 1;
}
return isDifferent;
},
/**
* Gets objects not yet uploaded to the DataONE server
* @returns {Array} An array of models that are in the queue or in
* progress of uploading
*/
getQueue() {
return this.filter(
(m) => m.get("uploadStatus") === "q" || m.get("uploadStatus") === "p",
);
},
/**
* Adds a DataONEObject model to this DataPackage collection
* @param {DataONEObject} model - The DataONEObject model to add
*/
addNewModel(model) {
// Check that this collection doesn't already contain this model
if (!this.contains(model)) {
this.add(model);
// Mark this data package as changed
this.packageModel.set("changed", true);
this.packageModel.trigger("change:changed");
}
},
/**
* Actions ot perform when a DataONEObject model is added to this
* collection
* @param {DataONEObject} dataONEObject - The DataONEObject model that was
* added
*/
handleAdd(dataONEObject) {
const metadataModel = this.find((m) => m.get("type") === "Metadata");
// Append to or create a new documents list
if (metadataModel) {
if (!Array.isArray(metadataModel.get("documents"))) {
metadataModel.set("documents", [dataONEObject.id]);
} else if (
!_.contains(metadataModel.get("documents"), dataONEObject.id)
)
metadataModel.get("documents").push(dataONEObject.id);
// Create an EML Entity for this DataONE Object if there isn't one
// already
if (
metadataModel.type === "EML" &&
!dataONEObject.get("metadataEntity") &&
dataONEObject.type !== "EML"
) {
metadataModel.createEntity(dataONEObject);
metadataModel.set("uploadStatus", "q");
}
}
this.saveReference(dataONEObject);
this.setLoadingFiles(dataONEObject);
},
/**
* Fetches this DataPackage from the Solr index by using a SolrResults
* collection and merging the models in.
*/
fetchFromIndex() {
if (typeof this.solrResults === "undefined" || !this.solrResults) {
this.solrResults = new SolrResults();
}
// If no query is set yet, use the FilterModel associated with this
// DataPackage
if (!this.solrResults.currentquery.length) {
this.solrResults.currentquery = this.filterModel.getQuery();
}
this.listenToOnce(this.solrResults, "reset", (solrResults) => {
// Merge the SolrResults into this collection
this.mergeModels(solrResults.models);
// Trigger the fetch as complete
this.trigger("complete");
});
// Query the index for this data package
this.solrResults.query();
},
/**
* Merge the attributes of other models into the corresponding models in
* this collection. This should be used when merging models of other types
* (e.g. SolrResult) that represent the same object that the DataONEObject
* models in the collection represent.
* @param {Backbone.Model[]} otherModels - the other models to merge with
* the models in this collection
* @param {string[]} [fieldsToMerge] - If specified, only these fields
* will be extracted from the otherModels
*/
mergeModels(otherModels, fieldsToMerge) {
// If no otherModels are given, exit the function since there is nothing
// to merge
if (
typeof otherModels === "undefined" ||
!otherModels ||
!otherModels.length
) {
return;
}
otherModels.forEach((otherModel) => {
// Get the model from this collection that matches ids with the other
// model
const modelInDataPackage = this.findWhere({
id: otherModel.get("id"),
});
// If a match is found,
if (modelInDataPackage) {
let valuesFromOtherModel;
// If specific fields to merge are given, get the values for those
// from the other model
if (fieldsToMerge && fieldsToMerge.length) {
valuesFromOtherModel = _.pick(otherModel.toJSON(), fieldsToMerge);
}
// If no specific fields are given, merge (almost) all others
else {
// Get the default values for this model type
const otherModelDefaults = otherModel.defaults;
// Get a JSON object of all the attributes on this model
const otherModelAttr = otherModel.toJSON();
// Start an array of attributes to omit during the merge
const omitKeys = [];
_.each(otherModelAttr, (val, key) => {
// If this model's attribute is the default, don't set it on our
// DataONEObject model because whatever value is in the
// DataONEObject model is better information than the default
// value of the other model.
if (otherModelDefaults[key] === val) omitKeys.push(key);
});
// Remove the properties that are still the default value
valuesFromOtherModel = _.omit(otherModelAttr, omitKeys);
}
// Set the values from the other model on the model in this
// collection
modelInDataPackage.set(valuesFromOtherModel);
}
});
},
/** Update the relationships in this resource map when its been udpated */
updateRelationships() {
// Get the old id
const oldId = this.packageModel.get("oldPid");
if (!oldId) return;
// Update the resource map list
this.each((m) => {
const updateRMaps = _.without(m.get("resourceMap"), oldId);
updateRMaps.push(this.packageModel.get("id"));
m.set("resourceMap", updateRMaps);
}, this);
},
/**
* Save a reference to this collection in the model
* @param {DataONEObject} model - The model to save a reference to
*/
saveReference(model) {
const currentCollections = model.get("collections");
if (currentCollections.length > 0) {
currentCollections.push(this);
model.set("collections", _.uniq(currentCollections));
} else model.set("collections", [this]);
},
/**
* Broadcast an accessPolicy across members of this package
*
* Note: Currently just sets the incoming accessPolicy on this object and
* doesn't broadcast to other members (such as data). How this works is
* likely to change in the future.
*
* Closely tied to the AccessPolicyView.broadcast property.
* @param {AccessPolicy} accessPolicy - The accessPolicy to broadcast
*/
broadcastAccessPolicy(accessPolicy) {
if (!accessPolicy) {
return;
}
const policy = _.clone(accessPolicy);
this.packageModel.set("accessPolicy", policy);
// Stop now if the package is new because we don't want force a save
// just yet
if (this.packageModel.isNew()) {
return;
}
this.packageModel.on("sysMetaUpdateError", (_e) => {
// Show a generic error. Any errors at this point are things the user
// can't really recover from. i.e., we've already checked that the
// user has changePermission perms and we've already re-tried the
// request a few times
const message =
"There was an error sharing your dataset. Not all of your changes were applied.";
// TODO: Is this really the right way to hook into the editor's error
// notification mechanism?
MetacatUI.appView.eml211EditorView.saveError(message);
});
this.packageModel.updateSysMeta();
},
/**
* Tracks the upload status of DataONEObject models in this collection. If
* they are `loading` into the DOM or `in progress` of an upload to the
* server, they will be considered as "loading" files.
* @param {DataONEObject} [dataONEObject] - A model to begin tracking.
* Optional. If no DataONEObject is given, then only the number of loading
* files will be calcualted and set on the packageModel.
* @since 2.17.1
*/
setLoadingFiles(dataONEObject) {
// Set the number of loading files and the isLoadingFiles flag
const numLoadingFiles =
this.where({ uploadStatus: "l" }).length +
this.where({ uploadStatus: "p" }).length;
this.packageModel.set({
isLoadingFiles: numLoadingFiles > 0,
numLoadingFiles,
});
if (dataONEObject) {
// Listen to the upload status to update the flag
this.listenTo(dataONEObject, "change:uploadStatus", () => {
// If the object is done being successfully saved
if (dataONEObject.get("uploadStatus") === "c") {
const newNumLoadingFiles =
this.where({ uploadStatus: "l" }).length +
this.where({ uploadStatus: "p" }).length;
// If all models in this DataPackage have finished loading, then
// mark the loading as complete
if (!newNumLoadingFiles) {
this.packageModel.set({
isLoadingFiles: false,
numLoadingFiles: newNumLoadingFiles,
});
} else {
this.packageModel.set("numLoadingFiles", newNumLoadingFiles);
}
}
});
}
},
/**
* Returns atLocation information found in this resourceMap for all the
* PIDs in this resourceMap
* @returns {object} - object with PIDs as key and atLocation paths as
* values
* @since 2.28.0
*/
getAtLocation() {
return this.atLocationObject;
},
/**
* Get the absolute path from a relative path, handling '~', '..', and
* '.'.
* @param {string} relativePath - The relative path to be converted to an
* absolute path.
* @returns {string} The absolute path after processing '~', '..', and
* '.'. If the result is empty, returns '/'.
* @since 2.28.0
*/
getAbsolutePath(relativePath) {
// Replace ~ with an empty space
const fullPath = relativePath.replace(/^~(?=$|\/|\\)/, "");
// Process '..' and '.'
const components = fullPath.split("/");
const resolvedPath = components.reduce((accumulator, component) => {
if (component === "..") {
accumulator.pop();
} else if (component !== "." && component !== "") {
accumulator.push(component);
}
return accumulator;
}, []);
// Join the resolved path components with '/'
const result = resolvedPath.join("/");
return result || "/";
},
},
);
return DataPackage;
});