diff --git a/package-lock.json b/package-lock.json index 1f62592fc..d6ed64872 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "metacatui", - "version": "2.27.0", + "version": "2.27.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "metacatui", - "version": "2.27.0", + "version": "2.27.1", "license": "Apache-2.0", "dependencies": { "@actions/core": "^1.9.1", @@ -522,9 +522,9 @@ } }, "node_modules/devtools-protocol": { - "version": "0.0.982423", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.982423.tgz", - "integrity": "sha512-FnVW2nDbjGNw1uD/JRC+9U5768W7e1TfUwqbDTcSsAu1jXFjITSX8w3rkW5FEpHRMPPGpvNSmO1pOpqByiWscA==" + "version": "0.0.1001819", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1001819.tgz", + "integrity": "sha512-G6OsIFnv/rDyxSqBa2lDLR6thp9oJioLsb2Gl+LbQlyoA9/OBAkrTU9jiCcQ8Pnh7z4d6slDiLaogR5hzgJLmQ==" }, "node_modules/diff": { "version": "5.2.0", @@ -1476,15 +1476,15 @@ } }, "node_modules/puppeteer": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-14.1.1.tgz", - "integrity": "sha512-4dC6GYR5YlXTmNO3TbYEHTdVSdml1cVD2Ok/h/f/xSTp4ITVdbRWkMjiOaEKRAhtIl6GqaP7B89zx+hfhcNGMQ==", - "deprecated": "< 18.1.0 is no longer supported", + "version": "14.4.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-14.4.1.tgz", + "integrity": "sha512-+H0Gm84aXUvSLdSiDROtLlOofftClgw2TdceMvvCU9UvMryappoeS3+eOLfKvoy4sm8B8MWnYmPhWxVFudAOFQ==", + "deprecated": "< 19.4.0 is no longer supported", "hasInstallScript": true, "dependencies": { "cross-fetch": "3.1.5", "debug": "4.3.4", - "devtools-protocol": "0.0.982423", + "devtools-protocol": "0.0.1001819", "extract-zip": "2.0.1", "https-proxy-agent": "5.0.1", "pkg-dir": "4.2.0", @@ -1493,7 +1493,7 @@ "rimraf": "3.0.2", "tar-fs": "2.1.1", "unbzip2-stream": "1.4.3", - "ws": "8.6.0" + "ws": "8.7.0" }, "engines": { "node": ">=14.1.0" @@ -1900,9 +1900,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "node_modules/ws": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", - "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz", + "integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==", "engines": { "node": ">=10.0.0" }, @@ -2325,9 +2325,9 @@ "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, "devtools-protocol": { - "version": "0.0.982423", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.982423.tgz", - "integrity": "sha512-FnVW2nDbjGNw1uD/JRC+9U5768W7e1TfUwqbDTcSsAu1jXFjITSX8w3rkW5FEpHRMPPGpvNSmO1pOpqByiWscA==" + "version": "0.0.1001819", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1001819.tgz", + "integrity": "sha512-G6OsIFnv/rDyxSqBa2lDLR6thp9oJioLsb2Gl+LbQlyoA9/OBAkrTU9jiCcQ8Pnh7z4d6slDiLaogR5hzgJLmQ==" }, "diff": { "version": "5.2.0", @@ -3044,13 +3044,13 @@ } }, "puppeteer": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-14.1.1.tgz", - "integrity": "sha512-4dC6GYR5YlXTmNO3TbYEHTdVSdml1cVD2Ok/h/f/xSTp4ITVdbRWkMjiOaEKRAhtIl6GqaP7B89zx+hfhcNGMQ==", + "version": "14.4.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-14.4.1.tgz", + "integrity": "sha512-+H0Gm84aXUvSLdSiDROtLlOofftClgw2TdceMvvCU9UvMryappoeS3+eOLfKvoy4sm8B8MWnYmPhWxVFudAOFQ==", "requires": { "cross-fetch": "3.1.5", "debug": "4.3.4", - "devtools-protocol": "0.0.982423", + "devtools-protocol": "0.0.1001819", "extract-zip": "2.0.1", "https-proxy-agent": "5.0.1", "pkg-dir": "4.2.0", @@ -3059,7 +3059,7 @@ "rimraf": "3.0.2", "tar-fs": "2.1.1", "unbzip2-stream": "1.4.3", - "ws": "8.6.0" + "ws": "8.7.0" } }, "qs": { @@ -3371,9 +3371,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.6.0.tgz", - "integrity": "sha512-AzmM3aH3gk0aX7/rZLYvjdvZooofDu3fFOzGqcSnQ1tOcTWwhM/o+q++E8mAyVVIyUdajrkzWUGftaVSDLn1bw==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.7.0.tgz", + "integrity": "sha512-c2gsP0PRwcLFzUiA8Mkr37/MI7ilIlHQxaEAtd0uNMbVMoy8puJyafRlm0bV9MbGSabUPeLrRRaqIBcFcA2Pqg==", "requires": {} }, "xmlcreate": { diff --git a/src/css/metacatui-common.css b/src/css/metacatui-common.css index 9c5252a3f..5e2f002a6 100644 --- a/src/css/metacatui-common.css +++ b/src/css/metacatui-common.css @@ -556,6 +556,64 @@ text-shadow: none; .data-tag-icon.crimson g{ fill: #800000; } +/****************************************** +** CatalogSearchView *** +******************************************/ +.catalog-search-view .catalog-search-inner{ + display: grid; + justify-content: stretch; + align-items: stretch; + grid-template-columns: auto 1fr 1fr; +} +.catalog-search-view .filter-groups-container { + width: 215px; + padding: var(--pad) +} +.catalog-search-view .search-results-container, +.catalog-search-view .map-container { +} +.catalog-search-body.mapMode{ + height: 100vh; + width: 100vw; + padding-bottom: 0px; + display: grid; + align-items: stretch; + justify-content: stretch; + overflow: hidden; +} +.catalog-search-body.mapMode #Content{ + padding: 40px 0px 0px 0px; +} +.catalog-search-body.mapMode .search-results-view .result-row:last-child{ + margin-bottom: 100px; +} +.catalog-search-body.mapMode .search-results-view { + overflow-y: scroll; + height: 100vh; + padding-bottom: 200px; /* Leaving room for the last row to show */ + padding-right: 15px; /* Padding for the scrollbar */ +} +.catalog-search-body.mapMode .search-results-panel-container .map-toggle-container{ + display: none; +} +.catalog-search-body.listMode .map-panel-container{ + display: none; +} +.catalog-search-body.listMode .catalog-search-inner{ + grid-template-columns: auto 1fr 0; +} +.catalog-search-view .cesium-widget-view { + width: inherit; + margin-left: 0; +} +.search-results-view .result-row{ + padding: var(--pad); +} +.catalog-search-view .no-search-results{ + padding: var(--pad); + text-align: center; +} + /****************************************** ** Results and Result Rows *** ******************************************/ @@ -7783,6 +7841,7 @@ ul.side-nav-items { #data-package-table { margin-bottom: 0px; width: calc(100% - 2px); + border-collapse: separate; } /* Editor package table headers */ #data-package-table-share, @@ -9072,4 +9131,102 @@ body > #extension-is-installed{ } .citation.header button.show-authors{ margin-left: 5px; -} \ No newline at end of file +} +/* Table Head */ +#data-package-table-head { + position: sticky; + top: 0; + z-index: 1; +} + +#data-package-table-head > .file-header > th { + background-color: #FDFDFD; + border-bottom: 1px solid #E3E3E3; + height: 32px; + position: sticky; + top: 0; + z-index: -2; +} + +#data-package-table-head > .file-header > th:not(:last-child) { + border-right: 1px solid #E3E3E3; + height: 30px; +} + +/* Table Body */ +#data-package-table-body > tr > td:first-child { + cursor: default; +} + +/* Table Container */ +.file-table-container { + height: max-content; + max-height: 25.4em; + overflow-y: scroll; + border: 1px solid #E3E3E3; + border-radius: 4px; +} + +.file-table-container::-webkit-scrollbar { + -webkit-appearance: none; + width: 10px; +} + +.file-table-container::-webkit-scrollbar-thumb { + border-radius: 5px; + background-color: rgba(0, 0, 0, 0.5); + -webkit-box-shadow: 0 0 1px rgba(255, 255, 255, 0.5); +} + +/* Buttons */ +.btn.btn-rounded.action { + border-radius: 50%; + height: 15px; + width: 15px; +} + +.btn.btn-rounded.action.downloadAction { + border: none; +} + +.btn.btn-rounded.action.downloadAction > i { + margin-left: -4px; +} + +/* Data Package Item */ +.data-package-item { + height: 38px; +} + +/* File Table Styles */ +.data-package-file-table > .data-package-item > .file-actions > .action .btn, +.data-package-file-table > .data-package-item > .file-actions > .action .btn-primary, +.data-package-file-table > .data-package-item > .file-actions > .action .btn-large, +.data-package-file-table > .data-package-item > .file-actions > .action .btn-small, +.data-package-file-table > .data-package-item > .file-actions > .action .btn-mini { + font-weight: 400; + background-image: none; + background-repeat: no-repeat; + background-color: transparent; + border: 1px solid #146660; + text-shadow: none; + box-shadow: none; + color: #146660; + padding: 0.5em; +} + +.data-package-file-table > .data-package-item > .file-actions > .action .btn:hover, +.data-package-file-table > .data-package-item > .file-actions > .action .btn:focus, +.header .nav li .btn:hover, +.header nav li .btn:focus { + background-color: #146660; + color: #FFF; + border: 0; + box-shadow: none; + border: 1px solid #146660; +} + +/* Tooltip */ +.data-package-file-table div.tooltip { + white-space: wrap; +} diff --git a/src/js/collections/DataPackage.js b/src/js/collections/DataPackage.js index 57a09c5a7..681d1fc49 100644 --- a/src/js/collections/DataPackage.js +++ b/src/js/collections/DataPackage.js @@ -1,14 +1,32 @@ /* global define */ "use strict"; -define(['jquery', 'underscore', 'backbone', 'rdflib', "uuid", "md5", - 'collections/SolrResults', - 'models/filters/Filter', - 'models/DataONEObject', 'models/metadata/ScienceMetadata', 'models/metadata/eml211/EML211'], - function($, _, Backbone, rdf, uuid, md5, SolrResults, Filter, - DataONEObject, ScienceMetadata, EML211) { - - /** +define([ + "jquery", + "underscore", + "backbone", + "rdflib", + "uuid", + "md5", + "collections/SolrResults", + "models/filters/Filter", + "models/DataONEObject", + "models/metadata/ScienceMetadata", + "models/metadata/eml211/EML211", +], function ( + $, + _, + 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. @@ -18,2133 +36,2575 @@ define(['jquery', 'underscore', 'backbone', 'rdflib', "uuid", "md5", * @extends Backbone.Collection * @constructor */ - var DataPackage = Backbone.Collection.extend( - /** @lends DataPackage.prototype */{ + var DataPackage = Backbone.Collection.extend( + /** @lends DataPackage.prototype */ { + /** + * The name of this type of collection + * @type {string} + */ + type: "DataPackage", - /** - * The name of this type of collection - * @type {string} - */ - type: "DataPackage", + /** + * The package identifier + * @type {string} + */ + id: null, - /** - * 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: [], + /** + * 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 ued for the package's edit status. Can be - * set to false to 'lock' the package - * @type {boolean} - */ - editable: true, + /** A flag ued 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, + /** + * The RDF graph representing this data package + * @type {RDFGraph} + */ + dataPackageGraph: null, - /** - * A DataONEObject representing the resource map itself - * @type {DataONEObject} - */ - packageModel: 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: {}, + /** 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: [], - - /** - * 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", + /** 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: [], - /** - * The nesting level in a data package hierarchy - * @type {number} - */ - nodeLevel: 0, + /** + * 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 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(), + /** + * The nesting level in a data package hierarchy + * @type {number} + */ + nodeLevel: 0, - /** - * A Filter model that should filter the Solr index for only the - * objects aggregated by this package. - * @type {Filter} - */ - filterModel: null, + /** + * 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(), - /** Define the namespaces used in the RDF XML - * @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#" - }, - - sources: [], - derivations: [], - provenanceFlag: null, - sourcePackages: [], - derivationPackages: [], - relatedModels: [], - - /** - * 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'] - */ - provEdits: [], + /** + * A Filter model that should filter the Solr index for only the + * objects aggregated by this package. + * @type {Filter} + */ + filterModel: null, - /** - * The number of models that have been updated during the current save(). - * This is reset to zero after the current save() is complete. - */ - numSaves: 0, - - // Constructor: Initialize a new DataPackage - initialize: function(models, options) { - if(typeof options == "undefined") - var 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(); - - let 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", function(){ - 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 - url: function(options) { - if(options && options.update){ - return MetacatUI.appModel.get("objectServiceUrl") + - (encodeURIComponent(this.packageModel.get("oldPid")) || encodeURIComponent(this.packageModel.get("seriesid"))); - } - else{ - //URL encode the id or seriesId - var 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) - else{ - return MetacatUI.appModel.get("resolveServiceUrl") + encodedId; + /** Define the namespaces used in the RDF XML + * @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#", + }, + + sources: [], + derivations: [], + provenanceFlag: null, + sourcePackages: [], + derivationPackages: [], + relatedModels: [], + + /** + * 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'] + */ + provEdits: [], + + /** + * The number of models that have been updated during the current save(). + * This is reset to zero after the current save() is complete. + */ + numSaves: 0, + + // Constructor: Initialize a new DataPackage + initialize: function (models, options) { + if (typeof options == "undefined") var 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(); + + let 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, + }) + ); } - } - }, - /* - * The DataPackage collection stores DataPackages and - * DataONEObjects, including Metadata and Data objects. - * Return the correct model based on the type - */ - model: function (attrs, options) { + 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", function () { + this.filterModel.set("values", [ + this.packageModel.get("id"), + ]); + }); - switch ( attrs.formatid ) { + this.on("add", this.handleAdd); + this.on("add", this.triggerComplete); + this.on("successSaving", this.updateRelationships); - case "http://www.openarchives.org/ore/terms": - return new DataPackage(null, {packageModel: attrs}); // TODO: is this correct? + return this; + }, - case "eml://ecoinformatics.org/eml-2.0.0": - return new EML211(attrs, options); + // Build the DataPackage URL based on the MetacatUI.appModel.objectServiceUrl + // and id or seriesid + url: function (options) { + if (options && options.update) { + return ( + MetacatUI.appModel.get("objectServiceUrl") + + (encodeURIComponent(this.packageModel.get("oldPid")) || + encodeURIComponent( + this.packageModel.get("seriesid") + )) + ); + } else { + //URL encode the id or seriesId + var 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) + else { + return ( + MetacatUI.appModel.get("resolveServiceUrl") + + encodedId + ); + } + } + }, - case "eml://ecoinformatics.org/eml-2.0.1": - return new EML211(attrs, options); + /* + * The DataPackage collection stores DataPackages and + * DataONEObjects, including Metadata and Data objects. + * Return the correct model based on the type + */ + 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.1.0": - return new EML211(attrs, options); + case "eml://ecoinformatics.org/eml-2.0.0": + return new EML211(attrs, options); - case "eml://ecoinformatics.org/eml-2.1.1": - 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.1": - return new EML211(attrs, options); + case "eml://ecoinformatics.org/eml-2.1.0": + return new EML211(attrs, options); - case "-//ecoinformatics.org//eml-access-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "eml://ecoinformatics.org/eml-2.1.1": + return new EML211(attrs, options); - case "-//ecoinformatics.org//eml-access-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "eml://ecoinformatics.org/eml-2.1.1": + return new EML211(attrs, options); - case "-//ecoinformatics.org//eml-attribute-2.0.0beta4//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-access-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-access-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-attribute-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-attribute-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-constraint-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-constraint-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-coverage-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-coverage-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-dataset-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-dataset-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-distribution-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-distribution-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-entity-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-entity-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-literature-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-literature-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-party-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-party-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-physical-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-physical-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-project-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-project-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-protocol-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-protocol-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-resource-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "-//ecoinformatics.org//eml-software-2.0.0beta6//EN": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-resource-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "FGDC-STD-001-1998": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-software-2.0.0beta4//EN": + return new ScienceMetadata(attrs, options); - case "FGDC-STD-001.1-1999": - return new ScienceMetadata(attrs, options); + case "-//ecoinformatics.org//eml-software-2.0.0beta6//EN": + return new ScienceMetadata(attrs, options); - case "FGDC-STD-001.2-1999": - return new ScienceMetadata(attrs, options); + case "FGDC-STD-001-1998": + return new ScienceMetadata(attrs, options); - case "INCITS-453-2009": - return new ScienceMetadata(attrs, options); + case "FGDC-STD-001.1-1999": + return new ScienceMetadata(attrs, options); - case "ddi:codebook:2_5": - return new ScienceMetadata(attrs, options); + case "FGDC-STD-001.2-1999": + return new ScienceMetadata(attrs, options); - case "http://datacite.org/schema/kernel-3.0": - return new ScienceMetadata(attrs, options); + case "INCITS-453-2009": + return new ScienceMetadata(attrs, options); - case "http://datacite.org/schema/kernel-3.1": - return new ScienceMetadata(attrs, options); + case "ddi:codebook:2_5": + return new ScienceMetadata(attrs, options); - case "http://datadryad.org/profile/v3.1": - return new ScienceMetadata(attrs, options); + case "http://datacite.org/schema/kernel-3.0": + return new ScienceMetadata(attrs, options); - case "http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd": - return new ScienceMetadata(attrs, options); + case "http://datacite.org/schema/kernel-3.1": + return new ScienceMetadata(attrs, options); - case "http://ns.dataone.org/metadata/schema/onedcx/v1.0": - return new ScienceMetadata(attrs, options); + case "http://datadryad.org/profile/v3.1": + return new ScienceMetadata(attrs, options); - case "http://purl.org/dryad/terms/": - return new ScienceMetadata(attrs, options); + case "http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd": + return new ScienceMetadata(attrs, options); - case "http://purl.org/ornl/schema/mercury/terms/v1.0": - return new ScienceMetadata(attrs, options); + case "http://ns.dataone.org/metadata/schema/onedcx/v1.0": + return new ScienceMetadata(attrs, options); - case "http://rs.tdwg.org/dwc/xsd/simpledarwincore/": - return new ScienceMetadata(attrs, options); + case "http://purl.org/dryad/terms/": + return new ScienceMetadata(attrs, options); - case "http://www.cuahsi.org/waterML/1.0/": - return new ScienceMetadata(attrs, options); + case "http://purl.org/ornl/schema/mercury/terms/v1.0": + return new ScienceMetadata(attrs, options); - case "http://www.cuahsi.org/waterML/1.1/": - return new ScienceMetadata(attrs, options); + case "http://rs.tdwg.org/dwc/xsd/simpledarwincore/": + return new ScienceMetadata(attrs, options); - case "http://www.esri.com/metadata/esriprof80.dtd": - return new ScienceMetadata(attrs, options); + case "http://www.cuahsi.org/waterML/1.0/": + return new ScienceMetadata(attrs, options); - case "http://www.icpsr.umich.edu/DDI": - return new ScienceMetadata(attrs, options); + case "http://www.cuahsi.org/waterML/1.1/": + return new ScienceMetadata(attrs, options); - case "http://www.isotc211.org/2005/gmd": - return new ScienceMetadata(attrs, options); + case "http://www.esri.com/metadata/esriprof80.dtd": + return new ScienceMetadata(attrs, options); - case "http://www.isotc211.org/2005/gmd-noaa": - return new ScienceMetadata(attrs, options); + case "http://www.icpsr.umich.edu/DDI": + return new ScienceMetadata(attrs, options); - case "http://www.loc.gov/METS/": - return new ScienceMetadata(attrs, options); + case "http://www.isotc211.org/2005/gmd": + return new ScienceMetadata(attrs, options); - case "http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2": - return new ScienceMetadata(attrs, options); + case "http://www.isotc211.org/2005/gmd-noaa": + return new ScienceMetadata(attrs, options); - default: - return new DataONEObject(attrs, options); + case "http://www.loc.gov/METS/": + return new ScienceMetadata(attrs, options); - } - }, - - /** - * Overload fetch calls for a DataPackage - * - * @param {Object} [options] - 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. - */ - fetch: function(options) { + case "http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2": + return new ScienceMetadata(attrs, options); - //Fetch the system metadata for this resource map - this.packageModel.fetch(); + default: + return new DataONEObject(attrs, options); + } + }, - if(typeof options == "object"){ + /** + * Overload fetch calls for a DataPackage + * + * @param {Object} [options] - 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. + */ + fetch: function(options) { + // 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); + } + // If the fetchFromIndex property is set to true + else if(options.fromIndex) { + this.fetchFromIndex(); + return; + } + } + + // Set some custom fetch options + var fetchOptions = _.extend({ dataType: "text" }, options); + + var thisPackage = this; + + // Function to retry fetching with user login details if the initial fetch fails + var retryFetch = function() { + // Add the authorization options + var 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(function() { + // trigger failure() + console.log("Fetch failed"); + + thisPackage.trigger("fetchFailed", thisPackage); + }); + }; + + // Fetch the resource map RDF XML + return Backbone.Collection.prototype.fetch.call(this, fetchOptions) + .fail(function() { + // If the initial fetch fails, retry with user login details + return retryFetch(); + }); + }, - //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; + /* + * Deserialize a Package from OAI-ORE RDF XML + */ + parse: function (response, options) { + //Save the raw XML in case it needs to be used later + this.objectXML = response; + + var RDF = this.rdf.Namespace(this.namespaces.RDF), + FOAF = this.rdf.Namespace(this.namespaces.FOAF), + OWL = this.rdf.Namespace(this.namespaces.OWL), + DC = this.rdf.Namespace(this.namespaces.DC), + ORE = this.rdf.Namespace(this.namespaces.ORE), + DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), + CITO = this.rdf.Namespace(this.namespaces.CITO), + PROV = this.rdf.Namespace(this.namespaces.PROV), + XSD = this.rdf.Namespace(this.namespaces.XSD); + + var memberStatements = [], + atLocationStatements = [], // array to store atLocation statements + memberURIParts, + memberPIDStr, + memberPID, + memberPIDs = [], + memberModel, + documentsStatements, + objectParts, + objectPIDStr, + objectPID, + objectAtLocationValue, + scimetaID, // documentor + scidataID, // documentee + models = []; // the models returned by parse() - this.once("reset", this.triggerComplete); + try { + //First, make sure we are only using one CN Base URL in the RDF or the RDF parsing will fail. + var cnResolveUrl = + MetacatUI.appModel.get("resolveServiceUrl"); + + var cnURLs = _.uniq( + response.match( + /cn\S+\.test\.dataone\.org\/cn\/v\d\/resolve|cn\.dataone\.org\/cn\/v\d\/resolve/g + ) + ); + if (cnURLs.length > 1) { + response = response.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( + response, + 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 + _.each( + memberStatements, + function (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); + + var 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")]; + } + }, + this + ); + + //Save the list of original ids + this.originalMembers = memberPIDs; + + // Get the isDocumentedBy relationships + documentsStatements = + this.dataPackageGraph.statementsMatching( + undefined, + CITO("documents"), + undefined, + undefined + ); + + var sciMetaPids = []; + + _.each( + documentsStatements, + function (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); + var dataObj = _.find(models, function (m) { + return m.get("id") == scidataID; + }); + + if (dataObj) { + //Get the isDocumentedBy field + var 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); + } + }, + this + ); + + //Save the list of science metadata pids + this.sciMetaPids = sciMetaPids; + + // Parse atLocation + var atLocationObject = {}; + atLocationStatements = + this.dataPackageGraph.statementsMatching( + undefined, + PROV("atLocation"), + undefined, + undefined + ); + + const ref = this; + + // Get atLocation information for each statement in the resourceMap + _.each( + atLocationStatements, + function (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), function (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) { + //Add the models to the collection now, silently + //this.add(models, {silent: true}); + + //Retrieve the model for each member + _.each( + models, + function (memberModel) { + var collection = this; + + memberModel.fetch(); + memberModel.once("sync", function (oldModel) { + //Get the right model type based on the model values + var newModel = + collection.getMember(oldModel); + + //If the model type has changed, then mark the model as unsynced, since there may be custom fetch() options for the new model + if (oldModel.type != newModel.type) { + // DataPackages shouldn't be fetched until we support nested packages better in the UI + if (newModel.type == "DataPackage") { + //Trigger a replace event so other parts of the app know when a model has been replaced with a different type + oldModel.trigger( + "replace", + newModel + ); + } else { + newModel.set("synced", false); + + newModel.fetch(); + newModel.once( + "sync", + function (fetchedModel) { + fetchedModel.set( + "synced", + true + ); + + //Remove the model from the collection and add it back + collection.remove(oldModel); + collection.add( + fetchedModel + ); + + //Trigger a replace event so other parts of the app know when a model has been replaced with a different type + oldModel.trigger( + "replace", + newModel + ); + + if (newModel.type == "EML") + collection.trigger( + "add:EML" + ); + } + ); + } + } else { + newModel.set("synced", true); + collection.add(newModel, { + merge: true, + }); + + if (newModel.type == "EML") + collection.trigger("add:EML"); + } + }); + }, + this + ); + } + } catch (error) { + console.log(error); } - //If the fetchFromIndex property is set to true - else if( options.fromIndex ){ - this.fetchFromIndex(); - return; + // 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: function () { + try { + /* Now run the SPARQL queries for the provenance relationships */ + var provQueries = []; + /* result: pidValue, wasDerivedFromValue (prov_wasDerivedFrom) */ + provQueries["prov_wasDerivedFrom"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \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"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \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"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \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"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \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"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \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"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \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"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \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"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \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"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \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"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \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"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \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"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \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"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \n" + + "PREFIX 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"] = + " \n" + + "PREFIX rdfs: \n" + + "PREFIX owl: \n" + + "PREFIX prov: \n" + + "PREFIX provone: \n" + + "PREFIX ore: \n" + + "PREFIX dcterms: \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 + var 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 (var iquery = 0; iquery < keys.length; iquery++) { + var eq = rdf.SPARQLToQuery( + provQueries[keys[iquery]], + false, + this.dataPackageGraph + ); + this.dataPackageGraph.query( + eq, + this.onResult, + this.url(), + this.onDone + ); + } + } catch (error) { + console.log(error); } - } + }, - //Set some custom fetch options - var fetchOptions = _.extend({dataType: "text"}, options); + // The return values have to be extracted from the result. + getValue: function (result, name) { + var res = result[name]; + // The result is of type 'NamedNode', just return the string value + if (res) { + return res.value; + } else return " "; + }, - //Add the authorization options - fetchOptions = _.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings()); + /* 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. - //Fetch the resource map RDF XML - return Backbone.Collection.prototype.fetch.call(this, fetchOptions); - }, + 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: function (result) { + var currentPid = this.getValue(result, "?pid"); + var resval; + var provFieldResult; + var provFieldValues; + + // 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 !== " ") { + var currentMember = null; + var provFieldValues; + var fieldName = null; + var vals = []; + var resultMember = null; + currentMember = this.find(function (model) { + return model.get("id") === currentPid; + }); - /* - * Deserialize a Package from OAI-ORE RDF XML - */ - parse: function(response, options) { - - //Save the raw XML in case it needs to be used later - this.objectXML = response; - - var RDF = this.rdf.Namespace(this.namespaces.RDF), - FOAF = this.rdf.Namespace(this.namespaces.FOAF), - OWL = this.rdf.Namespace(this.namespaces.OWL), - DC = this.rdf.Namespace(this.namespaces.DC), - ORE = this.rdf.Namespace(this.namespaces.ORE), - DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), - CITO = this.rdf.Namespace(this.namespaces.CITO), - XSD = this.rdf.Namespace(this.namespaces.XSD); - - var memberStatements = [], - memberURIParts, - memberPIDStr, - memberPID, - memberPIDs = [], - memberModel, - documentsStatements, - scimetaID, // documentor - scidataID, // documentee - 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. - var cnResolveUrl = MetacatUI.appModel.get('resolveServiceUrl'); - - var cnURLs = _.uniq(response.match(/cn\S+\.test\.dataone\.org\/cn\/v\d\/resolve|cn\.dataone\.org\/cn\/v\d\/resolve/g)); - if(cnURLs.length > 1){ - response = response.replace(/cn\S+\.test\.dataone\.org\/cn\/v\d\/resolve|cn\.dataone\.org\/cn\/v\d\/resolve/g, cnResolveUrl.substring(cnResolveUrl.indexOf("https://")+8)); + 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 (var iFld = 0; iFld < this.provFields.length; iFld++) { + 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. + 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(function (model) { + return model.get("id") === provFieldResult; + }); + + if (typeof resultMember !== "undefined") { + // If this prov field is a 'source' field, add it to 'sources' + + if (currentMember.isSourceField(fieldName)) { + // Get the package member that the id of the prov field is associated with + if ( + _.findWhere(this.sources, { + id: provFieldResult, + }) == null + ) { + this.sources.push(resultMember); + } + // Only add the result member if it has not already been added. + if ( + _.findWhere( + currentMember.get("provSources"), + { id: provFieldResult } + ) == null + ) { + vals = currentMember.get("provSources"); + vals.push(resultMember); + currentMember.set("provSources", vals); + } + } else if ( + currentMember.isDerivationField(fieldName) + ) { + // If this prov field is a 'derivation' field, add it to 'derivations' + if ( + _.findWhere(this.derivations, { + id: provFieldResult, + }) == null + ) { + this.derivations.push(resultMember); + } + + if ( + _.findWhere( + currentMember.get( + "provDerivations" + ), + { id: provFieldResult } + ) == null + ) { + 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: function () { + if (this.queriesToRun > 1) { + this.queriesToRun--; + } else { + // Signal that all prov queries have finished + this.provenanceFlag = "complete"; + this.trigger("queryComplete"); } + }, + /* + * Use the DataONEObject parseSysMeta() function + */ + parseSysMeta: function () { + return DataONEObject.parseSysMeta.call(this, arguments[0]); + }, - this.rdf.parse(response, this.dataPackageGraph, this.url(), 'application/rdf+xml'); + /** + * 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: function (options) { + if (!options) var options = {}; - // List the package members - memberStatements = this.dataPackageGraph.statementsMatching( - undefined, ORE("aggregates"), undefined, undefined); + this.packageModel.set("uploadStatus", "p"); - // Get system metadata for each member to eval the formatId - _.each(memberStatements, function(memberStatement){ - memberURIParts = memberStatement.object.value.split("/"); - memberPIDStr = _.last(memberURIParts); - memberPID = decodeURIComponent(memberPIDStr); + //Get the system metadata first if we haven't retrieved it yet + if (!this.packageModel.get("sysMetaXML")) { + var collection = this; + this.packageModel.fetch({ + success: function () { + collection.save(options); + }, + }); + return; + } - if ( memberPID ) - memberPIDs.push(memberPID); + //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; + } - //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)){ + if (options.resourceMapOnly !== true) { + //Sort the models in the collection so the metadata is saved first + var metadataModels = this.where({ type: "Metadata" }); + var dataModels = _.difference(this.models, metadataModels); + var sortedModels = _.union(metadataModels, dataModels); + var modelsInProgress = _.filter(sortedModels, function (m) { + return ( + m.get("uploadStatus") == "p" || + m.get("sysMetaUploadStatus") == "p" + ); + }); + var modelsToBeSaved = _.filter(sortedModels, function (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 + return ( + (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. + var sysMetaToUpdate = _.reject(dataModels, function (m) { + // Find models that don't have any content changes to save, + // and whose system metadata is not already saving + return ( + !m.hasUpdates() || + m.get("hasContentChanges") || + m.get("sysMetaUploadStatus") == "p" || + m.get("sysMetaUploadStatus") == "c" || + m.get("sysMetaUploadStatus") == "e" + ); + }); - memberModel = new DataONEObject({ - id: memberPID, - resourceMap: [this.packageModel.get("id")], - collections: [this] - }); + //First quickly validate all the models before attempting to save any + var allValid = _.every(modelsToBeSaved, function (m) { + if (m.isValid()) { + m.trigger("valid"); + return true; + } else { + return false; + } + }); - models.push(memberModel); + // If at least once model to be saved is invalid, + // or the metadata failed to save, cancel the save. + if ( + !allValid || + _.contains( + _.map(metadataModels, function (model) { + return 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 + var 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 the model already exists, add this resource map ID to it's list of resource maps - else{ - memberModel = this.get(memberPID); - models.push(memberModel); - - var 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")]; + + //First save all the models of the collection, if needed + _.each( + modelsToBeSaved, + function (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++; + }, + this + ); + + //Save the system metadata of all the Data objects + _.each( + sysMetaToUpdate, + function (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++; + }, + this + ); + + //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 + var 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; + } - }, this); + //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; - //Save the list of original ids - this.originalMembers = memberPIDs; + //Determine the HTTP request type + var requestType; + if (this.packageModel.isNew()) { + requestType = "POST"; + } else { + requestType = "PUT"; + } - // Get the isDocumentedBy relationships - documentsStatements = this.dataPackageGraph.statementsMatching( - undefined, CITO("documents"), undefined, undefined); + //Create a FormData object to send data with the XHR + var formData = new FormData(); - var sciMetaPids = []; + //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")); + } - _.each(documentsStatements, function(documentsStatement) { + //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 { + var 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; + } - // Extract and URI-decode the metadata pid - scimetaID = decodeURIComponent( - _.last(documentsStatement.subject.value.split("/"))); + //Make a Blob object from the serialized RDF XML + var mapBlob = new Blob([mapXML], { type: "application/xml" }); - sciMetaPids.push(scimetaID); + //Get the size of the new resource map + this.packageModel.set("size", mapBlob.size); - // Extract and URI-decode the data pid - scidataID = decodeURIComponent( - _.last(documentsStatement.object.value.split("/"))); + //Get the new checksum of the resource map + var checksum = md5(mapXML); + this.packageModel.set("checksum", checksum); + this.packageModel.set("checksumAlgorithm", "MD5"); - // 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]); + //Set the file name based on the id + this.packageModel.set( + "fileName", + this.packageModel.get("id").replace(/[^a-zA-Z0-9]/g, "_") + + ".rdf.xml" + ); - //Find the model in this collection for this data object - //var dataObj = this.get(scidataID); - var dataObj = _.find(models, function(m){ return m.get("id") == scidataID }); + //Create the system metadata + var sysMetaXML = this.packageModel.serializeSysMeta(); - if(dataObj){ - //Get the isDocumentedBy field - var 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]; + //Send the system metadata + var xmlBlob = new Blob([sysMetaXML], { + type: "application/xml", + }); - //Set the isDocumentedBy field - dataObj.set("isDocumentedBy", isDocBy); - } - }, this); + //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); + + var collection = this; + var requestSettings = { + url: this.packageModel.isNew() + ? this.url() + : this.url({ update: true }), + type: requestType, + cache: false, + contentType: false, + processData: false, + data: formData, + success: function (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" }), + function (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, function (dataModel) { + dataModel.set("sysMetaUploadStatus", "c"); + }); + }, + error: function (data) { + //Reset the id back to its original state + collection.packageModel.resetID(); + + //Reset the upload status for all members + _.each( + collection.where({ uploadStatus: "c" }), + function (m) { + m.set( + "uploadStatus", + m.defaults().uploadStatus + ); + } + ); + + //When there is no network connection (status == 0), there will be no response text + if (data.status == 408 || data.status == 0) { + var parsedResponse = + "There was a network issue that prevented this file from uploading. " + + "Make sure you are connected to a reliable internet connection."; + } else { + var parsedResponse = $(data.responseText) + .not("style, title") + .text(); + } - //Save the list of science metadata pids - this.sciMetaPids = sciMetaPids; + //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() + ) + ); + }, - //Put the science metadata pids first - memberPIDs = _.difference(memberPIDs, sciMetaPids); - _.each(_.uniq(sciMetaPids), function(id){ memberPIDs.unshift(id); }); + /* + * When a data package member updates, we evaluate it for its formatid, + * and update it appropriately if it is not a data object only + */ + getMember: function (context, args) { + var 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; - //Don't fetch each member model if the fetchModels property on this Collection is set to false - if( this.fetchModels !== false ){ + case "eml://ecoinformatics.org/eml-2.0.0": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new EML211(context.attributes); + break; - //Add the models to the collection now, silently - //this.add(models, {silent: true}); + case "eml://ecoinformatics.org/eml-2.0.1": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new EML211(context.attributes); + break; - //Retrieve the model for each member - _.each(models, function(memberModel){ + case "eml://ecoinformatics.org/eml-2.1.0": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new EML211(context.attributes); + break; - var collection = this; + case "eml://ecoinformatics.org/eml-2.1.1": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new EML211(context.attributes); + break; - memberModel.fetch(); - memberModel.once("sync", - function(oldModel){ + case "https://eml.ecoinformatics.org/eml-2.2.0": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new EML211(context.attributes); + break; - //Get the right model type based on the model values - var newModel = collection.getMember(oldModel); + case "-//ecoinformatics.org//eml-access-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - //If the model type has changed, then mark the model as unsynced, since there may be custom fetch() options for the new model - if(oldModel.type != newModel.type){ + case "-//ecoinformatics.org//eml-access-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - // DataPackages shouldn't be fetched until we support nested packages better in the UI - if(newModel.type == "DataPackage"){ - //Trigger a replace event so other parts of the app know when a model has been replaced with a different type - oldModel.trigger("replace", newModel); - } - else{ - newModel.set("synced", false); + case "-//ecoinformatics.org//eml-attribute-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - newModel.fetch(); - newModel.once("sync", function(fetchedModel){ - fetchedModel.set("synced", true); + case "-//ecoinformatics.org//eml-attribute-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - //Remove the model from the collection and add it back - collection.remove(oldModel); - collection.add(fetchedModel); + case "-//ecoinformatics.org//eml-constraint-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - //Trigger a replace event so other parts of the app know when a model has been replaced with a different type - oldModel.trigger("replace", newModel); + case "-//ecoinformatics.org//eml-constraint-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - if(newModel.type == "EML") - collection.trigger("add:EML"); - }); - } - } - else{ - newModel.set("synced", true); - collection.add(newModel, { merge: true }); + case "-//ecoinformatics.org//eml-coverage-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - if(newModel.type == "EML") - collection.trigger("add:EML"); - } - }); + case "-//ecoinformatics.org//eml-coverage-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - }, this); - } + case "-//ecoinformatics.org//eml-dataset-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - } catch (error) { - console.log(error); - } + case "-//ecoinformatics.org//eml-dataset-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - return models; - }, + case "-//ecoinformatics.org//eml-distribution-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - /* 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: function() { - - try { - /* Now run the SPARQL queries for the provenance relationships */ - var provQueries = []; - /* result: pidValue, wasDerivedFromValue (prov_wasDerivedFrom) */ - provQueries["prov_wasDerivedFrom"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \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"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \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"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \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"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \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"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \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"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \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"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \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"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \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"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \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"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \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"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \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"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \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"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \n"+ - "PREFIX 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"] = " \n"+ - "PREFIX rdfs: \n"+ - "PREFIX owl: \n"+ - "PREFIX prov: \n"+ - "PREFIX provone: \n"+ - "PREFIX ore: \n"+ - "PREFIX dcterms: \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 - var 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 (var iquery = 0; iquery < keys.length; iquery++) { + case "-//ecoinformatics.org//eml-distribution-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - var eq = rdf.SPARQLToQuery(provQueries[keys[iquery]], false, this.dataPackageGraph); - this.dataPackageGraph.query(eq, this.onResult, this.url(), this.onDone); + 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; - } catch (error) { + case "-//ecoinformatics.org//eml-literature-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - console.log(error); + 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; - // The return values have to be extracted from the result. - getValue: function(result, name) { - var res = result[name]; - // The result is of type 'NamedNode', just return the string value - if (res) { - return res.value; - } else - return " "; - }, + case "-//ecoinformatics.org//eml-party-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - /* 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. + case "-//ecoinformatics.org//eml-physical-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - 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: function(result) { - - var currentPid = this.getValue(result, "?pid"); - var resval; - var provFieldResult; - var provFieldValues; - - // 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 !== " ") { - var currentMember = null; - var provFieldValues; - var fieldName = null; - var vals = []; - var resultMember = null; - currentMember = this.find(function(model) { return 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 (var iFld = 0; iFld < this.provFields.length; iFld++) { - 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. - 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(function(model) { return model.get('id') === provFieldResult}); - - if (typeof resultMember !== 'undefined') { // If this prov field is a 'source' field, add it to 'sources' - - if(currentMember.isSourceField(fieldName)) { - // Get the package member that the id of the prov field is associated with - if (_.findWhere(this.sources, {id: provFieldResult}) == null) { - this.sources.push(resultMember); - } - // Only add the result member if it has not already been added. - if (_.findWhere(currentMember.get("provSources"), {id: provFieldResult}) == null) { - vals = currentMember.get("provSources"); - vals.push(resultMember); - currentMember.set("provSources", vals); - } - } - else if (currentMember.isDerivationField(fieldName)) { - // If this prov field is a 'derivation' field, add it to 'derivations' - if (_.findWhere(this.derivations, {id: provFieldResult}) == null) { - this.derivations.push(resultMember); - } - - if (_.findWhere(currentMember.get("provDerivations"), {id: provFieldResult}) == null) { - 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); - } + case "-//ecoinformatics.org//eml-physical-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - //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); - } - } + case "-//ecoinformatics.org//eml-project-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - } - } - } - }, - - /* This callback is called when all queries have finished. */ - onDone: function() { - if(this.queriesToRun > 1) { - this.queriesToRun--; - } else { - // Signal that all prov queries have finished - this.provenanceFlag = "complete"; - this.trigger("queryComplete"); - } - }, + case "-//ecoinformatics.org//eml-project-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - /* - * Use the DataONEObject parseSysMeta() function - */ - parseSysMeta: function(){ - return DataONEObject.parseSysMeta.call(this, arguments[0]); - }, - - /** - * 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: function(options){ + case "-//ecoinformatics.org//eml-protocol-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - if(!options) var options = {}; + case "-//ecoinformatics.org//eml-protocol-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - this.packageModel.set("uploadStatus", "p"); + case "-//ecoinformatics.org//eml-resource-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - //Get the system metadata first if we haven't retrieved it yet - if(!this.packageModel.get("sysMetaXML")){ - var collection = this; - this.packageModel.fetch({ - success: function(){ - 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 - var metadataModels = this.where({ type: "Metadata" }); - var dataModels = _.difference(this.models, metadataModels); - var sortedModels = _.union(metadataModels, dataModels); - var modelsInProgress = _.filter(sortedModels, function(m){ - return (m.get("uploadStatus") == "p" || m.get("sysMetaUploadStatus") == "p"); - }); - var modelsToBeSaved = _.filter(sortedModels, function(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 - return ( - (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. - var sysMetaToUpdate = _.reject(dataModels, function(m){ - // Find models that don't have any content changes to save, - // and whose system metadata is not already saving - return ( - ( !m.hasUpdates() || - m.get("hasContentChanges") || - m.get("sysMetaUploadStatus") == "p" || - m.get("sysMetaUploadStatus") == "c" || - m.get("sysMetaUploadStatus") == "e" - ) - ); - }); + case "-//ecoinformatics.org//eml-resource-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - //First quickly validate all the models before attempting to save any - var allValid = _.every(modelsToBeSaved, function(m) { - if( m.isValid() ){ - m.trigger("valid"); - return true; - } - else{ - return false; - } - }); + case "-//ecoinformatics.org//eml-software-2.0.0beta4//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - // If at least once model to be saved is invalid, - // or the metadata failed to save, cancel the save. - if ( !allValid || _.contains(_.map(metadataModels, function(model) { - return model.get("uploadStatus"); - } ), "e") ) { + case "-//ecoinformatics.org//eml-software-2.0.0beta6//EN": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - this.packageModel.set("changed", false); - this.packageModel.set("uploadStatus", "q"); - this.trigger("cancelSave"); - return; + 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; - //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 - var mapXML = this.serialize(); - } - catch (serializationException) { + case "FGDC-STD-001.2-1999": + context.set({ type: "Metadata", sortOrder: 1 }); + memberModel = new ScienceMetadata(context.attributes); + break; - //If serialization failed, revert back to our old id - this.packageModel.resetID(); + 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; - //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; + default: + // For other data formats, keep just the DataONEObject sysmeta + context.set({ type: "Data", sortOrder: 2 }); + memberModel = context; } - } - //First save all the models of the collection, if needed - _.each(modelsToBeSaved, function(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++; - - }, this); - - //Save the system metadata of all the Data objects - _.each(sysMetaToUpdate, function(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++; - }, this); - - //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(); + 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 } - //Create the resource map XML - var mapXML = this.serialize(); - } - catch (serializationException) { + return memberModel; + }, - //If serialization failed, revert back to our old id - this.packageModel.resetID(); + triggerComplete: function (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; - //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 - var requestType; - if(this.packageModel.isNew()){ - requestType = "POST"; - } - else{ - requestType = "PUT"; - } - - //Create a FormData object to send data with the XHR - var 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{ - var 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 - var 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 - var 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 - var sysMetaXML = this.packageModel.serializeSysMeta(); - - //Send the system metadata - var 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); - - var collection = this; - var requestSettings = { - url: this.packageModel.isNew()? this.url() : this.url({ update: true }), - type: requestType, - cache: false, - contentType: false, - processData: false, - data: formData, - success: function(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" }), function(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, function(dataModel){ - dataModel.set("sysMetaUploadStatus", "c"); - }); - - }, - error: function(data){ - - //Reset the id back to its original state - collection.packageModel.resetID(); - - //Reset the upload status for all members - _.each(collection.where({ uploadStatus: "c" }), function(m){ - m.set("uploadStatus", m.defaults().uploadStatus); - }); - - //When there is no network connection (status == 0), there will be no response text - if( data.status == 408 || data.status == 0 ){ - var parsedResponse = "There was a network issue that prevented this file from uploading. " + - "Make sure you are connected to a reliable internet connection."; - } - else { - var 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 - ); + this.trigger("complete", this); + + return; } - } - $.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 - */ - getMember: function(context, args) { - var 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; - - }, - - triggerComplete: function(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 - var notSynced = this.reject(function(m){ - return (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; + //Check if the collection is done being retrieved + var notSynced = this.reject(function (m) { + return 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); - - }, + this.sort(); + this.trigger("complete", this); + }, - /* Accumulate edits that are made to the provenance relationships via the ProvChartView. these + /* 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. */ - recordProvEdit: function(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. - var editFound = _.find(this.provEdits, function(edit) { - return(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 - var editListSize = this.provEdits.length; - var oppositeOp = (operation == "delete") ? "add" : "delete"; - - this.provEdits = _.reject(this.provEdits, function(edit) { - var editOperation = edit[0]; - var editSubjectId = edit[1]; - var editPredicate = edit[2]; - var editObject = edit[3]; - if (editOperation == oppositeOp - && editSubjectId == subject - && editPredicate == predicate - && editObject == object) { + recordProvEdit: function (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. + var editFound = _.find(this.provEdits, function (edit) { + return ( + edit[0] == operation && + edit[1] == subject && + edit[2] == predicate && + edit[3] == object + ); + }); - return true; - } - }); - - // 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]); - } - } - }, - - // Return true if the prov edits list is not empty - provEditsPending: function() { - 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: function() { - var rdf = this.rdf; - var graph = this.dataPackageGraph; - - var provEdits = this.provEdits; - if(!provEdits.length) { - return; - } - var RDF = rdf.Namespace(this.namespaces.RDF), - PROV = rdf.Namespace(this.namespaces.PROV), - PROVONE = rdf.Namespace(this.namespaces.PROVONE), - DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), - CITO = rdf.Namespace(this.namespaces.CITO), - XSD = rdf.Namespace(this.namespaces.XSD); - - var cnResolveUrl = this.getCnURI(); - - /* Check if this package member had provenance relationships added - or deleted by the provenance editor functionality of the ProvChartView - */ - _.each(provEdits, function(edit) { - var operation, subject, predicate, object; - var provStatements; - operation = edit[0]; - subject = edit[1]; - predicate = edit[2]; - object = edit[3]; - - // 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. - var executionId, executionURI, executionNode; - var programId, programURI, programNode; - var dataId, dataURI, dataNode; - var derivedDataURI, derivedDataNode; - var lastRef = false; - //var graph = this.dataPackageGraph; - - //Create a node for the subject and object - var subjectNode = rdf.sym( this.getURIFromRDF(subject) ), - objectNode = rdf.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")); + if (typeof editFound != "undefined") { + return; } - break; - case "prov_generatedByProgram": - programId = object; - dataNode = subjectNode; - var removed = false; - 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 = rdf.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); - removed = 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 = rdf.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); + // 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 + var editListSize = this.provEdits.length; + var oppositeOp = operation == "delete" ? "add" : "delete"; + + this.provEdits = _.reject(this.provEdits, function (edit) { + var editOperation = edit[0]; + var editSubjectId = edit[1]; + var editPredicate = edit[2]; + var editObject = edit[3]; + if ( + editOperation == oppositeOp && + editSubjectId == subject && + editPredicate == predicate && + editObject == object + ) { + return true; + } + }); - graph.removeMatches(executionNode, PROV("used"), dataNode) - removed = 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": - var entityNode = subjectNode; - var classNode = PROVONE(object); - if(operation == "add") { - this.addToGraph(entityNode, RDF("type"), classNode); - } else { - // Make sure there are no other references to this - this.removeIfLastProvRef(entityNode, RDF("type"), classNode); + // 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, + ]); } - break; - default: - // Print error if predicate for prov edit not found. - } - }, this); + } + }, + + // Return true if the prov edits list is not empty + provEditsPending: function () { + 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: function () { + var rdf = this.rdf; + var graph = this.dataPackageGraph; + + var provEdits = this.provEdits; + if (!provEdits.length) { + return; + } + var RDF = rdf.Namespace(this.namespaces.RDF), + PROV = rdf.Namespace(this.namespaces.PROV), + PROVONE = rdf.Namespace(this.namespaces.PROVONE), + DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), + CITO = rdf.Namespace(this.namespaces.CITO), + XSD = rdf.Namespace(this.namespaces.XSD); - // 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 - }); + var cnResolveUrl = this.getCnURI(); + + /* Check if this package member had provenance relationships added + or deleted by the provenance editor functionality of the ProvChartView + */ + _.each( + provEdits, + function (edit) { + var operation, subject, predicate, object; + var provStatements; + operation = edit[0]; + subject = edit[1]; + predicate = edit[2]; + object = edit[3]; + + // 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. + var executionId, executionURI, executionNode; + var programId, programURI, programNode; + var dataId, dataURI, dataNode; + var derivedDataURI, derivedDataNode; + var lastRef = false; + //var graph = this.dataPackageGraph; + + //Create a node for the subject and object + var subjectNode = rdf.sym(this.getURIFromRDF(subject)), + objectNode = rdf.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; + var removed = false; + 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 = rdf.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 + ); + removed = + 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 = rdf.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 + ); + removed = + 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": + var entityNode = subjectNode; + var classNode = PROVONE(object); + if (operation == "add") { + this.addToGraph( + entityNode, + RDF("type"), + classNode + ); + } else { + // Make sure there are no other references to this + this.removeIfLastProvRef( + entityNode, + RDF("type"), + classNode + ); + } + break; + default: + // Print error if predicate for prov edit not found. + } + }, + this + ); - }, + // 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. */ - addToGraph: function(subject, predicate, object) { + addToGraph: function (subject, predicate, object) { var graph = this.dataPackageGraph; - var statements = graph.statementsMatching(subject, predicate, object); + var statements = graph.statementsMatching( + subject, + predicate, + object + ); - if(!statements.length) { + if (!statements.length) { graph.add(subject, predicate, object); } }, - /* Remove the statement fromn the RDF graph only if the subject of this + /* 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. */ - removeIfLastProvRef: function(subjectNode, predicateNode, objectNode) { - var graph = this.dataPackageGraph; - var stillUsed = false; - var PROV = rdf.Namespace(this.namespaces.PROV); - var PROVONE = rdf.Namespace(this.namespaces.PROVONE); - // PROV namespace value, used to identify PROV statements - var provStr = PROV("").value; - // PROVONE namespace value, used to identify PROVONE statements - var provoneStr = PROVONE("").value; - // Get the statements from the RDF graph that reference the subject of the - // statement to remove. - var statements = graph.statementsMatching(undefined, undefined, subjectNode); - - var found = _.find(statements, function(statement) { - if(statement.subject == subjectNode && - statement.predicate == predicateNode && - statement.object == objectNode) return false; - - var 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 - var statements = graph.statementsMatching(subjectNode, undefined, undefined); - - found = _.find(statements, function(statement) { - if(statement.subject == subjectNode && - statement.predicate == predicateNode && - statement.object == objectNode) return false; - var 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: function() { - if (!this.dataPackageGraph || !this.dataPackageGraph.statements) { - return; - } - - // Collect an array of statements to be removed - var toRemove = []; - - _.each(this.dataPackageGraph.statements, function(statement) { - if (statement.object.termType !== "BlankNode") { - return; - } - - // For this statement, look for other statments about it - var matches = 0; - - _.each(this.dataPackageGraph.statements, function(other) { - if ( - other.subject.termType === "BlankNode" && - other.subject.id === statement.object.id - ) { - matches += 1; - } - }); + removeIfLastProvRef: function ( + subjectNode, + predicateNode, + objectNode + ) { + var graph = this.dataPackageGraph; + var stillUsed = false; + var PROV = rdf.Namespace(this.namespaces.PROV); + var PROVONE = rdf.Namespace(this.namespaces.PROVONE); + // PROV namespace value, used to identify PROV statements + var provStr = PROV("").value; + // PROVONE namespace value, used to identify PROVONE statements + var provoneStr = PROVONE("").value; + // Get the statements from the RDF graph that reference the subject of the + // statement to remove. + var statements = graph.statementsMatching( + undefined, + undefined, + subjectNode + ); + + var found = _.find( + statements, + function (statement) { + if ( + statement.subject == subjectNode && + statement.predicate == predicateNode && + statement.object == objectNode + ) + return false; + + var 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 + var statements = graph.statementsMatching( + subjectNode, + undefined, + undefined + ); + + found = _.find( + statements, + function (statement) { + if ( + statement.subject == subjectNode && + statement.predicate == predicateNode && + statement.object == objectNode + ) + return false; + var 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: function () { + if ( + !this.dataPackageGraph || + !this.dataPackageGraph.statements + ) { + return; + } - // If none are found, add it to our list - if (matches === 0) { - toRemove.push(statement); - } + // Collect an array of statements to be removed + var toRemove = []; + + _.each( + this.dataPackageGraph.statements, + function (statement) { + if (statement.object.termType !== "BlankNode") { + return; + } - }, this); + // For this statement, look for other statments about it + var matches = 0; + + _.each( + this.dataPackageGraph.statements, + function (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 - _.each(toRemove, function(statement) { - this.dataPackageGraph.removeStatement(statement); - }, this); - }, + // Remove collected statements + _.each( + toRemove, + function (statement) { + this.dataPackageGraph.removeStatement(statement); + }, + this + ); + }, - /* Get the execution identifier that is associated with a program id. + /* 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. */ - getExecutionId: function(programId) { - var rdf = this.rdf; - var graph = this.dataPackageGraph; - var stmts = null; - var cnResolveUrl = this.getCnURI(); - var RDF = rdf.Namespace(this.namespaces.RDF), - DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), - PROV = rdf.Namespace(this.namespaces.PROV), - PROVONE = rdf.Namespace(this.namespaces.PROVONE); - - var member = this.get(programId); - var executionId = member.get("prov_wasExecutedByExecution"); - if(executionId.length > 0) { - return(executionId[0]); - } else { - var programNode = rdf.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; - var 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) - } - }, + getExecutionId: function (programId) { + var rdf = this.rdf; + var graph = this.dataPackageGraph; + var stmts = null; + var cnResolveUrl = this.getCnURI(); + var RDF = rdf.Namespace(this.namespaces.RDF), + DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), + PROV = rdf.Namespace(this.namespaces.PROV), + PROVONE = rdf.Namespace(this.namespaces.PROVONE); + + var member = this.get(programId); + var executionId = member.get("prov_wasExecutedByExecution"); + if (executionId.length > 0) { + return executionId[0]; + } else { + var programNode = rdf.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; + var 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. + /* 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. */ - getExecutionNode: function(executionId) { - var rdf = this.rdf; - var graph = this.dataPackageGraph; - var stmts = null; - var testNode = null; - var cnResolveUrl = this.getCnURI(); - - // First see if the execution exists in the RDF graph as a 'bare' idenfier, i.e. - // a 'urn:uuid'. - stmts = graph.statementsMatching(rdf.sym(executionId), undefined, undefined); - if(typeof stmts == "undefined" || !stmts.length) { - // The execution node as urn was not found, look for fully qualified version. - testNode = rdf.sym(this.getURIFromRDF(executionId)); - stmts = graph.statementsMatching(rdf.sym(executionId), undefined, undefined); - if(typeof stmts == "undefined") { - // Couldn't find the execution, return the standard RDF node value - executionNode = rdf.sym(this.getURIFromRDF(executionId)); - return executionNode; + getExecutionNode: function (executionId) { + var rdf = this.rdf; + var graph = this.dataPackageGraph; + var stmts = null; + var testNode = null; + var cnResolveUrl = this.getCnURI(); + + // First see if the execution exists in the RDF graph as a 'bare' idenfier, i.e. + // a 'urn:uuid'. + stmts = graph.statementsMatching( + rdf.sym(executionId), + undefined, + undefined + ); + if (typeof stmts == "undefined" || !stmts.length) { + // The execution node as urn was not found, look for fully qualified version. + testNode = rdf.sym(this.getURIFromRDF(executionId)); + stmts = graph.statementsMatching( + rdf.sym(executionId), + undefined, + undefined + ); + if (typeof stmts == "undefined") { + // Couldn't find the execution, return the standard RDF node value + executionNode = rdf.sym( + this.getURIFromRDF(executionId) + ); + return executionNode; + } else { + return testNode; + } } else { - return testNode; + // The executionNode was found in the RDF graph as a urn + var executionNode = stmts[0].subject; + return executionNode; } - } else { - // The executionNode was found in the RDF graph as a urn - var executionNode = stmts[0].subject; - return executionNode; - } - }, - - addProgramToGraph: function(programId) { + }, + + addProgramToGraph: function (programId) { var rdf = this.rdf; var graph = this.dataPackageGraph; var RDF = rdf.Namespace(this.namespaces.RDF), - DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), - PROV = rdf.Namespace(this.namespaces.PROV), - PROVONE = rdf.Namespace(this.namespaces.PROVONE), - XSD = rdf.Namespace(this.namespaces.XSD); + DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), + PROV = rdf.Namespace(this.namespaces.PROV), + PROVONE = rdf.Namespace(this.namespaces.PROVONE), + XSD = rdf.Namespace(this.namespaces.XSD); var member = this.get(programId); var executionId = member.get("prov_wasExecutedByExecution"); var executionNode = null; @@ -2153,7 +2613,7 @@ define(['jquery', 'underscore', 'backbone', 'rdflib', "uuid", "md5", var associationNode = null; var cnResolveUrl = this.getCnURI(); - if(!executionId.length) { + 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]); @@ -2170,11 +2630,14 @@ define(['jquery', 'underscore', 'backbone', 'rdflib', "uuid", "md5", // 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. - var stmts = graph.statementsMatching(executionNode, - PROV("qualifiedAssociation"), undefined); + var 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) { + if (stmts.length) { associationNode = stmts[0].object; //associationId = stmts[0].object.value; } else { @@ -2186,11 +2649,31 @@ define(['jquery', 'underscore', 'backbone', 'rdflib', "uuid", "md5", //associationNode = graph.bnode(); programNode = rdf.sym(this.getURIFromRDF(programId)); try { - this.addToGraph(executionNode, PROV("qualifiedAssociation"), associationNode); - this.addToGraph(executionNode, RDF("type"), PROVONE("Execution")); - this.addToGraph(executionNode, DCTERMS("identifier"), rdf.literal(executionId, undefined, XSD("string"))); - this.addToGraph(associationNode, PROV("hadPlan"), programNode); - this.addToGraph(programNode, RDF("type"), PROVONE("Program")); + this.addToGraph( + executionNode, + PROV("qualifiedAssociation"), + associationNode + ); + this.addToGraph( + executionNode, + RDF("type"), + PROVONE("Execution") + ); + this.addToGraph( + executionNode, + DCTERMS("identifier"), + rdf.literal(executionId, undefined, XSD("string")) + ); + this.addToGraph( + associationNode, + PROV("hadPlan"), + programNode + ); + this.addToGraph( + programNode, + RDF("type"), + PROVONE("Program") + ); } catch (error) { console.log(error); } @@ -2200,24 +2683,24 @@ define(['jquery', 'underscore', 'backbone', 'rdflib', "uuid", "md5", // 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. - removeProgramFromGraph: function(programId) { + removeProgramFromGraph: function (programId) { var graph = this.dataPackageGraph; var rdf = this.rdf; var stmts = null; var cnResolveUrl = this.getCnURI(); var RDF = rdf.Namespace(this.namespaces.RDF), - DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), - PROV = rdf.Namespace(this.namespaces.PROV), - PROVONE = rdf.Namespace(this.namespaces.PROVONE), - XSD = rdf.Namespace(this.namespaces.XSD); + DCTERMS = rdf.Namespace(this.namespaces.DCTERMS), + PROV = rdf.Namespace(this.namespaces.PROV), + PROVONE = rdf.Namespace(this.namespaces.PROVONE), + XSD = rdf.Namespace(this.namespaces.XSD); var associationNode = null; var executionId = this.getExecutionId(programId); - if(executionId == null) return false; + if (executionId == null) return false; //var executionNode = rdf.sym(cnResolveUrl + encodeURIComponent(executionId)); var executionNode = this.getExecutionNode(executionId); - var programNode = rdf.sym( this.getURIFromRDF(programId) ); + var programNode = rdf.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). @@ -2227,23 +2710,56 @@ define(['jquery', 'underscore', 'backbone', 'rdflib', "uuid", "md5", 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); + 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); + 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); + stmts = graph.statementsMatching( + undefined, + PROV("hadPlan"), + programNode + ); associationNode = stmts[0].subject; } catch (error) { console.log(error); @@ -2251,662 +2767,1046 @@ define(['jquery', 'underscore', 'backbone', 'rdflib', "uuid", "md5", // 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"), rdf.literal(executionId, undefined, XSD("string"))); - graph.removeMatches(executionNode, PROV("qualifiedAssociation"), associationNode); + 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"), + rdf.literal(executionId, undefined, XSD("string")) + ); + graph.removeMatches( + executionNode, + PROV("qualifiedAssociation"), + associationNode + ); } catch (error) { console.log(error); } - return(true) + return true; }, /* * Serialize the DataPackage to OAI-ORE RDF XML */ - serialize: function() { - //Create an RDF serializer - var serializer = this.rdf.Serializer(), - oldPidVariations, - modifiedDate, - subjectClone, - predicateClone, - objectClone; - - serializer.store = this.dataPackageGraph; - - //Define the namespaces - var ORE = this.rdf.Namespace(this.namespaces.ORE), - CITO = this.rdf.Namespace(this.namespaces.CITO), - DC = this.rdf.Namespace(this.namespaces.DC), - DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), - FOAF = this.rdf.Namespace(this.namespaces.FOAF), - RDF = this.rdf.Namespace(this.namespaces.RDF), - XSD = this.rdf.Namespace(this.namespaces.XSD); - - //Get the pid of this package - depends on whether we are updating or creating a resource map - var pid = this.packageModel.get("id"), - oldPid = this.packageModel.get("oldPid"), - cnResolveUrl = this.getCnURI(); - - //Get a list of the model pids that should be aggregated by this package - var idsFromModel = []; - this.each(function(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 - var idsFromXML = []; - - var identifierStatements = this.dataPackageGraph.statementsMatching(undefined, DCTERMS("identifier"), undefined); - _.each(identifierStatements, function(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 - var 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 - var addedIds = _.without(_.difference(idsFromModel, idsFromXML), oldPidVariations); - - //Start an array to track all the member id variations - var allMemberIds = idsFromModel; - - //Add the ids with the CN Resolve URLs - _.each(idsFromModel, function(id){ - allMemberIds.push(cnResolveUrl + encodeURIComponent(id), cnResolveUrl + id, encodeURIComponent(id)); + serialize: function () { + //Create an RDF serializer + var serializer = this.rdf.Serializer(), + oldPidVariations, + modifiedDate, + subjectClone, + predicateClone, + objectClone; + + serializer.store = this.dataPackageGraph; + + //Define the namespaces + var ORE = this.rdf.Namespace(this.namespaces.ORE), + CITO = this.rdf.Namespace(this.namespaces.CITO), + DC = this.rdf.Namespace(this.namespaces.DC), + DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), + FOAF = this.rdf.Namespace(this.namespaces.FOAF), + RDF = this.rdf.Namespace(this.namespaces.RDF), + XSD = this.rdf.Namespace(this.namespaces.XSD); + + //Get the pid of this package - depends on whether we are updating or creating a resource map + var pid = this.packageModel.get("id"), + oldPid = this.packageModel.get("oldPid"), + cnResolveUrl = this.getCnURI(); + + //Get a list of the model pids that should be aggregated by this package + var idsFromModel = []; + this.each(function (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")); + } }); - //Find the identifier statement in the resource map - var idNode = this.rdf.lit(oldPid); - var idStatements = this.dataPackageGraph.statementsMatching(undefined, undefined, idNode); - - //Change all the resource map identifier literal node in the RDF graph - if ( idStatements.length ) { - - var idStatement = idStatements[0]; - - //Remove the identifier statement - try { - this.dataPackageGraph.remove(idStatement); - } catch (error) { - console.log(error); - } - - //Replace the id in the subject URI with the new id - var 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 - var rMapNode = this.rdf.sym(newRMapURI), - 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 - var aggByStatements = $.extend(true, [], - this.dataPackageGraph.statementsMatching(undefined, ORE("isAggregatedBy"))); - - // Remove any other isAggregatedBy statements that are not listed as members of this model - _.each(aggByStatements, function(statement) { - if( !_.contains(allMemberIds, statement.subject.value) ) { - this.removeFromAggregation(statement.subject.value); + 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 + var idsFromXML = []; + + var identifierStatements = + this.dataPackageGraph.statementsMatching( + undefined, + DCTERMS("identifier"), + undefined + ); + _.each( + identifierStatements, + function (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 + var childPackages = this.packageModel.get("childPackages"); + if (typeof childPackages == "object") { + idsFromModel = _.union( + idsFromModel, + Object.keys(childPackages) + ); } - }, this); - - // Change all the statements in the RDF where the aggregation is the subject, to reflect the new resource map ID - var aggregationNode; - _.each(oldPidVariations, function(oldPid){ - //Create a node for the old aggregation using this pid variation - aggregationNode = this.rdf.sym( oldPid + "#aggregation" ); - var aggregationLitNode = this.rdf.lit( oldPid + "#aggregation", "", XSD("anyURI") ); - - //Get all the triples where the old aggregation is the subject - var aggregationSubjStatements = _.union(this.dataPackageGraph.statementsMatching(aggregationNode), - this.dataPackageGraph.statementsMatching(aggregationLitNode)); - - if(aggregationSubjStatements.length) { - _.each(aggregationSubjStatements, function(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); - }, this); - - //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 - var aggregationObjStatements = _.union(this.dataPackageGraph.statementsMatching(undefined, undefined, aggregationNode), - this.dataPackageGraph.statementsMatching(undefined, undefined, aggregationLitNode)); - - if(aggregationObjStatements.length) { - _.each(aggregationObjStatements, function(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); - }, this); - - //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 - var rMapNode = this.rdf.sym( this.getURIFromRDF(oldPid) ); - var rMapStatements = $.extend(true, [], this.dataPackageGraph.statementsMatching(rMapNode)); - // then repopulate them with correct values - _.each(rMapStatements, function(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(); - } + //Find the difference between the model IDs and the XML IDs to get a list of added members + var addedIds = _.without( + _.difference(idsFromModel, idsFromXML), + oldPidVariations + ); + + //Start an array to track all the member id variations + var allMemberIds = idsFromModel; + + //Add the ids with the CN Resolve URLs + _.each(idsFromModel, function (id) { + allMemberIds.push( + cnResolveUrl + encodeURIComponent(id), + cnResolveUrl + id, + encodeURIComponent(id) + ); + }); - //Update the subject to the new pid - subjectClone.value = this.getURIFromRDF(pid); + //Find the identifier statement in the resource map + var idNode = this.rdf.lit(oldPid); + var idStatements = this.dataPackageGraph.statementsMatching( + undefined, + undefined, + idNode + ); + + //Change all the resource map identifier literal node in the RDF graph + if (idStatements.length) { + var idStatement = idStatements[0]; + + //Remove the identifier statement + try { + this.dataPackageGraph.remove(idStatement); + } catch (error) { + console.log(error); + } - //Remove the old resource map statement - this.dataPackageGraph.remove(statement); + //Replace the id in the subject URI with the new id + var 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) + ); + } - //Add the statement with the new subject pid, but the same predicate and object - this.dataPackageGraph.add(subjectClone, predicateClone, objectClone); + //Create resource map nodes for the subject and object + var rMapNode = this.rdf.sym(newRMapURI), + rMapIdNode = this.rdf.lit(pid); + //Add the triple for the resource map id + this.dataPackageGraph.add( + rMapNode, + DCTERMS("identifier"), + rMapIdNode + ); + } - }, this); + //Get all the isAggregatedBy statements + var aggByStatements = $.extend( + true, + [], + this.dataPackageGraph.statementsMatching( + undefined, + ORE("isAggregatedBy") + ) + ); + + // Remove any other isAggregatedBy statements that are not listed as members of this model + _.each( + aggByStatements, + function (statement) { + if ( + !_.contains( + allMemberIds, + statement.subject.value + ) + ) { + this.removeFromAggregation( + statement.subject.value + ); + } + }, + this + ); + + // Change all the statements in the RDF where the aggregation is the subject, to reflect the new resource map ID + var aggregationNode; + _.each( + oldPidVariations, + function (oldPid) { + //Create a node for the old aggregation using this pid variation + aggregationNode = this.rdf.sym( + oldPid + "#aggregation" + ); + var aggregationLitNode = this.rdf.lit( + oldPid + "#aggregation", + "", + XSD("anyURI") + ); + + //Get all the triples where the old aggregation is the subject + var aggregationSubjStatements = _.union( + this.dataPackageGraph.statementsMatching( + aggregationNode + ), + this.dataPackageGraph.statementsMatching( + aggregationLitNode + ) + ); + + if (aggregationSubjStatements.length) { + _.each( + aggregationSubjStatements, + function (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 + ); + }, + this + ); + + //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 + var aggregationObjStatements = _.union( + this.dataPackageGraph.statementsMatching( + undefined, + undefined, + aggregationNode + ), + this.dataPackageGraph.statementsMatching( + undefined, + undefined, + aggregationLitNode + ) + ); + + if (aggregationObjStatements.length) { + _.each( + aggregationObjStatements, + function (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 + ); + }, + this + ); + + //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 + var rMapNode = this.rdf.sym( + this.getURIFromRDF(oldPid) + ); + var rMapStatements = $.extend( + true, + [], + this.dataPackageGraph.statementsMatching( + rMapNode + ) + ); + + // then repopulate them with correct values + _.each( + rMapStatements, + function (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 + ); + }, + this + ); + }, + this + ); + + // 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 + _.each( + addedIds, + function (id) { + this.addToAggregation(id); + }, + this + ); + } else { + // Create the OAI-ORE graph from scratch + this.dataPackageGraph = this.rdf.graph(); + cnResolveUrl = this.getCnURI(); + + //Create a resource map node + var rMapNode = this.rdf.sym( + this.getURIFromRDF(this.packageModel.id) + ); + //Create an aggregation node + var aggregationNode = this.rdf.sym( + this.getURIFromRDF(this.packageModel.id) + + "#aggregation" + ); + + // Describe the resource map with a Creator + var creatorNode = this.rdf.blankNode(); + var 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 + ); + var 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 + _.each( + idsFromModel, + function (id) { + this.addToAggregation(id); + }, + this + ); + } - }, this); + // 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(); - // 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") + var xmlString = serializer.statementsToXML( + this.dataPackageGraph.statements ); - this.dataPackageGraph.add( - this.rdf.sym(this.getURIFromRDF(pid) + "#aggregation"), - ORE("isDescribedBy"), - this.rdf.sym(this.getURIFromRDF(pid)) - ); - - //Add nodes for new package members - _.each(addedIds, function(id) { - this.addToAggregation(id); - }, this); - - } - else { - - // Create the OAI-ORE graph from scratch - this.dataPackageGraph = this.rdf.graph(); - cnResolveUrl = this.getCnURI(); - - //Create a resource map node - var rMapNode = this.rdf.sym(this.getURIFromRDF(this.packageModel.id)); - //Create an aggregation node - var aggregationNode = this.rdf.sym(this.getURIFromRDF(this.packageModel.id) + "#aggregation"); - - // Describe the resource map with a Creator - var creatorNode = this.rdf.blankNode(); - var 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); - var 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 - _.each(idsFromModel, function(id) { - this.addToAggregation(id); - }, this); - } - - // 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(); - - var xmlString = serializer.statementsToXML(this.dataPackageGraph.statements); - - return xmlString; + return xmlString; }, // Clone an rdflib.js Node by creaing a new node based on the // original node RDF term type and data type. - cloneNode: function(nodeToClone) { - switch(nodeToClone.termType) { + cloneNode: function (nodeToClone) { + switch (nodeToClone.termType) { case "NamedNode": - return(this.rdf.sym(nodeToClone.value)); + return this.rdf.sym(nodeToClone.value); break; 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)); + if (typeof nodeToClone.datatype !== "undefined") { + return this.rdf.literal( + nodeToClone.value, + undefined, + nodeToClone.datatype + ); } else { - return(this.rdf.literal(nodeToClone.value)); + return this.rdf.literal(nodeToClone.value); } break; case "BlankNode": //Blank nodes don't need to be cloned - return nodeToClone;//(this.rdf.blankNode(nodeToClone.value)); + return nodeToClone; //(this.rdf.blankNode(nodeToClone.value)); break; case "Collection": // TODO: construct a list of nodes for this term type. - return(this.rdf.list(nodeToClone.value)); + return this.rdf.list(nodeToClone.value); break; default: - console.log("ERROR: unknown node type to clone: " + nodeToClone.termType); + console.log( + "ERROR: unknown node type to clone: " + + nodeToClone.termType + ); } }, // Adds a new object to the resource map RDF graph - addToAggregation: function(id){ - - // Initialize the namespaces - var ORE = this.rdf.Namespace(this.namespaces.ORE), - DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), - XSD = this.rdf.Namespace(this.namespaces.XSD), - CITO = this.rdf.Namespace(this.namespaces.CITO); - - // Create a node for this object, the identifier, the resource map, and the aggregation - var objectNode = this.rdf.sym(this.getURIFromRDF(id)), - rMapURI = this.getURIFromRDF(this.packageModel.get("id")), - mapNode = this.rdf.sym(rMapURI), - aggNode = this.rdf.sym(rMapURI + "#aggregation"), - idNode = this.rdf.literal(id, undefined, XSD("string")), - idStatements = [], - aggStatements = [], - aggByStatements = [], - documentsStatements = [], - 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 - var model = this.findWhere({ "id" : id }), - isDocBy = model.get("isDocumentedBy"), - 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 - var metadataInPackage = _.compact(_.map(this.models, function(m){ if(m.get("formatType") == "METADATA") return m; })), - metadataInPackageIDs = _.each(metadataInPackage, function(m){ return m.get("id") }); - - // Find the metadata IDs that are in this package that also documents this data object - var 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 == 0 ){ - - for(var i=0; i < metadataInPackage.length; i++){ - - //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; - } - } - + addToAggregation: function (id) { + // Initialize the namespaces + var ORE = this.rdf.Namespace(this.namespaces.ORE), + DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), + XSD = this.rdf.Namespace(this.namespaces.XSD), + CITO = this.rdf.Namespace(this.namespaces.CITO); + + // Create a node for this object, the identifier, the resource map, and the aggregation + var objectNode = this.rdf.sym(this.getURIFromRDF(id)), + rMapURI = this.getURIFromRDF(this.packageModel.get("id")), + mapNode = this.rdf.sym(rMapURI), + aggNode = this.rdf.sym(rMapURI + "#aggregation"), + idNode = this.rdf.literal(id, undefined, XSD("string")), + idStatements = [], + aggStatements = [], + aggByStatements = [], + documentsStatements = [], + 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 + ); } - // For each metadata that documents this object, add a CITO:isDocumentedBy and CITO:documents statement - _.each(metadataIds, function(metaId){ - //Create the named nodes and statements - var dataNode = this.rdf.sym( this.getURIFromRDF(id) ), - metadataNode = this.rdf.sym( this.getURIFromRDF(metaId) ), - isDocByStatement = this.rdf.st(dataNode, CITO("isDocumentedBy"), metadataNode), - 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); - } - }, this); - } - - // If this object documents a data object - if(documents && documents.length){ - - // Create a literal node for it - var metadataNode = this.rdf.sym( this.getURIFromRDF(id) ); - - _.each(documents, function(dataID){ - - //Make sure the id is one that will be aggregated - if( _.contains(this.idsToAggregate, dataID) ){ - - //Find the identifier statement for this data object - var dataURI = this.getURIFromRDF(dataID); - - //Create a data node using the exact way the identifier URI is written - var dataNode = this.rdf.sym(dataURI); + // 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 + ); + } - //Get the statements for data isDocumentedBy metadata - isDocumentedByStatements = this.dataPackageGraph.statementsMatching(dataNode, CITO("isDocumentedBy"), metadataNode); + // 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 + ); + } - //If that statement is not in the RDF already... - if ( isDocumentedByStatements.length < 1 ) { - // Create a statement: This data is documented by this metadata - var isDocByStatement = this.rdf.st(dataNode, CITO("isDocumentedBy"), metadataNode); - //Add the "isDocumentedBy" statement - this.dataPackageGraph.add(isDocByStatement); - } + // Find the metadata doc that describes this object + var model = this.findWhere({ id: id }), + isDocBy = model.get("isDocumentedBy"), + documents = model.get("documents"); - //Get the statements for metadata documents data - documentsStatements = this.dataPackageGraph.statementsMatching(metadataNode, CITO("documents"), dataNode); + // 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 that statement is not in the RDF already... - if ( documentsStatements.length < 1 ) { - // Create a statement: This metadata documents data - var documentsStatement = this.rdf.st(metadataNode, CITO("documents"), dataNode); - //Add the "isDocumentedBy" statement - this.dataPackageGraph.add(documentsStatement); + // If this object is documented by any metadata... + if (isDocBy && isDocBy.length) { + // Get the ids of all the metadata objects in this package + var metadataInPackage = _.compact( + _.map(this.models, function (m) { + if (m.get("formatType") == "METADATA") return m; + }) + ), + metadataInPackageIDs = _.each( + metadataInPackage, + function (m) { + return m.get("id"); + } + ); + + // Find the metadata IDs that are in this package that also documents this data object + var 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 == 0) { + for (var i = 0; i < metadataInPackage.length; i++) { + //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; + } + } } - } - }, this); - } + // For each metadata that documents this object, add a CITO:isDocumentedBy and CITO:documents statement + _.each( + metadataIds, + function (metaId) { + //Create the named nodes and statements + var dataNode = this.rdf.sym(this.getURIFromRDF(id)), + metadataNode = this.rdf.sym( + this.getURIFromRDF(metaId) + ), + isDocByStatement = this.rdf.st( + dataNode, + CITO("isDocumentedBy"), + metadataNode + ), + 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); + } + }, + this + ); + } + // If this object documents a data object + if (documents && documents.length) { + // Create a literal node for it + var metadataNode = this.rdf.sym(this.getURIFromRDF(id)); + + _.each( + documents, + function (dataID) { + //Make sure the id is one that will be aggregated + if (_.contains(this.idsToAggregate, dataID)) { + //Find the identifier statement for this data object + var dataURI = this.getURIFromRDF(dataID); + + //Create a data node using the exact way the identifier URI is written + var 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 + var 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 + var documentsStatement = this.rdf.st( + metadataNode, + CITO("documents"), + dataNode + ); + //Add the "isDocumentedBy" statement + this.dataPackageGraph.add( + documentsStatement + ); + } + } + }, + this + ); + } }, /* * Removes an object from the aggregation in the RDF graph */ - removeFromAggregation: function(id){ - if ( id.indexOf(this.dataPackageGraph.cnResolveUrl) == -1 ) { + removeFromAggregation: function (id) { + if (id.indexOf(this.dataPackageGraph.cnResolveUrl) == -1) { id = this.getURIFromRDF(id); } - // Create a literal node for the removed object - var removedObjNode = this.rdf.sym(id), - // Get the statements from the RDF where the removed object is the subject or object - statements = $.extend(true, [], - _.union(this.dataPackageGraph.statementsMatching(undefined, undefined, removedObjNode), - this.dataPackageGraph.statementsMatching(removedObjNode))); - - // Remove all the statements mentioning this object + // Create a literal node for the removed object + var removedObjNode = this.rdf.sym(id), + // Get the statements from the RDF where the removed object is the subject or object + 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) { console.log(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 - * @return {string} - The full URI for the given id as it exists in the RDF. - */ - getURIFromRDF: function(id){ - - //Exit if no id was given - if( !id ) - return ""; - - //Create a literal node with the identifier as the value - var XSD = this.rdf.Namespace(this.namespaces.XSD), - DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), - idNode = this.rdf.literal(id, undefined, XSD("string")), - //Find the identifier statements for the given id - 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; - } - else{ - return this.getCnURI() + encodeURIComponent(id); - } + * 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 + * @return {string} - The full URI for the given id as it exists in the RDF. + */ + getURIFromRDF: function (id) { + //Exit if no id was given + if (!id) return ""; + + //Create a literal node with the identifier as the value + var XSD = this.rdf.Namespace(this.namespaces.XSD), + DCTERMS = this.rdf.Namespace(this.namespaces.DCTERMS), + idNode = this.rdf.literal(id, undefined, XSD("string")), + //Find the identifier statements for the given id + 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; + } else { + 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. - * - * @return {string} - The CN resolve URL - */ - getCnURI: function(){ - - //If the CN resolve URL was already found, return it - if( this.dataPackageGraph.cnResolveUrl ){ - return this.dataPackageGraph.cnResolveUrl; - } - else if( this.packageModel.get("oldPid") ){ - - //Find the identifier statement for the resource map in the RDF graph - var idNode = this.rdf.lit( this.packageModel.get("oldPid") ), - idStatements = this.dataPackageGraph.statementsMatching(undefined, undefined, idNode), - 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"); + * Parses out the CN Resolve URL from the existing statements in the RDF + * or if not found in the RDF, from the app configuration. + * + * @return {string} - The CN resolve URL + */ + getCnURI: function () { + //If the CN resolve URL was already found, return it + if (this.dataPackageGraph.cnResolveUrl) { + return this.dataPackageGraph.cnResolveUrl; + } else if (this.packageModel.get("oldPid")) { + //Find the identifier statement for the resource map in the RDF graph + var idNode = this.rdf.lit(this.packageModel.get("oldPid")), + idStatements = this.dataPackageGraph.statementsMatching( + undefined, + undefined, + idNode + ), + 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"); } - } - else{ - this.dataPackageGraph.cnResolveUrl = MetacatUI.appModel.get("resolveServiceUrl"); - } - - //Return the CN resolve URL - return this.dataPackageGraph.cnResolveUrl; + //Return the CN resolve URL + return this.dataPackageGraph.cnResolveUrl; }, /** * Checks if this resource map has had any changes that requires an update */ - needsUpdate: function(){ - //Check for changes to the list of aggregated members - var ids = this.pluck("id"); - if(this.originalMembers.length != ids.length || _.intersection(this.originalMembers, ids).length != ids.length) - return true; + needsUpdate: function () { + //Check for changes to the list of aggregated members + var 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 - var isDifferent = false, - i = 0; - - //Keep going until we find a difference - while(!isDifferent && i 0){ - currentCollections.push(this); - model.set("collections", _.uniq(currentCollections)); - } - else - model.set("collections", [this]); + saveReference: function (model) { + //Save a reference to this collection in the model + var currentCollections = model.get("collections"); + if (currentCollections.length > 0) { + currentCollections.push(this); + model.set("collections", _.uniq(currentCollections)); + } else model.set("collections", [this]); }, /** @@ -3063,68 +3974,126 @@ define(['jquery', 'underscore', 'backbone', 'rdflib', "uuid", "md5", * @param {AccessPolicy} accessPolicy - The accessPolicy to * broadcast */ - broadcastAccessPolicy: function(accessPolicy) { - if (!accessPolicy) { - return; - } + broadcastAccessPolicy: function (accessPolicy) { + if (!accessPolicy) { + return; + } - var policy = _.clone(accessPolicy); - this.packageModel.set("accessPolicy", policy); + var 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; - } + // 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", function (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 + var 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(); + }, - this.packageModel.on("sysMetaUpdateError", function(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 - var message = "There was an error sharing your dataset. Not all of your changes were applied."; + /** + * 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: function (dataONEObject) { + //Set the number of loading files and the isLoadingFiles flag + let numLoadingFiles = + this.where({ uploadStatus: "l" }).length + + this.where({ uploadStatus: "p" }).length; + this.packageModel.set({ + isLoadingFiles: numLoadingFiles > 0, + numLoadingFiles: numLoadingFiles, + }); - // TODO: Is this really the right way to hook into the editor's - // error notification mechanism? - MetacatUI.appView.eml211EditorView.saveError(message); - }); + if (dataONEObject) { + //Listen to the upload status to update the flag + this.listenTo( + dataONEObject, + "change:uploadStatus", + function () { + //If the object is done being successfully saved + if (dataONEObject.get("uploadStatus") == "c") { + let numLoadingFiles = + 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 (!numLoadingFiles) { + this.packageModel.set({ + isLoadingFiles: false, + numLoadingFiles: numLoadingFiles, + }); + } else { + this.packageModel.set( + "numLoadingFiles", + numLoadingFiles + ); + } + } + } + ); + } + }, - this.packageModel.updateSysMeta(); + /** + * Returns atLocation information found in this resourceMap + * for all the PIDs in this resourceMap + * @returns object with PIDs as key and atLocation paths as values + * @since x.x.x + */ + getAtLocation: function () { + return this.atLocationObject; }, /** - * 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: function(dataONEObject){ - //Set the number of loading files and the isLoadingFiles flag - let numLoadingFiles = this.where({ uploadStatus: "l" }).length + this.where({ uploadStatus: "p" }).length; - this.packageModel.set({"isLoadingFiles": numLoadingFiles > 0, "numLoadingFiles": numLoadingFiles}); - - if( dataONEObject ){ - //Listen to the upload status to update the flag - this.listenTo(dataONEObject, "change:uploadStatus", function(){ - //If the object is done being successfully saved - if( dataONEObject.get("uploadStatus") == "c" ){ - let numLoadingFiles = 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( !numLoadingFiles ){ - this.packageModel.set({"isLoadingFiles": false, "numLoadingFiles": numLoadingFiles}); - } - else{ - this.packageModel.set("numLoadingFiles", numLoadingFiles); - } + * 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 x.x.x + */ + 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; - } -); + return DataPackage; +}); diff --git a/src/js/models/AppModel.js b/src/js/models/AppModel.js index 8701b1f18..d3c1604cb 100644 --- a/src/js/models/AppModel.js +++ b/src/js/models/AppModel.js @@ -91,7 +91,7 @@ define(['jquery', 'underscore', 'backbone'], * DataCatalog). This can be set to either "google" (the default), or "cesium". To * use Google maps, the {@link AppConfig#googleAnalyticsKey} must be set. To use * Cesium maps, the {@link AppConfig#enableCesium} property must be set to true, and - * the {@link AppConfig#cesiumToken} must be set. DEPRECATION NOTE: This configuration + * the {@link AppConfig#cesiumToken} must be set. DEPRECATION NOTE: This configuration * is deprecated along with the {@link DataCatalogView} and {@link DataCatalogViewWithFilters} * views and Google Maps. The {@link CatalogSearchView} will replace these as the primary search view and will only * support Cesium, not Google Maps. @@ -103,14 +103,25 @@ define(['jquery', 'underscore', 'backbone'], dataCatalogMap: "google", /** - * The default options for the Cesium map used in the - * {@link CatalogSearchView} for searching the data catalog. Add custom - * layers, a default home position (for example, zoom into your area of - * research), and enable/disable map widgets. See {@link MapConfig} for - * the full suite of options. Use any satellite imagery layer of your - * choice, such as a self-hosted imagery layer or hosted on Cesium Ion. If - * no layer of type `CesiumGeohash` is included, a geohash layer will be - * added automatically in order to show the search results on the map. + * Set this option to true to display the filtering button for data package table + * @type {boolean} + * @since x.x.x + */ + dataPackageFiltering: false, + + /** + * Set this option to true to display the sorting button for data package table + * @type {boolean} + * @since x.x.x + */ + dataPackageSorting: false, + + /** + * The default options for the Cesium map used in the {@link CatalogSearchView} for searching the data + * catalog. Add custom layers, a default home position (for example, zoom into your area of research), + * and enable/disable map widgets. See {@link MapConfig} for the full suite of options. Keep the `CesiumGeohash` + * layer here in order to show the search results in the map as geohash boxes. Use any satellite imagery + * layer of your choice, such as a self-hosted imagery layer or hosted on Cesium Ion. * @type {MapConfig} * @since 2.22.0 */ @@ -359,25 +370,25 @@ define(['jquery', 'underscore', 'backbone'], contact: true }, - /** - * A list of required fields for each EMLParty (People) in the dataset editor. + /** + * A list of required fields for each EMLParty (People) in the dataset editor. * This is a literal object where the keys are the EML Party type (e.g. creator, principalInvestigator) {@link see EMLParty.partytypes} * and the values are arrays of field names. * By default, EMLPartys are *always* required to have an individual's name, position name, or organization name. * @type {object} * @since 2.21.0 - * @example + * @example * { * contact: ["email"], * creator: ["email", "address", "phone"] * principalInvestigator: ["organizationName"] * } - * @default + * @default * { * } */ emlEditorRequiredFields_EMLParty: { - + }, /** @@ -526,7 +537,7 @@ define(['jquery', 'underscore', 'backbone'], * @since 2.19.0 */ customEMLMethods: [], - + /** * Configuration options for a drop down list of taxa. * @typedef {object} AppConfig#quickAddTaxaList @@ -1696,14 +1707,14 @@ define(['jquery', 'underscore', 'backbone'], * @since 2.17.0 */ queryIdentifierFields: ["id", "identifier", "seriesId", "isPartOf"], - + /** * The name of the query fields that specify latitude. Filter models that these * fields are handled specially, since they must be a float value and have a * pre-determined minRange and maxRange (-90 to 90). */ queryLatitudeFields: ["northBoundCoord", "southBoundCoord"], - + /** * The name of the query fields that specify longitude. Filter models that these * fields are handled specially, since they must be a float value and have a @@ -1819,8 +1830,8 @@ define(['jquery', 'underscore', 'backbone'], } ], - /** - * The document fields to return when conducting a search. This is the list of fields returned by the main catalog search view. + /** + * The document fields to return when conducting a search. This is the list of fields returned by the main catalog search view. * @type {string[]} * @since 2.22.0 * @example ["id", "title", "obsoletedBy"] diff --git a/src/js/models/DataONEObject.js b/src/js/models/DataONEObject.js index 78d4cde85..be307c1fa 100644 --- a/src/js/models/DataONEObject.js +++ b/src/js/models/DataONEObject.js @@ -65,6 +65,7 @@ define(['jquery', 'underscore', 'backbone', 'uuid', 'he', 'collections/AccessPol latestVersion: null, isDocumentedBy: null, documents: [], + members: [], resourceMap: [], nodeLevel: 0, // Indicates hierarchy level in the view for indentation sortOrder: 2, // Metadata: 1, Data: 2, DataPackage: 3 @@ -260,6 +261,33 @@ define(['jquery', 'underscore', 'backbone', 'uuid', 'he', 'collections/AccessPol } }, + /** + * Create the URL string that is used to download this package + * @returns PackageURL string for this DataONE Object + * @since x.x.x + */ + getPackageURL: function(){ + var url = null; + + // With no id, we can't do anything + if( !this.get("id") && !this.get("seriesid") ) + return url; + + //If we haven't set a packageServiceURL upon app initialization and we are querying a CN, then the packageServiceURL is dependent on the MN this package is from + if((MetacatUI.appModel.get("d1Service").toLowerCase().indexOf("cn/") > -1) && MetacatUI.nodeModel.get("members").length){ + var source = this.get("datasource"), + node = _.find(MetacatUI.nodeModel.get("members"), {identifier: source}); + + //If this node has MNRead v2 services... + if(node && node.readv2) + url = node.baseURL + "/v2/packages/application%2Fbagit-097/" + encodeURIComponent(this.get("id")); + } + else if(MetacatUI.appModel.get("packageServiceUrl")) + url = MetacatUI.appModel.get("packageServiceUrl") + encodeURIComponent(this.get("id")); + + return url; + }, + /** * Overload Backbone.Model.fetch, so that we can set custom options for each fetch() request */ @@ -1192,6 +1220,61 @@ define(['jquery', 'underscore', 'backbone', 'uuid', 'he', 'collections/AccessPol }, + /** + * Looks up human readable format of the DataONE Object + * @returns format String + * @since x.x.x + */ + getFormat: function(){ + var formatMap = { + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" : "Microsoft Excel OpenXML", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" : "Microsoft Word OpenXML", + "application/vnd.ms-excel.sheet.binary.macroEnabled.12" : "Microsoft Office Excel 2007 binary workbooks", + "application/vnd.openxmlformats-officedocument.presentationml.presentation" : "Microsoft Office OpenXML Presentation", + "application/vnd.ms-excel" : "Microsoft Excel", + "application/msword" : "Microsoft Word", + "application/vnd.ms-powerpoint" : "Microsoft Powerpoint", + "text/html" : "HTML", + "text/plain": "plain text (.txt)", + "video/avi" : "Microsoft AVI file", + "video/x-ms-wmv" : "Windows Media Video (.wmv)", + "audio/x-ms-wma" : "Windows Media Audio (.wma)", + "application/vnd.google-earth.kml xml" : "Google Earth Keyhole Markup Language (KML)", + "http://docs.annotatorjs.org/en/v1.2.x/annotation-format.html" : "annotation", + "application/mathematica" : "Mathematica Notebook", + "application/postscript" : "Postscript", + "application/rtf" : "Rich Text Format (RTF)", + "application/xml" : "XML Application", + "text/xml" : "XML", + "application/x-fasta" : "FASTA sequence file", + "nexus/1997" : "NEXUS File Format for Systematic Information", + "anvl/erc-v02" : "Kernel Metadata and Electronic Resource Citations (ERCs), 2010.05.13", + "http://purl.org/dryad/terms/" : "Dryad Metadata Application Profile Version 3.0", + "http://datadryad.org/profile/v3.1" : "Dryad Metadata Application Profile Version 3.1", + "application/pdf" : "PDF", + "application/zip" : "ZIP file", + "http://www.w3.org/TR/rdf-syntax-grammar" : "RDF/XML", + "http://www.w3.org/TR/rdfa-syntax" : "RDFa", + "application/rdf xml" : "RDF", + "text/turtle" : "TURTLE", + "text/n3" : "N3", + "application/x-gzip" : "GZIP Format", + "application/x-python" : "Python script", + "http://www.w3.org/2005/Atom" : "ATOM-1.0", + "application/octet-stream" : "octet stream (application file)", + "http://digir.net/schema/conceptual/darwin/2003/1.0/darwin2.xsd" : "Darwin Core, v2.0", + "http://rs.tdwg.org/dwc/xsd/simpledarwincore/" : "Simple Darwin Core", + "eml://ecoinformatics.org/eml-2.1.0" : "EML v2.1.0", + "eml://ecoinformatics.org/eml-2.1.1" : "EML v2.1.1", + "eml://ecoinformatics.org/eml-2.0.1" : "EML v2.0.1", + "eml://ecoinformatics.org/eml-2.0.0" : "EML v2.0.0", + "https://eml.ecoinformatics.org/eml-2.2.0" : "EML v2.2.0", + + } + + return formatMap[this.get("formatId")] || this.get("formatId"); + }, + /** * Build a fresh system metadata document for this object when it is new * Return it as a DOM object @@ -1737,34 +1820,37 @@ define(['jquery', 'underscore', 'backbone', 'uuid', 'he', 'collections/AccessPol }, /** - * Converts the number of bytes into a human readable format and updates the `sizeStr` attribute + * Converts the number of bytes into a human readable format and + * updates the `sizeStr` attribute + * @returns: None + * */ bytesToSize: function(){ - var kilobyte = 1024; - var megabyte = kilobyte * 1024; - var gigabyte = megabyte * 1024; - var terabyte = gigabyte * 1024; + var kibibyte = 1024; + var mebibyte = kibibyte * 1024; + var gibibyte = mebibyte * 1024; + var tebibyte = gibibyte * 1024; var precision = 0; var bytes = this.get("size"); - if ((bytes >= 0) && (bytes < kilobyte)) { + if ((bytes >= 0) && (bytes < kibibyte)) { this.set("sizeStr", bytes + ' B'); - } else if ((bytes >= kilobyte) && (bytes < megabyte)) { - this.set("sizeStr", (bytes / kilobyte).toFixed(precision) + ' KB'); + } else if ((bytes >= kibibyte) && (bytes < mebibyte)) { + this.set("sizeStr", (bytes / kibibyte).toFixed(precision) + ' KiB'); - } else if ((bytes >= megabyte) && (bytes < gigabyte)) { + } else if ((bytes >= mebibyte) && (bytes < gibibyte)) { precision = 2; - this.set("sizeStr", (bytes / megabyte).toFixed(precision) + ' MB'); + this.set("sizeStr", (bytes / mebibyte).toFixed(precision) + ' MiB'); - } else if ((bytes >= gigabyte) && (bytes < terabyte)) { + } else if ((bytes >= gibibyte) && (bytes < tebibyte)) { precision = 2; - this.set("sizeStr", (bytes / gigabyte).toFixed(precision) + ' GB'); + this.set("sizeStr", (bytes / gibibyte).toFixed(precision) + ' GiB'); - } else if (bytes >= terabyte) { + } else if (bytes >= tebibyte) { precision = 2; - this.set("sizeStr", (bytes / terabyte).toFixed(precision) + ' TB'); + this.set("sizeStr", (bytes / tebibyte).toFixed(precision) + ' TiB'); } else { this.set("sizeStr", bytes + ' B'); @@ -1772,6 +1858,100 @@ define(['jquery', 'underscore', 'backbone', 'uuid', 'he', 'collections/AccessPol } }, + /** + * This method will download this object while + * sending the user's auth token in the request. + * @returns None + * @since: x.x.x + */ + downloadWithCredentials: function(){ + //if(this.get("isPublic")) return; + + //Get info about this object + var url = this.get("url"), + model = this; + + //Create an XHR + var xhr = new XMLHttpRequest(); + + //Open and send the request with the user's auth token + xhr.open('GET', url); + + if(MetacatUI.appUserModel.get("loggedIn")) + xhr.withCredentials = true; + + //When the XHR is ready, create a link with the raw data (Blob) and click the link to download + xhr.onload = function(){ + + if( this.status == 404 ){ + this.onerror.call(this); + return; + } + + //Get the file name to save this file as + var filename = xhr.getResponseHeader('Content-Disposition'); + + if(!filename){ + filename = model.get("fileName") || model.get("title") || model.get("id") || "download"; + } + else + filename = filename.substring(filename.indexOf("filename=")+9).replace(/"/g, ""); + + //Replace any whitespaces + filename = filename.trim().replace(/ /g, "_"); + + //For IE, we need to use the navigator API + if (navigator && navigator.msSaveOrOpenBlob) { + navigator.msSaveOrOpenBlob(xhr.response, filename); + } + //Other browsers can download it via a link + else{ + var a = document.createElement('a'); + a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob + + // Set the file name. + a.download = filename + + a.style.display = 'none'; + document.body.appendChild(a); + a.click(); + a.remove(); + } + + model.trigger("downloadComplete"); + + // Track this event + MetacatUI.analytics?.trackEvent( + "download", + "Download DataONEObject", + model.get("id") + ); + }; + + xhr.onerror = function(e){ + model.trigger("downloadError"); + + // Track the error + MetacatUI.analytics?.trackException( + `Download DataONEObject error: ${e || ""}`, model.get("id"), true + ); + }; + + xhr.onprogress = function(e){ + if (e.lengthComputable){ + var percent = (e.loaded / e.total) * 100; + model.set("downloadPercent", percent); + } + }; + + xhr.responseType = "blob"; + + if(MetacatUI.appUserModel.get("loggedIn")) + xhr.setRequestHeader("Authorization", "Bearer " + MetacatUI.appUserModel.get("token")); + + xhr.send(); + }, + /** * Creates a file name for this DataONEObject and updates the `fileName` attribute */ diff --git a/src/js/models/PackageModel.js b/src/js/models/PackageModel.js index abe8501bd..0f8daf8b5 100644 --- a/src/js/models/PackageModel.js +++ b/src/js/models/PackageModel.js @@ -1269,10 +1269,11 @@ define(['jquery', 'underscore', 'backbone', 'uuid', 'md5', 'rdflib', 'models/Sol a.remove(); } - // Track this event - MetacatUI.analytics?.trackEvent("download", "Download Package", model.get("id")) + model.trigger("downloadComplete"); + + // Track this event + MetacatUI.analytics?.trackEvent("download", "Download Package", model.get("id")) - model.trigger("downloadComplete"); }; xhr.onprogress = function(e){ @@ -1446,27 +1447,27 @@ define(['jquery', 'underscore', 'backbone', 'uuid', 'md5', 'rdflib', 'models/Sol * @return string */ bytesToSize: function(bytes, precision){ - var kilobyte = 1024; - var megabyte = kilobyte * 1024; - var gigabyte = megabyte * 1024; - var terabyte = gigabyte * 1024; + var kibibyte = 1024; + var mebibyte = kibibyte * 1024; + var gibibyte = mebibyte * 1024; + var tebibyte = gibibyte * 1024; if(typeof bytes === "undefined") var bytes = this.get("size"); - if ((bytes >= 0) && (bytes < kilobyte)) { + if ((bytes >= 0) && (bytes < kibibyte)) { return bytes + ' B'; - } else if ((bytes >= kilobyte) && (bytes < megabyte)) { - return (bytes / kilobyte).toFixed(precision) + ' KB'; + } else if ((bytes >= kibibyte) && (bytes < mebibyte)) { + return (bytes / kibibyte).toFixed(precision) + ' KiB'; - } else if ((bytes >= megabyte) && (bytes < gigabyte)) { - return (bytes / megabyte).toFixed(precision) + ' MB'; + } else if ((bytes >= mebibyte) && (bytes < gibibyte)) { + return (bytes / mebibyte).toFixed(precision) + ' MiB'; - } else if ((bytes >= gigabyte) && (bytes < terabyte)) { - return (bytes / gigabyte).toFixed(precision) + ' GB'; + } else if ((bytes >= gibibyte) && (bytes < tebibyte)) { + return (bytes / gibibyte).toFixed(precision) + ' GiB'; - } else if (bytes >= terabyte) { - return (bytes / terabyte).toFixed(precision) + ' TB'; + } else if (bytes >= tebibyte) { + return (bytes / tebibyte).toFixed(precision) + ' TiB'; } else { return bytes + ' B'; diff --git a/src/js/models/SolrResult.js b/src/js/models/SolrResult.js index 2e51cd8fa..9dfa962c8 100644 --- a/src/js/models/SolrResult.js +++ b/src/js/models/SolrResult.js @@ -762,27 +762,27 @@ define(['jquery', 'underscore', 'backbone'], * @return string */ bytesToSize: function(bytes, precision){ - var kilobyte = 1024; - var megabyte = kilobyte * 1024; - var gigabyte = megabyte * 1024; - var terabyte = gigabyte * 1024; + var kibibyte = 1024; + var mebibyte = kibibyte * 1024; + var gibibyte = mebibyte * 1024; + var tebibyte = gibibyte * 1024; if(typeof bytes === "undefined") var bytes = this.get("size"); - if ((bytes >= 0) && (bytes < kilobyte)) { + if ((bytes >= 0) && (bytes < kibibyte)) { return bytes + ' B'; - } else if ((bytes >= kilobyte) && (bytes < megabyte)) { - return (bytes / kilobyte).toFixed(precision) + ' KB'; + } else if ((bytes >= kibibyte) && (bytes < mebibyte)) { + return (bytes / kibibyte).toFixed(precision) + ' KiB'; - } else if ((bytes >= megabyte) && (bytes < gigabyte)) { - return (bytes / megabyte).toFixed(precision) + ' MB'; + } else if ((bytes >= mebibyte) && (bytes < gibibyte)) { + return (bytes / mebibyte).toFixed(precision) + ' MiB'; - } else if ((bytes >= gigabyte) && (bytes < terabyte)) { - return (bytes / gigabyte).toFixed(precision) + ' GB'; + } else if ((bytes >= gibibyte) && (bytes < tebibyte)) { + return (bytes / gibibyte).toFixed(precision) + ' GiB'; - } else if (bytes >= terabyte) { - return (bytes / terabyte).toFixed(precision) + ' TB'; + } else if (bytes >= tebibyte) { + return (bytes / tebibyte).toFixed(precision) + ' TiB'; } else { return bytes + ' B'; diff --git a/src/js/routers/router.js b/src/js/routers/router.js index 734999507..c9688dc70 100644 --- a/src/js/routers/router.js +++ b/src/js/routers/router.js @@ -444,6 +444,14 @@ function ($, _, Backbone) { MetacatUI.appView.metadataView.seriesId = seriesId; MetacatUI.appView.metadataView.pid = pid; + // MetacatUI resets the dataPackage and dataPackageSynced + // attributes before rendering the view. These attributes are + // initialized on a per-dataset basis to prevent displaying the + // same dataset repeatedly. + + MetacatUI.appView.metadataView.dataPackage = null; + MetacatUI.appView.metadataView.dataPackageSynced = false; + MetacatUI.appView.showView(MetacatUI.appView.metadataView); } }, diff --git a/src/js/templates/dataItem.html b/src/js/templates/dataItem.html index 47cd734d6..d03578c5c 100644 --- a/src/js/templates/dataItem.html +++ b/src/js/templates/dataItem.html @@ -1,10 +1,25 @@ -<% if ( typeof type !== "undefined" && type !== null && type == "Metadata" ) { %> +<% if ( viewType === "edit" && typeof type !== "undefined" && type !== null && type == "Metadata" ) { %> <% var indentFolder = nodeLevel * 40; %> +<% } else if (viewType === "view" && icon !== "undefined") { %> + <% if ( type !== "Metadata" ) { + indentFile = (nodeLevel + 1) * 25; + %> + + + + <% } else { %> + + + + + + <% } %> + <% } else { %> <% var indentFile = (nodeLevel + 1) * 40; %> @@ -17,7 +32,7 @@ <% var filenameEditable = "false" %> <% var filenameTitle = "You are not authorized to rename this file" %> -<% if (canRename) { %> +<% if (viewType === "edit" && canRename) { %> <% fileClasses = fileClasses + " canRename" %> <% var filenameEditable = "true" %> <% var filenameTitle = "Click to rename" %> @@ -50,83 +65,112 @@ <% } %> -<% if (type && type != "Metadata") { %> +<% if (viewType === "edit" && type && type != "Metadata") { %> <%= type %> +<% } else if (viewType === "view" && fileType) { %> + <%= fileType %> <% } else { %> - - -<% } %> - -<% if( MetacatUI.appModel.get("allowAccessPolicyChanges")){ %> - - - + <% } %> - - - <% if( uploadStatus == "p" ){ %> -
-
-
- <% } else if( (!uploadStatus || uploadStatus == "c" || uploadStatus == "q") && type != "Metadata" && numAttributes == 0 ) { %> - - <% } else if( uploadStatus == "w" && type != "Metadata" ) { %> - - <% } else if( type != "Metadata" && numAttributes > 0 && hasInvalidAttribute ) { %> - - <% } else if( type != "Metadata" && numAttributes > 0 && !hasInvalidAttribute && entityIsValid ) { %> - - <% } else if( type != "Metadata" && !entityIsValid ) { %> - - <% } else if( uploadStatus == "c" ) { %> - - <% } else if( uploadStatus == "e" ) { %> - - <% } else if( uploadStatus == "l" && type != "Metadata"){ %> - +<% if (viewType === "edit") { %> + + <% if( MetacatUI.appModel.get("allowAccessPolicyChanges")){ %> + + + <% } %> - - - - <% if ( typeof formatType !== "undefined" && formatType !== null && formatType.toLowerCase() === "metadata" ) { %> - - - - - - <% } - else { %> - -
- <% if( hasInvalidAttribute || !entityIsValid ){ %> - - <% } else if( (!uploadStatus || uploadStatus == "c" || uploadStatus == "q" || uploadStatus == "w") && type != "Metadata" && numAttributes == 0 ){ %> - - <% } else { %> - - <% } %> - - - - -
- - <% } %> - + + + + <% if( uploadStatus == "p" ){ %> +
+
+
+ <% } else if( (!uploadStatus || uploadStatus == "c" || uploadStatus == "q") && type != "Metadata" && numAttributes == 0 ) { %> + + <% } else if( uploadStatus == "w" && type != "Metadata" ) { %> + + <% } else if( type != "Metadata" && numAttributes > 0 && hasInvalidAttribute ) { %> + + <% } else if( type != "Metadata" && numAttributes > 0 && !hasInvalidAttribute && entityIsValid ) { %> + + <% } else if( type != "Metadata" && !entityIsValid ) { %> + + <% } else if( uploadStatus == "c" ) { %> + + <% } else if( uploadStatus == "e" ) { %> + + <% } else if( uploadStatus == "l" && type != "Metadata"){ %> + + <% } %> + + + + + <% if ( typeof formatType !== "undefined" && formatType !== null && formatType.toLowerCase() === "metadata" ) { %> + + + + + + <% } + else { %> + +
+ <% if( hasInvalidAttribute || !entityIsValid ){ %> + + <% } else if( (!uploadStatus || uploadStatus == "c" || uploadStatus == "q" || uploadStatus == "w") && type != "Metadata" && numAttributes == 0 ){ %> + + <% } else { %> + + <% } %> + + + + +
+ + <% } %> + + +<% } else if (viewType == "view") { %> + <% if (memberRowMetrics !== undefined) { %> + + + <%= memberRowMetrics %> + + + <% } else { %> + + + + <% } %> + + <% if (isMetadata !== undefined && !isMetadata) { %> + + <% } %> + + <% if (downloadUrl !== undefined ) { %> + + <% } %> + + + +<% } %> diff --git a/src/js/templates/dataItemHierarchy.html b/src/js/templates/dataItemHierarchy.html new file mode 100644 index 000000000..5c5475fdc --- /dev/null +++ b/src/js/templates/dataItemHierarchy.html @@ -0,0 +1,73 @@ + + + <% if (nodeLevel !== "undefined") { + if (isFolder) { + var indentFile = (nodeLevel) * 30 + 10; + } + else { + var indentFile = (nodeLevel) * 30; + } + } + else { + var indentFile = 30; + } + %> + + <% if (icon !== "undefined") { + %> + <% if (isFolder) { + %> + + + + <% } else { + %> + + <% } + %> + + <% } else { %> + + <% } %> + + + <%= objectTitle %> + + + + +<% if ( typeof sizeStr != "undefined" && sizeStr ) { %> + <%= sizeStr %> + +<% } else {%> + <%= size %> +<% } %> + + +<% if (fileType) { %> + <%= fileType %> +<% } else { %> + +<% } %> + +<% if (memberRowMetrics !== undefined && memberRowMetrics > 0) { %> + + + <%= memberRowMetrics %> + + +<% } else { %> + + + +<% } %> + + + <% if (isMetadata !== undefined && insertInfoIcon && !isMetadata && moreInfoLink !== undefined) { %> + + + + <% } %> + + + diff --git a/src/js/templates/dataPackage.html b/src/js/templates/dataPackage.html index 6d686475a..1f7e0bcfb 100644 --- a/src/js/templates/dataPackage.html +++ b/src/js/templates/dataPackage.html @@ -1,19 +1,79 @@ - - - - - Files - - Size - - Type - - <% if( MetacatUI.appModel.get("allowAccessPolicyChanges") ){ %> - Share - <% } %> - - Status - - +<% + if (!edit){ + if(!title) var title = "Files in this dataset"; + if(!colspan) var colspan = 7; + } +%> +<% if (!edit) {%> + + + <%=title%> + + + + + + + Files and Folders + + <% if (dataPackageSorting) {%> + + <% } %> + <% if (dataPackageFiltering) {%> + + + Filter + + <% } %> + + + + Size + <% if (dataPackageSorting) {%> + + <% } %> + + + + Type + <% if (dataPackageSorting) {%> + + <% } %> + + + <% if( MetacatUI.appModel.get("displayDatasetMetrics") ){ %> + + Metrics + <% if (dataPackageSorting) {%> + + <% } %> + + <% } %> + + Actions + + + <%=loading%> +<% } else { %> + + + + + + Files + + Size + + Type + + <% if( edit && MetacatUI.appModel.get("allowAccessPolicyChanges") ){ %> + Share + <% } %> + + Status + + + <%=loading%> +<% } %> \ No newline at end of file diff --git a/src/js/templates/dataPackageHeader.html b/src/js/templates/dataPackageHeader.html new file mode 100644 index 000000000..11426b4bc --- /dev/null +++ b/src/js/templates/dataPackageHeader.html @@ -0,0 +1,42 @@ + + + <% + var indentFile = 10; + %> + + + + + + + + <%= title %> + + + + + + + + + + + + + + + + + + + + + + <% if (!disablePackageDownloads) { %> + + <% } %> + + + \ No newline at end of file diff --git a/src/js/templates/metadata/metadata.html b/src/js/templates/metadata/metadata.html index 4645b0c29..22a2ad4d8 100644 --- a/src/js/templates/metadata/metadata.html +++ b/src/js/templates/metadata/metadata.html @@ -10,7 +10,7 @@
-
+