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://github.com/NCEAS/eml/blob/master/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 } }, /* * 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: * * file-alt.1.1.txt * file-again.1.1.txt * file.1.1.txt * A file summary * */ 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 = errors; 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); } // 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 var attributeList = this.get("attributeList"); var attributeListInDOM = $(objectDOM).children("attributelist"); 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; } );