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
*/
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;
}
);