From 9409832d2ce0fddfab79c37b66d507ca2151e1de Mon Sep 17 00:00:00 2001 From: Robyn Thiessen-Bock Date: Fri, 28 Jul 2023 14:54:43 -0400 Subject: [PATCH] Add serializing and parsing to EMLDistribution - Use in the EML211 model Issue #1380 --- src/js/models/metadata/eml211/EML211.js | 25 ++- .../models/metadata/eml211/EMLDistribution.js | 211 ++++++++++++++---- 2 files changed, 194 insertions(+), 42 deletions(-) diff --git a/src/js/models/metadata/eml211/EML211.js b/src/js/models/metadata/eml211/EML211.js index 42a833005..8d5eb77f0 100644 --- a/src/js/models/metadata/eml211/EML211.js +++ b/src/js/models/metadata/eml211/EML211.js @@ -55,8 +55,7 @@ define(['jquery', 'underscore', 'backbone', 'uuid', keywordSets: [], //array of EMLKeywordSet objects additionalInfo: [], intellectualRights: "This work is dedicated to the public domain under the Creative Commons Universal 1.0 Public Domain Dedication. To view a copy of this dedication, visit https://creativecommons.org/publicdomain/zero/1.0/.", - onlineDist: [], // array of EMLOnlineDist objects - offlineDist: [], // array of EMLOfflineDist objects + distribution: [], // array of EMLDistribution objects geoCoverage : [], //an array for EMLGeoCoverages temporalCoverage : [], //an array of EMLTempCoverage models taxonCoverage : [], //an array of EMLTaxonCoverages @@ -515,13 +514,13 @@ define(['jquery', 'underscore', 'backbone', 'uuid', })); } //EML Distribution modules are stored in EMLDistribution models - else if(_.contains(emlDistribution, thisNode.localName)){ + else if(_.contains(emlDistribution, thisNode.localName)) { if(typeof modelJSON[thisNode.localName] == "undefined") modelJSON[thisNode.localName] = []; modelJSON[thisNode.localName].push(new EMLDistribution({ objectDOM: thisNode, parentModel: model - })); + }, { parse: true })); } //The EML Project is stored in the EMLProject model else if(thisNode.localName == "project"){ @@ -1028,6 +1027,24 @@ define(['jquery', 'underscore', 'backbone', 'uuid', .html("" + this.get("intellectualRights") + "")); } } + + // Serialize the distribution + const distributions = this.get('distribution'); + if (distributions && distributions.length > 0) { + // Remove existing nodes + datasetNode.children('distribution').remove(); + // Get the updated DOMs + const distributionDOMs = distributions.map(d => d.updateDOM()); + // Insert the updated DOMs in their correct positions + distributionDOMs.forEach((dom, i) => { + const insertAfter = this.getEMLPosition(eml, 'distribution'); + if (insertAfter) { + insertAfter.after(dom); + } else { + datasetNode.append(dom); + } + }); + } //Detach the project elements from the DOM if(datasetNode.find("project").length){ diff --git a/src/js/models/metadata/eml211/EMLDistribution.js b/src/js/models/metadata/eml211/EMLDistribution.js index 844ead921..b15c6fdf4 100644 --- a/src/js/models/metadata/eml211/EMLDistribution.js +++ b/src/js/models/metadata/eml211/EMLDistribution.js @@ -5,24 +5,61 @@ define(["jquery", "underscore", "backbone", "models/DataONEObject"], function ( Backbone, DataONEObject ) { + /** + * @class EMLDistribution + * @classdesc Information on how the resource is distributed online and offline + * @classcategory Models/Metadata/EML211 + * @see https://eml.ecoinformatics.org/schema/eml-resource_xsd.html#DistributionType + * @extends Backbone.Model + * @constructor + */ var EMLDistribution = Backbone.Model.extend({ defaults: { + type: "distribution", objectXML: null, objectDOM: null, mediumName: null, mediumVolume: null, mediumFormat: null, mediumNote: null, + url: null, onlineDescription: null, parentModel: null, }, - initialize: function (attributes) { - if (attributes.objectDOM) this.parse(attributes.objectDOM); + /** + * The direct children of the node that can have values, and + * that are supported by this model. "inline" is not supported yet. A + * distribution may have ONE of these nodes. + * @type {string[]} + * @since x.x.x + */ + distLocations: ["offline", "online"], + + /** + * lower-case EML node names that belong within the node. These must be in the correct order. + * @type {string[]} + * @since x.x.x + */ + offlineNodes: ["mediumname", "mediumvolume", "mediumformat", "mediumnote"], - this.on( - "change:mediumName change:mediumVolume change:mediumFormat " + - "change:mediumNote change:onlineDescription", + /** + * lower-case EML node names that belong within the node. These must be in the correct order. + * @type {string[]} + * @since x.x.x + */ + onlineNodes: ["url"], + + /** + * Initializes this EMLDistribution object + * @param {Object} options - A literal object with options to pass to the + * model + */ + initialize: function (attributes, options) { + const nodeAttr = Object.values(this.nodeNameMap()); + this.listenTo( + this, + "change:" + nodeAttr.join(" change:"), this.trickleUpChange ); }, @@ -41,69 +78,167 @@ define(["jquery", "underscore", "backbone", "models/DataONEObject"], function ( mediumname: "mediumName", mediumnote: "mediumNote", mediumvolume: "mediumVolume", - onlinedescription: "onlineDescription", + url: "url", }; }, - parse: function (objectDOM) { - if (!objectDOM) var xml = this.get("objectDOM"); - - var offline = $(xml).find("offline"), - online = $(xml).find("online"); - - if (offline.length) { - if ($(offline).children("mediumname").length) - this.parseNode($(offline).children("mediumname")); - if ($(offline).children("mediumvolume").length) - this.parseNode($(offline).children("mediumvolume")); - if ($(offline).children("mediumformat").length) - this.parseNode($(offline).children("mediumformat")); - if ($(offline).children("mediumnote").length) - this.parseNode($(offline).children("mediumnote")); - } - - if (online.length) { - if ($(online).children("onlinedescription").length) - this.parseNode($(online).children("onlinedescription")); - } - }, + /** + * Parses the given XML node or object and sets the model's attributes + * @param {Object} attributes - the attributes passed in when the model is + * instantiated. Should include objectDOM or objectXML to be parsed. + */ + parse: function (attributes) { + if (!attributes) attributes = {}; + const objectDOM = attributes.objectDOM || attributes.objectXML; + if (!objectDOM) return attributes; + const $objectDOM = $(objectDOM); - parseNode: function (node) { - if (!node || (Array.isArray(node) && !node.length)) return; + const nodeNameMap = this.nodeNameMap(); + this.distLocations.forEach((distLocation) => { + const location = $objectDOM.find(distLocation); + if (location.length) { + this[`${distLocation}Nodes`].forEach((nodeName) => { + const value = location.children(nodeName)?.text()?.trim(); + if (value.length) { + attributes[nodeNameMap[nodeName]] = value; + } + }); + } + }); - this.set($(node)[0].localName, $(node).text()); + return attributes; }, + /** + * Returns the XML string representation of this model + * @returns {string} + */ serialize: function () { - var objectDOM = this.updateDOM(), - xmlString = objectDOM.outerHTML; + const objectDOM = this.updateDOM(); + const xmlString = objectDOM.outerHTML; - //Camel-case the XML + // Camel-case the XML xmlString = this.formatXML(xmlString); return xmlString; }, + /** + * Check if the model has values for the given distribution location. + * @param {string} location - one of the names of the direct children of the + * node, i.e. any of the values in this.distLocations. + * @returns {boolean} - true if the model has values for the given location, + * false otherwise. + * @since x.x.x + */ + hasValuesForDistributionLocation(location) { + const nodeNameMap = this.nodeNameMap(); + return this[`${location}Nodes`].some((nodeName) => { + return this.get(nodeNameMap[nodeName]); + }); + }, + /* * Makes a copy of the original XML DOM and updates it with the new values * from the model. */ updateDOM: function () { - var objectDOM = this.get("objectDOM").cloneNode(true); + const objectDOM = + this.get("objectDOM") || document.createElement(this.get("type")); + const $objectDOM = $(objectDOM); // Remove empty (zero-length or whitespace-only) nodes - $(objectDOM) + $objectDOM .find("*") .filter(function () { - return $.trim(this.innerHTML) === ""; + return !$.trim($(this).text()); }) .remove(); + const nodeNameMap = this.nodeNameMap(); + + // Determine if this is an online, offline, or inline distribution + const distLocation = this.distLocations.find((location) => { + return this.hasValuesForDistributionLocation(location); + }); + + // Remove all other distribution locations + this.distLocations.forEach((location) => { + if (location !== distLocation) { + $objectDOM.find(location).remove(); + } + }); + + // Add the distribution location if it doesn't exist + if (!$objectDOM.find(distLocation).length) { + $objectDOM.append(`<${distLocation}>`); + } + + // For each node in the distribution location, add the value from the + // model. If the model value is empty, remove the node. Make sure that we + // don't replace any existing nodes, since not all nodes are supported by + // this model yet. We also need to ensure that the nodes are in the + // correct order. + this[`${distLocation}Nodes`].forEach((nodeName) => { + const nodeValue = this.get(nodeNameMap[nodeName]); + if (nodeValue) { + const node = $objectDOM.find(`${distLocation} > ${nodeName}`); + if (node.length) { + node.text(nodeValue); + } else { + const newNode = $(`<${nodeName}>${nodeValue}`); + const position = this.getEMLPosition(objectDOM, nodeName); + if (position) { + newNode.insertAfter(position); + } else { + $objectDOM.children(distLocation).append(newNode); + } + } + } else { + $objectDOM.find(`${distLocation} > ${nodeName}`).remove(); + } + }); return objectDOM; }, /* - * Climbs up the model heirarchy until it finds the EML model + * Returns the node in the object DOM that the given node type should be + * inserted after. + * @param {string} nodeName - The name of the node to find the position for + * @return {jQuery} - The jQuery object of the node that the given node + * should be inserted after, or false if the node is not supported by this + * model. + * @since x.x.x + */ + getEMLPosition: function (objectDOM, nodeName) { + // If this is a top level node, return false since it should be inserted + // within the node, and there must only be one. + if (this.distLocations.includes(nodeName)) return false; + + // Handle according to whether it's an online or offline node + const nodeNameMap = this.nodeNameMap(); + this.distLocations.forEach((distLocation) => { + const nodeOrder = this[`${distLocation}Nodes`]; + const siblingNodes = $(objectDOM).find(distLocation).children(); + let position = nodeOrder.indexOf(nodeName); + if (position > -1) { + // Go through each node in the node list and find the position where this + // node will be inserted after + for (var i = position - 1; i >= 0; i--) { + const checkNode = siblingNodes.filter(nodeOrder[i]); + if (checkNode.length) { + return checkNode.last(); + } + } + } + }); + + // If we get here, the node is not supported by this model + return false; + }, + + /* + * Climbs up the model hierarchy until it finds the EML model * * @return {EML211 or false} - Returns the EML 211 Model or false if not * found