define([
"jquery",
"underscore",
"backbone",
"uuid",
"models/DataONEObject",
"models/metadata/eml211/EMLAttribute",
], function ($, _, Backbone, uuid, DataONEObject, EMLAttribute) {
/**
* @class EMLEntity
* @classdesc EMLEntity represents an abstract data entity, corresponding
* with the EML EntityGroup and other elements common to all
* entity types, including otherEntity, dataTable, spatialVector,
* spatialRaster, and storedProcedure
* @classcategory Models/Metadata/EML211
* @see https://eml.ecoinformatics.org/schema/eml-entity_xsd
* @extends Backbone.Model
*/
var EMLEntity = Backbone.Model.extend(
/** @lends EMLEntity.prototype */ {
//The class name for this model
type: "EMLEntity",
/* Attributes of any entity */
defaults: function () {
return {
/* Attributes from EML */
xmlID: null, // The XML id of the entity
alternateIdentifier: [], // Zero or more alt ids
entityName: null, // Required, the name of the entity
entityDescription: null, // Description of the entity
physical: [], // Zero to many EMLPhysical objects
physicalMD5Checksum: null,
physicalSize: null,
physicalObjectName: null,
coverage: [], // Zero to many EML{Geo|Taxon|Temporal}Coverage objects
methods: null, // Zero or one EMLMethod object
additionalInfo: [], // Zero to many EMLText objects
attributeList: [], // Zero to many EMLAttribute objects
constraint: [], // Zero to many EMLConstraint objects
references: null, // A reference to another EMLEntity by id (needs work)
//Temporary attribute until we implement the eml-physical module
downloadID: null,
formatName: null,
/* Attributes not from EML */
nodeOrder: [
// The order of the top level XML element nodes
"alternateIdentifier",
"entityName",
"entityDescription",
"physical",
"coverage",
"methods",
"additionalInfo",
"annotation",
"attributeList",
"constraint",
],
parentModel: null, // The parent model this entity belongs to
dataONEObject: null, //Reference to the DataONEObject this EMLEntity describes
objectXML: null, // The serialized XML of this EML entity
objectDOM: null, // The DOM of this EML entity
type: "otherentity",
};
},
/*
* The map of lower case to camel case node names
* needed to deal with parsing issues with $.parseHTML().
* Use this until we can figure out issues with $.parseXML().
*/
nodeNameMap: {
alternateidentifier: "alternateIdentifier",
entityname: "entityName",
entitydescription: "entityDescription",
additionalinfo: "additionalInfo",
attributelist: "attributeList",
},
/* Initialize an EMLEntity object */
initialize: function (attributes, options) {
// if options.parse = true, Backbone will call parse()
// Register change events
this.on(
"change:alternateIdentifier " +
"change:entityName " +
"change:entityDescription " +
"change:physical " +
"change:coverage " +
"change:methods " +
"change:additionalInfo " +
"change:attributeList " +
"change:constraint " +
"change:references",
EMLEntity.trickleUpChange,
);
//Listen to changes on the DataONEObject file name
if (this.get("dataONEObject")) {
this.listenTo(
this.get("dataONEObject"),
"change:fileName",
this.updateFileName,
);
}
//Listen to changes on the DataONEObject to reset the listener
this.on("change:dataONEObject", function (entity, dataONEObj) {
//Stop listening to the old DataONEObject
if (this.previous("dataONEObject")) {
this.stopListening(
this.previous("dataONEObject"),
"change:fileName",
);
}
//Listen to changes on the file name
this.listenTo(dataONEObj, "change:fileName", this.updateFileName);
});
},
/*
* Parse the incoming entity's common XML elements
* Content example:
* <otherEntity>
* <alternateIdentifier>file-alt.1.1.txt</alternateIdentifier>
* <alternateIdentifier>file-again.1.1.txt</alternateIdentifier>
* <entityName>file.1.1.txt</entityName>
* <entityDescription>A file summary</entityDescription>
* </otherEntity>
*/
parse: function (attributes, options) {
var $objectDOM;
var objectDOM = attributes.objectDOM;
var objectXML = attributes.objectXML;
// Use the cached object if we have it
if (objectDOM) {
$objectDOM = $(objectDOM);
} else if (objectXML) {
$objectDOM = $(objectXML);
}
// Add the XML id
attributes.xmlID = $objectDOM.attr("id");
// Add the alternateIdentifiers
attributes.alternateIdentifier = [];
var alternateIds = $objectDOM.children("alternateidentifier");
_.each(alternateIds, function (alternateId) {
attributes.alternateIdentifier.push(alternateId.textContent);
});
// Add the entityName
attributes.entityName = $objectDOM.children("entityname").text();
// Add the entityDescription
attributes.entityDescription = $objectDOM
.children("entitydescription")
.text();
//Get some physical attributes from the EMLPhysical module
var physical = $objectDOM.find("physical");
if (physical) {
attributes.physicalSize = physical.find("size").text();
attributes.physicalObjectName = physical.find("objectname").text();
var checksumType = physical.find("authentication").attr("method");
if (checksumType == "MD5")
attributes.physicalMD5Checksum = physical
.find("authentication")
.text();
}
attributes.objectXML = objectXML;
attributes.objectDOM = $objectDOM[0];
//Find the id from the download distribution URL
var urlNode = $objectDOM.find("url");
if (urlNode.length) {
var downloadURL = urlNode.text(),
downloadID = "";
if (downloadURL.indexOf("/resolve/") > -1)
downloadID = downloadURL.substring(
downloadURL.indexOf("/resolve/") + 9,
);
else if (downloadURL.indexOf("/object/") > -1)
downloadID = downloadURL.substring(
downloadURL.indexOf("/object/") + 8,
);
else if (downloadURL.indexOf("ecogrid") > -1) {
var withoutEcoGridPrefix = downloadURL.substring(
downloadURL.indexOf("ecogrid://") + 10,
),
downloadID = withoutEcoGridPrefix.substring(
withoutEcoGridPrefix.indexOf("/") + 1,
);
}
if (downloadID.length) attributes.downloadID = downloadID;
}
//Find the format name
var formatNode = $objectDOM.find("formatName");
if (formatNode.length) {
attributes.formatName = formatNode.text();
}
// Add the attributeList
var attributeList = $objectDOM.find("attributelist");
var attribute; // An individual EML attribute
var options = { parse: true };
attributes.attributeList = [];
if (attributeList.length) {
_.each(
attributeList[0].children,
function (attr) {
attribute = new EMLAttribute(
{
objectDOM: attr,
objectXML: attr.outerHTML,
parentModel: this,
},
options,
);
// Can't use this.addAttribute() here (no this yet)
attributes.attributeList.push(attribute);
},
this,
);
}
return attributes;
},
/*
* Add an attribute to the attributeList, inserting it
* at the zero-based index
*/
addAttribute: function (attribute, index) {
if (typeof index == "undefined") {
this.get("attributeList").push(attribute);
} else {
this.get("attributeList").splice(index, attribute);
}
this.trigger("change:attributeList");
},
/*
* Remove an EMLAttribute model from the attributeList array
*
* @param {EMLAttribute} - The EMLAttribute model to remove from this model's attributeList
*/
removeAttribute: function (attribute) {
//Get the index of the EMLAttribute in the array
var attrIndex = this.get("attributeList").indexOf(attribute);
//If this attribute model does not exist in the attribute list, don't do anything
if (attrIndex == -1) {
return;
}
//Remove that index from the array
this.get("attributeList").splice(attrIndex, 1);
//Trickle the change up the model chain
this.trickleUpChange();
},
/* Validate the top level EMLEntity fields */
validate: function () {
var errors = {};
// will be run by calls to isValid()
if (!this.get("entityName")) {
errors.entityName = "An entity name is required.";
}
//Validate the attributes
var attributeErrors = this.validateAttributes();
if (attributeErrors.length) errors.attributeList = attributeErrors;
if (Object.keys(errors).length) return errors;
else {
this.trigger("valid");
return false;
}
},
/*
* Validates each of the EMLAttribute models in the attributeList
*
* @return {Array} - Returns an array of error messages for all the EMlAttribute models
*/
validateAttributes: function () {
var errors = [];
//Validate each of the EMLAttributes
_.each(this.get("attributeList"), function (attribute) {
if (!attribute.isValid()) {
errors.push(attribute.validationError);
}
});
return errors;
},
/* Copy the original XML and update fields in a DOM object */
updateDOM: function (objectDOM) {
var nodeToInsertAfter;
var type = this.get("type") || "otherEntity";
if (!objectDOM) {
objectDOM = this.get("objectDOM");
}
var objectXML = this.get("objectXML");
// If present, use the cached DOM
if (objectDOM) {
objectDOM = objectDOM.cloneNode(true);
// otherwise, use the cached XML
} else if (objectXML) {
objectDOM = $(objectXML)[0].cloneNode(true);
// This is new, create it
} else {
objectDOM = document.createElement(type);
}
//Update the id attribute on this XML node
// update the id attribute
if (this.get("dataONEObject")) {
//Ideally, the EMLEntity will use the object's id in it's id attribute, so we wil switch them
var xmlID = this.get("dataONEObject").getXMLSafeID();
//Set the xml-safe id on the model and use it as the id attribute
$(objectDOM).attr("id", xmlID);
this.set("xmlID", xmlID);
}
//If there isn't a matching DataONEObject but there is an id set on this model, use that id
else if (this.get("xmlID")) {
$(objectDOM).attr("id", this.get("xmlID"));
}
// Update the alternateIdentifiers
var altIDs = this.get("alternateIdentifier");
if (altIDs) {
if (altIDs.length) {
// Copy and reverse the array for prepending
altIDs = Array.from(altIDs).reverse();
// Remove all current alternateIdentifiers
$(objectDOM).find("alternateIdentifier").remove();
// Add the new list back in
_.each(altIDs, function (altID) {
$(objectDOM).prepend(
$(document.createElement("alternateIdentifier")).text(altID),
);
});
}
} else {
// Remove all current alternateIdentifiers
$(objectDOM).find("alternateIdentifier").remove();
}
// Update the entityName
if (this.get("entityName")) {
if ($(objectDOM).find("entityName").length) {
$(objectDOM).find("entityName").text(this.get("entityName"));
} else {
nodeToInsertAfter = this.getEMLPosition(objectDOM, "entityName");
if (!nodeToInsertAfter) {
$(objectDOM).append(
$(document.createElement("entityName")).text(
this.get("entityName"),
)[0],
);
} else {
$(nodeToInsertAfter).after(
$(document.createElement("entityName")).text(
this.get("entityName"),
)[0],
);
}
}
}
// Update the entityDescription
if (this.get("entityDescription")) {
if ($(objectDOM).find("entityDescription").length) {
$(objectDOM)
.find("entityDescription")
.text(this.get("entityDescription"));
} else {
nodeToInsertAfter = this.getEMLPosition(
objectDOM,
"entityDescription",
);
if (!nodeToInsertAfter) {
$(objectDOM).append(
$(document.createElement("entityDescription")).text(
this.get("entityDescription"),
)[0],
);
} else {
$(nodeToInsertAfter).after(
$(document.createElement("entityDescription")).text(
this.get("entityDescription"),
)[0],
);
}
}
}
//If there is no entity description
else {
//If there is an entity description node in the XML, remove it
$(objectDOM).find("entityDescription").remove();
}
// TODO: Update the physical section
// TODO: Update the coverage section
// TODO: Update the methods section
// Update the additionalInfo
var addInfos = this.get("additionalInfo");
if (addInfos) {
if (addInfos.length) {
// Copy and reverse the array for prepending
addInfos = Array.from(addInfos).reverse();
// Remove all current alternateIdentifiers
$(objectDOM).find("additionalInfo").remove();
// Add the new list back in
_.each(addInfos, function (additionalInfo) {
$(objectDOM).prepend(
document.createElement("additionalInfo").text(additionalInfo),
);
});
}
}
// Update the attributeList section
let attributeList = this.get("attributeList");
let attributeListInDOM = $(objectDOM).children("attributelist");
let attributeListNode;
if (attributeListInDOM.length) {
attributeListNode = attributeListInDOM[0];
$(attributeListNode).children().remove(); // Each attr will be replaced
} else {
attributeListNode = document.createElement("attributeList");
nodeToInsertAfter = this.getEMLPosition(objectDOM, "attributeList");
if (!nodeToInsertAfter) {
$(objectDOM).append(attributeListNode);
} else {
$(nodeToInsertAfter).after(attributeListNode);
}
}
var updatedAttrDOM;
if (attributeList.length) {
// Add each attribute
_.each(
attributeList,
function (attribute) {
updatedAttrDOM = attribute.updateDOM();
$(attributeListNode).append(updatedAttrDOM);
},
this,
);
} else {
// Attributes are not defined, remove them from the DOM
attributeListNode.remove();
}
// TODO: Update the constraint section
return objectDOM;
},
/**
* Update the file name in the EML
*/
updateFileName: function () {
var dataONEObj = this.get("dataONEObject");
//Get the DataONEObject model associated with this EML Entity
if (dataONEObj) {
//If the last file name matched the EML entity name, then update it
if (dataONEObj.previous("fileName") == this.get("entityName")) {
this.set("entityName", dataONEObj.get("fileName"));
}
//If the DataONEObject doesn't have an old file name or entity name, then update it
else if (
!dataONEObj.previous("fileName") ||
!this.get("entityName")
) {
this.set("entityName", dataONEObj.get("fileName"));
}
}
},
/*
* Get the DOM node preceding the given nodeName
* to find what position in the EML document
* the named node should be appended
*/
getEMLPosition: function (objectDOM, nodeName) {
var nodeOrder = this.get("nodeOrder");
var position = _.indexOf(nodeOrder, nodeName);
// Append to the bottom if not found
if (position == -1) {
return $(objectDOM).children().last()[0];
}
// Otherwise, 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--) {
if ($(objectDOM).find(nodeOrder[i].toLowerCase()).length) {
return $(objectDOM).find(nodeOrder[i].toLowerCase()).last()[0];
}
}
},
/*
* Climbs up the model heirarchy until it finds the EML model
*
* @return {EML211 or false} - Returns the EML 211 Model or false if not found
*/
getParentEML: function () {
var emlModel = this.get("parentModel"),
tries = 0;
while (emlModel.type !== "EML" && tries < 6) {
emlModel = emlModel.get("parentModel");
tries++;
}
if (emlModel && emlModel.type == "EML") return emlModel;
else return false;
},
/*Format the EML XML for entities*/
formatXML: function (xmlString) {
return DataONEObject.prototype.formatXML.call(this, xmlString);
},
/* Let the top level package know of attribute changes from this object */
trickleUpChange: function () {
MetacatUI.rootDataPackage.packageModel.set("changed", true);
},
},
);
return EMLEntity;
});