/* global define */
define(['jquery', 'underscore', 'backbone', 'uuid',
'collections/Units',
'models/metadata/ScienceMetadata',
'models/DataONEObject',
'models/metadata/eml211/EMLGeoCoverage',
'models/metadata/eml211/EMLKeywordSet',
'models/metadata/eml211/EMLTaxonCoverage',
'models/metadata/eml211/EMLTemporalCoverage',
'models/metadata/eml211/EMLDistribution',
'models/metadata/eml211/EMLEntity',
'models/metadata/eml211/EMLDataTable',
'models/metadata/eml211/EMLOtherEntity',
'models/metadata/eml211/EMLParty',
'models/metadata/eml211/EMLProject',
'models/metadata/eml211/EMLText',
'models/metadata/eml211/EMLMethods',
'collections/metadata/eml/EMLAnnotations',
'models/metadata/eml211/EMLAnnotation'],
function($, _, Backbone, uuid, Units, ScienceMetadata, DataONEObject,
EMLGeoCoverage, EMLKeywordSet, EMLTaxonCoverage, EMLTemporalCoverage,
EMLDistribution, EMLEntity, EMLDataTable, EMLOtherEntity, EMLParty,
EMLProject, EMLText, EMLMethods, EMLAnnotations, EMLAnnotation) {
/**
* @class EML211
* @classdesc An EML211 object represents an Ecological Metadata Language
* document, version 2.1.1
* @classcategory Models/Metadata/EML211
* @extends ScienceMetadata
*/
var EML211 = ScienceMetadata.extend(
/** @lends EML211.prototype */{
type: "EML",
defaults: function(){
return _.extend(ScienceMetadata.prototype.defaults(), {
id: "urn:uuid:" + uuid.v4(),
formatId: "https://eml.ecoinformatics.org/eml-2.2.0",
objectXML: null,
isEditable: false,
alternateIdentifier: [],
shortName: null,
title: [],
creator: [], // array of EMLParty objects
metadataProvider: [], // array of EMLParty objects
associatedParty : [], // array of EMLParty objects
contact: [], // array of EMLParty objects
publisher: [], // array of EMLParty objects
pubDate: null,
language: null,
series: null,
abstract: [], //array of EMLText objects
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/.",
distribution: [], // array of EMLDistribution objects
geoCoverage : [], //an array for EMLGeoCoverages
temporalCoverage : [], //an array of EMLTempCoverage models
taxonCoverage : [], //an array of EMLTaxonCoverages
purpose: [],
entities: [], //An array of EMLEntities
pubplace: null,
methods: new EMLMethods(), // An EMLMethods objects
project: null, // An EMLProject object,
annotations: null, // Dataset-level annotations
dataSensitivityPropertyURI: "http://purl.dataone.org/odo/SENSO_00000005",
nodeOrder: [
"alternateidentifier",
"shortname",
"title",
"creator",
"metadataprovider",
"associatedparty",
"pubdate",
"language",
"series",
"abstract",
"keywordset",
"additionalinfo",
"intellectualrights",
"licensed",
"distribution",
"coverage",
"annotation",
"purpose",
"introduction",
"gettingstarted",
"acknowledgements",
"maintenance",
"contact",
"publisher",
"pubplace",
"methods",
"project",
"datatable",
"spatialraster",
"spatialvector",
"storedprocedure",
"view",
"otherentity",
"referencepublications",
"usagecitations",
"literaturecited",
]
});
},
units: new Units(),
initialize: function(attributes) {
// Call initialize for the super class
ScienceMetadata.prototype.initialize.call(this, attributes);
// EML211-specific init goes here
// this.set("objectXML", this.createXML());
this.parse(this.createXML());
this.on("sync", function(){
this.set("synced", true);
});
//Create a Unit collection
if(!this.units.length)
this.createUnits();
},
url: function(options) {
var identifier;
if ( options && options.update ) {
identifier = this.get("oldPid") || this.get("seriesid");
} else {
identifier = this.get("id") || this.get("seriesid");
}
return MetacatUI.appModel.get("objectServiceUrl") + encodeURIComponent(identifier);
},
/*
* Maps the lower-case EML node names (valid in HTML DOM) to the camel-cased EML node names (valid in EML).
* Used during parse() and serialize()
*/
nodeNameMap: function(){
return _.extend(
this.constructor.__super__.nodeNameMap(),
EMLDistribution.prototype.nodeNameMap(),
EMLGeoCoverage.prototype.nodeNameMap(),
EMLKeywordSet.prototype.nodeNameMap(),
EMLParty.prototype.nodeNameMap(),
EMLProject.prototype.nodeNameMap(),
EMLTaxonCoverage.prototype.nodeNameMap(),
EMLTemporalCoverage.prototype.nodeNameMap(),
EMLMethods.prototype.nodeNameMap(),
{
"accuracyreport" : "accuracyReport",
"actionlist" : "actionList",
"additionalclassifications" : "additionalClassifications",
"additionalinfo" : "additionalInfo",
"additionallinks" : "additionalLinks",
"additionalmetadata" : "additionalMetadata",
"allowfirst" : "allowFirst",
"alternateidentifier" : "alternateIdentifier",
"altitudedatumname" : "altitudeDatumName",
"altitudedistanceunits" : "altitudeDistanceUnits",
"altituderesolution" : "altitudeResolution",
"altitudeencodingmethod" : "altitudeEncodingMethod",
"altitudesysdef" : "altitudeSysDef",
"asneeded" : "asNeeded",
"associatedparty" : "associatedParty",
"attributeaccuracyexplanation" : "attributeAccuracyExplanation",
"attributeaccuracyreport" : "attributeAccuracyReport",
"attributeaccuracyvalue" : "attributeAccuracyValue",
"attributedefinition" : "attributeDefinition",
"attributelabel" : "attributeLabel",
"attributelist" : "attributeList",
"attributename" : "attributeName",
"attributeorientation" : "attributeOrientation",
"attributereference" : "attributeReference",
"awardnumber" : "awardNumber",
"awardurl" : "awardUrl",
"audiovisual" : "audioVisual",
"authsystem" : "authSystem",
"banddescription" : "bandDescription",
"bilinearfit" : "bilinearFit",
"binaryrasterformat" : "binaryRasterFormat",
"blockedmembernode" : "blockedMemberNode",
"booktitle" : "bookTitle",
"cameracalibrationinformationavailability" : "cameraCalibrationInformationAvailability",
"casesensitive" : "caseSensitive",
"cellgeometry" : "cellGeometry",
"cellsizexdirection" : "cellSizeXDirection",
"cellsizeydirection" : "cellSizeYDirection",
"changehistory" : "changeHistory",
"changedate" : "changeDate",
"changescope" : "changeScope",
"chapternumber" : "chapterNumber",
"characterencoding" : "characterEncoding",
"checkcondition" : "checkCondition",
"checkconstraint" : "checkConstraint",
"childoccurences" : "childOccurences",
"citableclassificationsystem" : "citableClassificationSystem",
"cloudcoverpercentage" : "cloudCoverPercentage",
"codedefinition" : "codeDefinition",
"codeexplanation" : "codeExplanation",
"codesetname" : "codesetName",
"codeseturl" : "codesetURL",
"collapsedelimiters" : "collapseDelimiters",
"communicationtype" : "communicationType",
"compressiongenerationquality" : "compressionGenerationQuality",
"compressionmethod" : "compressionMethod",
"conferencedate" : "conferenceDate",
"conferencelocation" : "conferenceLocation",
"conferencename" : "conferenceName",
"conferenceproceedings" : "conferenceProceedings",
"constraintdescription" : "constraintDescription",
"constraintname" : "constraintName",
"constanttosi" : "constantToSI",
"controlpoint" : "controlPoint",
"cornerpoint" : "cornerPoint",
"customunit" : "customUnit",
"dataformat" : "dataFormat",
"datasetgpolygon" : "datasetGPolygon",
"datasetgpolygonoutergring" : "datasetGPolygonOuterGRing",
"datasetgpolygonexclusiongring" : "datasetGPolygonExclusionGRing",
"datatable" : "dataTable",
"datatype" : "dataType",
"datetime" : "dateTime",
"datetimedomain" : "dateTimeDomain",
"datetimeprecision" : "dateTimePrecision",
"defaultvalue" : "defaultValue",
"definitionattributereference" : "definitionAttributeReference",
"denomflatratio" : "denomFlatRatio",
"depthsysdef" : "depthSysDef",
"depthdatumname" : "depthDatumName",
"depthdistanceunits" : "depthDistanceUnits",
"depthencodingmethod" : "depthEncodingMethod",
"depthresolution" : "depthResolution",
"descriptorvalue" : "descriptorValue",
"dictref" : "dictRef",
"diskusage" : "diskUsage",
"domainDescription" : "domainDescription",
"editedbook" : "editedBook",
"encodingmethod" : "encodingMethod",
"endcondition" : "endCondition",
"entitycodelist" : "entityCodeList",
"entitydescription" : "entityDescription",
"entityname" : "entityName",
"entityreference" : "entityReference",
"entitytype" : "entityType",
"enumerateddomain" : "enumeratedDomain",
"errorbasis" : "errorBasis",
"errorvalues" : "errorValues",
"externalcodeset" : "externalCodeSet",
"externallydefinedformat" : "externallyDefinedFormat",
"fielddelimiter" : "fieldDelimiter",
"fieldstartcolumn" : "fieldStartColumn",
"fieldwidth" : "fieldWidth",
"filmdistortioninformationavailability" : "filmDistortionInformationAvailability",
"foreignkey" : "foreignKey",
"formatname" : "formatName",
"formatstring" : "formatString",
"formatversion" : "formatVersion",
"fractiondigits" : "fractionDigits",
"fundername" : "funderName",
"funderidentifier" : "funderIdentifier",
"gettingstarted" : "gettingStarted",
"gring" : "gRing",
"gringpoint" : "gRingPoint",
"gringlatitude" : "gRingLatitude",
"gringlongitude" : "gRingLongitude",
"geogcoordsys" : "geogCoordSys",
"geometricobjectcount" : "geometricObjectCount",
"georeferenceinfo" : "georeferenceInfo",
"highwavelength" : "highWavelength",
"horizontalaccuracy" : "horizontalAccuracy",
"horizcoordsysdef" : "horizCoordSysDef",
"horizcoordsysname" : "horizCoordSysName",
"identifiername" : "identifierName",
"illuminationazimuthangle" : "illuminationAzimuthAngle",
"illuminationelevationangle" : "illuminationElevationAngle",
"imagingcondition" : "imagingCondition",
"imagequalitycode" : "imageQualityCode",
"imageorientationangle" : "imageOrientationAngle",
"intellectualrights" : "intellectualRights",
"imagedescription" : "imageDescription",
"isbn" : "ISBN",
"issn" : "ISSN",
"joincondition" : "joinCondition",
"keywordtype" : "keywordType",
"languagevalue" : "LanguageValue",
"languagecodestandard" : "LanguageCodeStandard",
"lensdistortioninformationavailability" : "lensDistortionInformationAvailability",
"licensename" : "licenseName",
"licenseurl" : "licenseURL",
"linenumber" : "lineNumber",
"literalcharacter" : "literalCharacter",
"literallayout" : "literalLayout",
"literaturecited" : "literatureCited",
"lowwavelength" : "lowWaveLength",
"machineprocessor" : "machineProcessor",
"maintenanceupdatefrequency" : "maintenanceUpdateFrequency",
"matrixtype" : "matrixType",
"maxexclusive" : "maxExclusive",
"maxinclusive" : "maxInclusive",
"maxlength" : "maxLength",
"maxrecordlength" : "maxRecordLength",
"maxvalues" : "maxValues",
"measurementscale" : "measurementScale",
"metadatalist" : "metadataList",
"methodstep" : "methodStep",
"minexclusive" : "minExclusive",
"mininclusive" : "minInclusive",
"minlength" : "minLength",
"minvalues" : "minValues",
"missingvaluecode" : "missingValueCode",
"moduledocs" : "moduleDocs",
"modulename" : "moduleName",
"moduledescription" : "moduleDescription",
"multiband" : "multiBand",
"multipliertosi" : "multiplierToSI",
"nonnumericdomain" : "nonNumericDomain",
"notnullconstraint" : "notNullConstraint",
"notplanned" : "notPlanned",
"numberofbands" : "numberOfBands",
"numbertype" : "numberType",
"numericdomain" : "numericDomain",
"numfooterlines" : "numFooterLines",
"numheaderlines" : "numHeaderLines",
"numberofrecords" : "numberOfRecords",
"numberofvolumes" : "numberOfVolumes",
"numphysicallinesperrecord" : "numPhysicalLinesPerRecord",
"objectname" : "objectName",
"oldvalue" : "oldValue",
"operatingsystem" : "operatingSystem",
"orderattributereference" : "orderAttributeReference",
"originalpublication" : "originalPublication",
"otherentity" : "otherEntity",
"othermaintenanceperiod" : "otherMaintenancePeriod",
"parameterdefinition" : "parameterDefinition",
"packageid" : "packageId",
"pagerange" : "pageRange",
"parentoccurences" : "parentOccurences",
"parentsi" : "parentSI",
"peakresponse" : "peakResponse",
"personalcommunication" : "personalCommunication",
"physicallinedelimiter" : "physicalLineDelimiter",
"pointinpixel" : "pointInPixel",
"preferredmembernode" : "preferredMemberNode",
"preprocessingtypecode" : "preProcessingTypeCode",
"primarykey" : "primaryKey",
"primemeridian" : "primeMeridian",
"proceduralstep" : "proceduralStep",
"programminglanguage" : "programmingLanguage",
"projcoordsys" : "projCoordSys",
"projectionlist" : "projectionList",
"propertyuri" : "propertyURI",
"pubdate" : "pubDate",
"pubplace" : "pubPlace",
"publicationplace" : "publicationPlace",
"quantitativeaccuracyreport" : "quantitativeAccuracyReport",
"quantitativeaccuracyvalue" : "quantitativeAccuracyValue",
"quantitativeaccuracymethod" : "quantitativeAccuracyMethod",
"quantitativeattributeaccuracyassessment" : "quantitativeAttributeAccuracyAssessment",
"querystatement" : "queryStatement",
"quotecharacter" : "quoteCharacter",
"radiometricdataavailability" : "radiometricDataAvailability",
"rasterorigin" : "rasterOrigin",
"recommendedunits" : "recommendedUnits",
"recommendedusage" : "recommendedUsage",
"referencedkey" : "referencedKey",
"referencetype" : "referenceType",
"relatedentry" : "relatedEntry",
"relationshiptype" : "relationshipType",
"reportnumber" : "reportNumber",
"reprintedition" : "reprintEdition",
"researchproject" : "researchProject",
"researchtopic" : "researchTopic",
"recorddelimiter" : "recordDelimiter",
"referencepublication" : "referencePublication",
"revieweditem" : "reviewedItem",
"rowcolumnorientation" : "rowColumnOrientation",
"runtimememoryusage" : "runtimeMemoryUsage",
"samplingdescription" : "samplingDescription",
"scalefactor" : "scaleFactor",
"sequenceidentifier" : "sequenceIdentifier",
"semiaxismajor" : "semiAxisMajor",
"shortname" : "shortName",
"simpledelimited" : "simpleDelimited",
"spatialraster" : "spatialRaster",
"spatialreference" : "spatialReference",
"spatialvector" : "spatialVector",
"standalone" : "standAlone",
"standardunit" : "standardUnit",
"startcondition" : "startCondition",
"studyareadescription" : "studyAreaDescription",
"storagetype" : "storageType",
"studyextent" : "studyExtent",
"studytype" : "studyType",
"textdelimited" : "textDelimited",
"textdomain" : "textDomain",
"textfixed" : "textFixed",
"textformat" : "textFormat",
"topologylevel" : "topologyLevel",
"tonegradation" : "toneGradation",
"totaldigits" : "totalDigits",
"totalfigures" : "totalFigures",
"totalpages" : "totalPages",
"totaltables" : "totalTables",
"triangulationindicator" : "triangulationIndicator",
"typesystem" : "typeSystem",
"uniquekey" : "uniqueKey",
"unittype" : "unitType",
"unitlist" : "unitList",
"usagecitation" : "usageCitation",
"valueuri" : "valueURI",
"valueattributereference" : "valueAttributeReference",
"verticalaccuracy" : "verticalAccuracy",
"vertcoordsys" : "vertCoordSys",
"virtualmachine" : "virtualMachine",
"wavelengthunits" : "waveLengthUnits",
"whitespace" : "whiteSpace",
"xintercept" : "xIntercept",
"xcoordinate" : "xCoordinate",
"xsi:schemalocation" : "xsi:schemaLocation",
"xslope" : "xSlope",
"ycoordinate" : "yCoordinate",
"yintercept" : "yIntercept",
"yslope" : "ySlope"
}
);
},
/**
* Fetch the EML from the MN object service
* @param {object} [options] - A set of options for this fetch()
* @property {boolean} [options.systemMetadataOnly=false] - If true, only the system metadata will be fetched.
* If false, the system metadata AND EML document will be fetched.
*/
fetch: function(options) {
if( ! options ) var options = {};
//Add the authorization header and other AJAX settings
_.extend(options, MetacatUI.appUserModel.createAjaxSettings(), {dataType: "text"});
// Merge the system metadata into the object first
_.extend(options, {merge: true});
this.fetchSystemMetadata(options);
//If we are retrieving system metadata only, then exit now
if(options.systemMetadataOnly)
return;
//Call Backbone.Model.fetch to retrieve the info
return Backbone.Model.prototype.fetch.call(this, options);
},
/*
Deserialize an EML 2.1.1 XML document
*/
parse: function(response) {
// Save a reference to this model for use in setting the
// parentModel inside anonymous functions
var model = this;
//If the response is XML
if((typeof response == "string") && response.indexOf("<") == 0){
//Look for a system metadata tag and call DataONEObject parse instead
if(response.indexOf("systemMetadata>") > -1)
return DataONEObject.prototype.parse.call(this, response);
response = this.cleanUpXML(response);
response = this.dereference(response);
this.set("objectXML", response);
var emlElement = $($.parseHTML(response)).filter("eml\\:eml");
}
var datasetEl;
if(emlElement[0])
datasetEl = $(emlElement[0]).find("dataset");
if(!datasetEl || !datasetEl.length)
return {};
var emlParties = ["metadataprovider", "associatedparty", "creator", "contact", "publisher"],
emlDistribution = ["distribution"],
emlEntities = ["datatable", "otherentity", "spatialvector", "spatialraster", "storedprocedure", "view"],
emlText = ["abstract", "additionalinfo"],
emlMethods = ["methods"];
var nodes = datasetEl.children(),
modelJSON = {};
for(var i=0; i<nodes.length; i++){
var thisNode = nodes[i];
var convertedName = this.nodeNameMap()[thisNode.localName] || thisNode.localName;
//EML Party modules are stored in EMLParty models
if(_.contains(emlParties, thisNode.localName)){
if(thisNode.localName == "metadataprovider")
var attributeName = "metadataProvider";
else if(thisNode.localName == "associatedparty")
var attributeName = "associatedParty";
else
var attributeName = thisNode.localName;
if(typeof modelJSON[attributeName] == "undefined") modelJSON[attributeName] = [];
modelJSON[attributeName].push(new EMLParty({
objectDOM: thisNode,
parentModel: model,
type: attributeName
}));
}
//EML Distribution modules are stored in EMLDistribution models
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"){
modelJSON.project = new EMLProject({
objectDOM: thisNode,
parentModel: model
});
}
//EML Temporal, Taxonomic, and Geographic Coverage modules are stored in their own models
else if(thisNode.localName == "coverage"){
var temporal = $(thisNode).children("temporalcoverage"),
geo = $(thisNode).children("geographiccoverage"),
taxon = $(thisNode).children("taxonomiccoverage");
if(temporal.length){
modelJSON.temporalCoverage = [];
_.each(temporal, function(t){
modelJSON.temporalCoverage.push(new EMLTemporalCoverage({
objectDOM: t,
parentModel: model
}));
});
}
if(geo.length){
modelJSON.geoCoverage = [];
_.each(geo, function(g){
modelJSON.geoCoverage.push(new EMLGeoCoverage({
objectDOM: g,
parentModel: model
}));
});
}
if(taxon.length){
modelJSON.taxonCoverage = [];
_.each(taxon, function(t){
modelJSON.taxonCoverage.push(new EMLTaxonCoverage({
objectDOM: t,
parentModel: model
}));
});
}
}
//Parse EMLText modules
else if(_.contains(emlText, thisNode.localName)){
if(typeof modelJSON[convertedName] == "undefined") modelJSON[convertedName] = [];
modelJSON[convertedName].push(new EMLText({
objectDOM: thisNode,
parentModel: model
}));
}
else if(_.contains(emlMethods, thisNode.localName)) {
if(typeof modelJSON[thisNode.localName] === "undefined") modelJSON[thisNode.localName] = [];
modelJSON[thisNode.localName] = new EMLMethods({
objectDOM: thisNode,
parentModel: model
});
}
//Parse keywords
else if(thisNode.localName == "keywordset"){
//Start an array of keyword sets
if(typeof modelJSON["keywordSets"] == "undefined") modelJSON["keywordSets"] = [];
modelJSON["keywordSets"].push(new EMLKeywordSet({
objectDOM: thisNode,
parentModel: model
}));
}
//Parse intellectual rights
else if(thisNode.localName == "intellectualrights"){
var value = "";
if($(thisNode).children("para").length == 1)
value = $(thisNode).children("para").first().text().trim();
else
$(thisNode).text().trim();
//If the value is one of our pre-defined options, then add it to the model
//if(_.contains(this.get("intellRightsOptions"), value))
modelJSON["intellectualRights"] = value;
}
//Parse Entities
else if(_.contains(emlEntities, thisNode.localName)){
//Start an array of Entities
if(typeof modelJSON["entities"] == "undefined")
modelJSON["entities"] = [];
//Create the model
var entityModel;
if(thisNode.localName == "otherentity"){
entityModel = new EMLOtherEntity({
objectDOM: thisNode,
parentModel: model
}, {
parse: true
});
} else if ( thisNode.localName == "datatable") {
entityModel = new EMLDataTable({
objectDOM: thisNode,
parentModel: model
}, {
parse: true
});
}
else {
entityModel = new EMLEntity({
objectDOM: thisNode,
parentModel: model,
entityType: "application/octet-stream",
type: thisNode.localName
}, {
parse: true
});
}
modelJSON["entities"].push(entityModel);
}
//Parse dataset-level annotations
else if (thisNode.localName === "annotation") {
if( !modelJSON["annotations"] ) {
modelJSON["annotations"] = new EMLAnnotations();
}
var annotationModel = new EMLAnnotation({
objectDOM: thisNode
}, { parse: true });
modelJSON["annotations"].add(annotationModel);
}
else{
//Is this a multi-valued field in EML?
if(Array.isArray(this.get(convertedName))){
//If we already have a value for this field, then add this value to the array
if(Array.isArray(modelJSON[convertedName]))
modelJSON[convertedName].push(this.toJson(thisNode));
//If it's the first value for this field, then create a new array
else
modelJSON[convertedName] = [this.toJson(thisNode)];
}
else
modelJSON[convertedName] = this.toJson(thisNode);
}
}
return modelJSON;
},
/*
* Retireves the model attributes and serializes into EML XML, to produce the new or modified EML document.
* Returns the EML XML as a string.
*/
serialize: function(){
//Get the EML document
var xmlString = this.get("objectXML"),
html = $.parseHTML(xmlString),
eml = $(html).filter("eml\\:eml"),
datasetNode = $(eml).find("dataset");
//Update the packageId on the eml node with the EML id
$(eml).attr("packageId", this.get("id"));
// Set id attribute on dataset node if needed
if (this.get("xmlID")) {
$(datasetNode).attr("id", this.get("xmlID"));
}
// Set schema version
$(eml).attr("xmlns:eml",
MetacatUI.appModel.get("editorSerializationFormat") ||
"https://eml.ecoinformatics.org/eml-2.2.0");
// Set formatID
this.set("formatId",
MetacatUI.appModel.get("editorSerializationFormat") ||
"https://eml.ecoinformatics.org/eml-2.2.0");
// Ensure xsi:schemaLocation has a value for the current format
eml = this.setSchemaLocation(eml);
var nodeNameMap = this.nodeNameMap();
//Serialize the basic text fields
var basicText = ["alternateIdentifier", "title"];
_.each(basicText, function(fieldName){
var basicTextValues = this.get(fieldName);
if(!Array.isArray(basicTextValues))
basicTextValues = [basicTextValues];
// Remove existing nodes
datasetNode.children(fieldName.toLowerCase()).remove();
// Create new nodes
var nodes = _.map(basicTextValues, function(value) {
if(value){
var node = document.createElement(fieldName.toLowerCase());
$(node).text(value);
return node;
}
else{
return "";
}
});
var insertAfter = this.getEMLPosition(eml, fieldName.toLowerCase());
if(insertAfter){
insertAfter.after(nodes);
}
else{
datasetNode.prepend(nodes);
}
}, this);
// Serialize pubDate
// This one is special because it has a default behavior, unlike
// the others: When no pubDate is set, it should be set to
// the current year
var pubDate = this.get('pubDate');
datasetNode.find('pubdate').remove();
if (pubDate != null && pubDate.length > 0) {
var pubDateEl = document.createElement('pubdate');
$(pubDateEl).text(pubDate);
this.getEMLPosition(eml, 'pubdate').after(pubDateEl);
}
// Serialize the parts of EML that are eml-text modules
var textFields = ["abstract", "additionalInfo"];
_.each(textFields, function(field){
var fieldName = this.nodeNameMap()[field] || field;
// Get the EMLText model
var emlTextModels = Array.isArray(this.get(field)) ? this.get(field) : [this.get(field)];
if( ! emlTextModels.length ) return;
// Get the node from the EML doc
var nodes = datasetNode.find(fieldName);
// Update the DOMs for each model
_.each(emlTextModels, function(thisTextModel, i){
//Don't serialize falsey values
if(!thisTextModel) return;
var node;
//Get the existing node or create a new one
if(nodes.length < i+1){
node = document.createElement(fieldName);
this.getEMLPosition(eml, fieldName).after(node);
}
else {
node = nodes[i];
}
$(node).html( $(thisTextModel.updateDOM() ).html());
}, this);
// Remove the extra nodes
this.removeExtraNodes(nodes, emlTextModels);
}, this);
//Create a <coverage> XML node if there isn't one
if( datasetNode.children('coverage').length === 0 ) {
var coverageNode = $(document.createElement('coverage')),
coveragePosition = this.getEMLPosition(eml, 'coverage');
if(coveragePosition)
coveragePosition.after(coverageNode);
else
datasetNode.append(coverageNode);
}
else{
var coverageNode = datasetNode.children("coverage").first();
}
//Serialize the geographic coverage
if ( typeof this.get('geoCoverage') !== 'undefined' && this.get('geoCoverage').length > 0) {
// Don't serialize if geoCoverage is invalid
var validCoverages = _.filter(this.get('geoCoverage'), function(cov) {
return cov.isValid();
});
//Get the existing geo coverage nodes from the EML
var existingGeoCov = datasetNode.find("geographiccoverage");
//Update the DOM of each model
_.each(validCoverages, function(cov, position){
//Update the existing node if it exists
if(existingGeoCov.length-1 >= position){
$(existingGeoCov[position]).replaceWith(cov.updateDOM());
}
//Or, append new nodes
else{
var insertAfter = existingGeoCov.length? datasetNode.find("geographiccoverage").last() : null;
if(insertAfter)
insertAfter.after(cov.updateDOM());
else
coverageNode.append(cov.updateDOM());
}
}, this);
//Remove existing taxon coverage nodes that don't have an accompanying model
this.removeExtraNodes(datasetNode.find("geographiccoverage"), validCoverages);
}
else{
//If there are no geographic coverages, remove the nodes
coverageNode.children("geographiccoverage").remove();
}
//Serialize the taxonomic coverage
if ( typeof this.get('taxonCoverage') !== 'undefined' && this.get('taxonCoverage').length > 0) {
// Group the taxonomic coverage models into empty and non-empty
var sortedTaxonModels = _.groupBy(this.get('taxonCoverage'), function(t) {
if( _.flatten(t.get('taxonomicClassification')).length > 0 ){
return "notEmpty";
}
else{
return "empty";
}
});
//Get the existing taxon coverage nodes from the EML
var existingTaxonCov = coverageNode.children("taxonomiccoverage");
//Iterate over each taxon coverage and update it's DOM
if(sortedTaxonModels["notEmpty"] && sortedTaxonModels["notEmpty"].length > 0) {
//Update the DOM of each model
_.each(sortedTaxonModels["notEmpty"], function(taxonCoverage, position){
//Update the existing taxonCoverage node if it exists
if(existingTaxonCov.length-1 >= position){
$(existingTaxonCov[position]).replaceWith(taxonCoverage.updateDOM());
}
//Or, append new nodes
else{
coverageNode.append(taxonCoverage.updateDOM());
}
});
//Remove existing taxon coverage nodes that don't have an accompanying model
this.removeExtraNodes(existingTaxonCov, this.get("taxonCoverage"));
}
//If all the taxon coverages are empty, remove the parent taxonomicCoverage node
else if( !sortedTaxonModels["notEmpty"] || sortedTaxonModels["notEmpty"].length == 0 ){
existingTaxonCov.remove();
}
}
//Serialize the temporal coverage
var existingTemporalCoverages = datasetNode.find("temporalcoverage");
//Update the DOM of each model
_.each(this.get("temporalCoverage"), function(temporalCoverage, position){
//Update the existing temporalCoverage node if it exists
if(existingTemporalCoverages.length-1 >= position){
$(existingTemporalCoverages[position]).replaceWith(temporalCoverage.updateDOM());
}
//Or, append new nodes
else{
coverageNode.append(temporalCoverage.updateDOM());
}
});
//Remove existing taxon coverage nodes that don't have an accompanying model
this.removeExtraNodes(existingTemporalCoverages, this.get("temporalCoverage"));
//Remove the temporal coverage if it is empty
if( !coverageNode.children("temporalcoverage").children().length ){
coverageNode.children("temporalcoverage").remove();
}
//Remove the <coverage> node if it's empty
if(coverageNode.children().length == 0){
coverageNode.remove();
}
// Dataset-level annotations
datasetNode.children("annotation").remove();
if( this.get("annotations") ){
this.get("annotations").each(function(annotation) {
if (annotation.isEmpty()) {
return;
}
var after = this.getEMLPosition(eml, "annotation");
$(after).after(annotation.updateDOM());
}, this);
//Since there is at least one annotation, the dataset node needs to have an id attribute.
datasetNode.attr("id", this.getUniqueEntityId(this));
}
//If there is no creator, create one from the user
if(!this.get("creator").length){
var party = new EMLParty({ parentModel: this, type: "creator" });
party.createFromUser();
this.set("creator", [party]);
}
//Serialize the creators
this.serializeParties(eml, "creator");
//Serialize the metadata providers
this.serializeParties(eml, "metadataProvider");
//Serialize the associated parties
this.serializeParties(eml, "associatedParty");
//Serialize the contacts
this.serializeParties(eml, "contact");
//Serialize the publishers
this.serializeParties(eml, "publisher");
// Serialize methods
if(this.get('methods')) {
//If the methods model is empty, remove it from the EML
if( this.get("methods").isEmpty() )
datasetNode.find("methods").remove();
else{
//Serialize the methods model
var methodsEl = this.get('methods').updateDOM();
//If the methodsEl is an empty string or other falsey value, then remove the methods node
if( !methodsEl || !$(methodsEl).children().length ){
datasetNode.find("methods").remove();
}
else{
//Add the <methods> node to the EML
datasetNode.find("methods").detach();
var insertAfter = this.getEMLPosition(eml, "methods");
if(insertAfter)
insertAfter.after(methodsEl);
else
datasetNode.append(methodsEl);
}
}
}
//If there are no methods, then remove the methods nodes
else{
if( datasetNode.find("methods").length > 0 ){
datasetNode.find("methods").remove();
}
}
//Serialize the keywords
this.serializeKeywords(eml, "keywordSets");
//Serialize the intellectual rights
if(this.get("intellectualRights")){
if(datasetNode.find("intellectualRights").length)
datasetNode.find("intellectualRights").html("<para>" + this.get("intellectualRights") + "</para>")
else{
this.getEMLPosition(eml, "intellectualrights").after(
$(document.createElement("intellectualRights"))
.html("<para>" + this.get("intellectualRights") + "</para>"));
}
}
// 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){
datasetNode.find("project").detach();
}
//If there is an EMLProject, update its DOM
if(this.get("project")){
this.getEMLPosition(eml, "project").after(this.get("project").updateDOM());
}
//Get the existing taxon coverage nodes from the EML
var existingEntities = datasetNode.find("otherEntity, dataTable, spatialRaster, spatialVector, storedProcedure, view");
//Serialize the entities
_.each(this.get("entities"), function(entity, position) {
//Update the existing node if it exists
if(existingEntities.length - 1 >= position) {
//Remove the entity from the EML
$(existingEntities[position]).detach();
//Insert it into the correct position
this.getEMLPosition(eml, entity.get("type").toLowerCase()).after(entity.updateDOM()); }
//Or, append new nodes
else {
//Inser the entity into the correct position
this.getEMLPosition(eml, entity.get("type").toLowerCase()).after(entity.updateDOM());
}
}, this);
//Remove extra entities that have been removed
var numExtraEntities = existingEntities.length - this.get("entities").length;
for( var i = (existingEntities.length - numExtraEntities); i<existingEntities.length; i++){
$(existingEntities)[i].remove();
}
//Do a final check to make sure there are no duplicate ids in the EML
var elementsWithIDs = $(eml).find("[id]"),
//Get an array of all the ids in this EML doc
allIDs = _.map(elementsWithIDs, function(el){ return $(el).attr("id") });
//If there is at least one id in the EML...
if(allIDs && allIDs.length){
//Boil the array down to just the unique values
var uniqueIDs = _.uniq(allIDs);
//If the unique array is shorter than the array of all ids,
// then there is a duplicate somewhere
if(uniqueIDs.length < allIDs.length){
//For each element in the EML that has an id,
_.each(elementsWithIDs, function(el){
//Get the id for this element
var id = $(el).attr("id");
//If there is more than one element in the EML with this id,
if( $(eml).find("[id='" + id + "']").length > 1 ){
//And if it is not a unit node, which we don't want to change,
if( !$(el).is("unit") )
//Then change the id attribute to a random uuid
$(el).attr("id", "urn-uuid-" + uuid.v4());
}
});
}
}
//Camel-case the XML
var emlString = "";
_.each(html, function(rootEMLNode){ emlString += this.formatXML(rootEMLNode); }, this);
return emlString;
},
/*
* Given an EML DOM and party type, this function updated and/or adds the EMLParties to the EML
*/
serializeParties: function(eml, type){
//Remove the nodes from the EML for this party type
$(eml).children("dataset").children(type.toLowerCase()).remove();
//Serialize each party of this type
_.each(this.get(type), function(party, i){
//Get the last node of this type to insert after
var insertAfter = $(eml).children("dataset").children(type.toLowerCase()).last();
//If there isn't a node found, find the EML position to insert after
if( !insertAfter.length ) {
insertAfter = this.getEMLPosition(eml, type);
}
//Update the DOM of the EMLParty
var emlPartyDOM = party.updateDOM();
//Make sure we don't insert empty EMLParty nodes into the EML
if( $(emlPartyDOM).children().length ){
//Insert the party DOM at the insert position
if ( insertAfter && insertAfter.length )
insertAfter.after(emlPartyDOM);
//If an insert position still hasn't been found, then just append to the dataset node
else
$(eml).find("dataset").append(emlPartyDOM);
}
}, this);
//Create a certain parties from the current app user if none is given
if(type == "contact" && !this.get("contact").length){
//Get the creators
var creators = this.get("creator"),
contacts = [];
_.each(creators, function(creator){
//Clone the creator model and add it to the contacts array
var newModel = new EMLParty({ parentModel: this });
newModel.set(creator.toJSON());
newModel.set("type", type);
contacts.push(newModel);
}, this);
this.set(type, contacts);
//Call this function again to serialize the new models
this.serializeParties(eml, type);
}
},
serializeKeywords: function(eml) {
// Remove all existing keywordSets before appending
$(eml).find('dataset').find('keywordset').remove();
if (this.get('keywordSets').length == 0) return;
// Create the new keywordSets nodes
var nodes = _.map(this.get('keywordSets'), function(kwd) {
return kwd.updateDOM();
});
this.getEMLPosition(eml, "keywordset").after(nodes);
},
/*
* Remoes nodes from the EML that do not have an accompanying model
* (Were probably removed from the EML by the user during editing)
*/
removeExtraNodes: function(nodes, models){
// Remove the extra nodes
var extraNodes = nodes.length - models.length;
if(extraNodes > 0){
for(var i = models.length; i < nodes.length; i++){
$(nodes[i]).remove();
}
}
},
/*
* Saves the EML document to the server using the DataONE API
*/
save: function(attributes, options){
//Validate before we try anything else
if(!this.isValid()){
this.trigger("invalid");
this.trigger("cancelSave");
return false;
}
else{
this.trigger("valid");
}
this.setFileName();
//Set the upload transfer as in progress
this.set("uploadStatus", "p");
//Reset the draftSaved attribute
this.set("draftSaved", false);
//Create the creator from the current user if none is provided
if(!this.get("creator").length){
var party = new EMLParty({ parentModel: this, type: "creator" });
party.createFromUser();
this.set("creator", [party]);
}
//Create the contact from the current user if none is provided
if(!this.get("contact").length){
var party = new EMLParty({ parentModel: this, type: "contact" });
party.createFromUser();
this.set("contact", [party]);
}
//If this is an existing object and there is no system metadata, retrieve it
if(!this.isNew() && !this.get("sysMetaXML")){
var model = this;
//When the system metadata is fetched, try saving again
var fetchOptions = {
success: function(response){
model.set(DataONEObject.prototype.parse.call(model, response));
model.save(attributes, options);
}
}
//Fetch the system metadata now
this.fetchSystemMetadata(fetchOptions);
return;
}
//Create a FormData object to send data with our XHR
var formData = new FormData();
try{
//Add the identifier to the XHR data
if(this.isNew()){
formData.append("pid", this.get("id"));
}
else{
//Create a new ID
this.updateID();
//Add the ids to the form data
formData.append("newPid", this.get("id"));
formData.append("pid", this.get("oldPid"));
}
//Serialize the EML XML
var xml = this.serialize();
var xmlBlob = new Blob([xml], {type : 'application/xml'});
//Get the size of the new EML XML
this.set("size", xmlBlob.size);
//Get the new checksum of the EML XML
var checksum = md5(xml);
this.set("checksum", checksum);
this.set("checksumAlgorithm", "MD5");
//Create the system metadata XML
var sysMetaXML = this.serializeSysMeta();
//Send the system metadata as a Blob
var sysMetaXMLBlob = 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", sysMetaXMLBlob, "sysmeta");
formData.append("object", xmlBlob);
}
catch(error){
//Reset the identifier since we didn't actually update the object
this.resetID();
this.set("uploadStatus", "e");
this.trigger("error");
this.trigger("cancelSave");
return false;
}
var model = this;
var saveOptions = options || {};
_.extend(saveOptions, {
data : formData,
cache: false,
contentType: false,
dataType: "text",
processData: false,
parse: false,
//Use the URL function to determine the URL
url: this.isNew() ? this.url() : this.url({update: true}),
xhr: function(){
var xhr = new window.XMLHttpRequest();
//Upload progress
xhr.upload.addEventListener("progress", function(evt){
if (evt.lengthComputable) {
var percentComplete = evt.loaded / evt.total * 100;
model.set("uploadProgress", percentComplete);
}
}, false);
return xhr;
},
success: function(model, response, xhr){
model.set("numSaveAttempts", 0);
model.set("uploadStatus", "c");
model.set("sysMetaXML", model.serializeSysMeta());
model.set("oldPid", null);
model.fetch({merge: true, systemMetadataOnly: true});
model.trigger("successSaving", model);
},
error: function(model, response, xhr){
model.set("numSaveAttempts", model.get("numSaveAttempts") + 1);
var numSaveAttempts = model.get("numSaveAttempts");
//Reset the identifier changes
model.resetID();
if( numSaveAttempts < 3 && (response.status == 408 || response.status == 0) ){
//Try saving again in 10, 40, and 90 seconds
setTimeout(function(){
model.save.call(model);
},
(numSaveAttempts * numSaveAttempts) * 10000);
}
else{
model.set("numSaveAttempts", 0);
//Get the error error information
var errorDOM = $($.parseHTML(response.responseText)),
errorContainer = errorDOM.filter("error"),
msgContainer = errorContainer.length? errorContainer.find("description") : errorDOM.not("style, title"),
errorMsg = msgContainer.length? msgContainer.text() : errorDOM;
//When there is no network connection (status == 0), there will be no response text
if(!errorMsg || (response.status == 408 || response.status == 0))
errorMsg = "There was a network issue that prevented your metadata from uploading. " +
"Make sure you are connected to a reliable internet connection.";
//Save the error message in the model
model.set("errorMessage", errorMsg);
//Set the model status as e for error
model.set("uploadStatus", "e");
//Save the EML as a plain text file, until drafts are a supported feature
var copy = model.createTextCopy();
//If the EML copy successfully saved, let the user know that there is a copy saved behind the scenes
model.listenToOnce(copy, "successSaving", function(){
model.set("draftSaved", true);
//Trigger the errorSaving event so other parts of the app know that the model failed to save
//And send the error message with it
model.trigger("errorSaving", errorMsg);
});
//If the EML copy fails to save too, then just display the usual error message
model.listenToOnce(copy, "errorSaving", function(){
//Trigger the errorSaving event so other parts of the app know that the model failed to save
//And send the error message with it
model.trigger("errorSaving", errorMsg);
});
//Save the EML plain text copy
copy.save();
// Track the error
MetacatUI.analytics?.trackException(
`EML save error: ${errorMsg}, EML draft: ${copy.get("id")}`,
model.get("id"),
true
);
}
}
}, MetacatUI.appUserModel.createAjaxSettings());
return Backbone.Model.prototype.save.call(this, attributes, saveOptions);
},
/*
* Checks if this EML model has all the required values necessary to save to the server
*/
validate: function() {
let errors = {};
//A title is always required by EML
if( !this.get("title").length || !this.get("title")[0] ){
errors.title = "A title is required";
}
// Validate the publication date
if (this.get("pubDate") != null) {
if (!this.isValidYearDate(this.get("pubDate"))) {
errors["pubDate"] = ["The value entered for publication date, '"
+ this.get("pubDate") +
"' is not a valid value for this field. Enter with a year (e.g. 2017) or a date in the format YYYY-MM-DD."]
}
}
// Validate the temporal coverage
errors.temporalCoverage = [];
//If temporal coverage is required and there aren't any, return an error
if( MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage &&
!this.get("temporalCoverage").length ){
errors.temporalCoverage = [{ beginDate: "Provide a begin date." }];
}
//If temporal coverage is required and they are all empty, return an error
else if( MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage &&
_.every(this.get("temporalCoverage"), function(tc){
return tc.isEmpty();
}) ){
errors.temporalCoverage = [{ beginDate: "Provide a begin date." }];
}
//If temporal coverage is not required, validate each one
else if( this.get("temporalCoverage").length ||
( MetacatUI.appModel.get("emlEditorRequiredFields").temporalCoverage &&
_.every(this.get("temporalCoverage"), function(tc){
return tc.isEmpty();
}) )) {
//Iterate over each temporal coverage and add it's validation errors
_.each(this.get("temporalCoverage"), function(temporalCoverage){
if( !temporalCoverage.isValid() && !temporalCoverage.isEmpty() ){
errors.temporalCoverage.push(temporalCoverage.validationError);
}
});
}
//Remove the temporalCoverage attribute if no errors were found
if( errors.temporalCoverage.length == 0 ){
delete errors.temporalCoverage;
}
//Validate the EMLParty models
var partyTypes = ["associatedParty", "contact", "creator", "metadataProvider", "publisher"];
_.each(partyTypes, function(type){
var people = this.get(type);
_.each(people, function(person, i){
if( !person.isValid() ){
if( !errors[type] )
errors[type] = [person.validationError];
else
errors[type].push(person.validationError);
}
}, this);
}, this);
//Validate the EMLGeoCoverage models
_.each(this.get("geoCoverage"), function(geoCoverageModel, i){
if( !geoCoverageModel.isValid() ){
if( !errors.geoCoverage )
errors.geoCoverage = [geoCoverageModel.validationError];
else
errors.geoCoverage.push(geoCoverageModel.validationError);
}
}, this);
//Validate the EMLTaxonCoverage model
var taxonModel = this.get("taxonCoverage")[0];
if( !taxonModel.isEmpty() && !taxonModel.isValid() ){
errors = _.extend(errors, taxonModel.validationError);
}
else if( taxonModel.isEmpty() &&
this.get("taxonCoverage").length == 1 &&
MetacatUI.appModel.get("emlEditorRequiredFields").taxonCoverage ){
taxonModel.isValid();
errors = _.extend(errors, taxonModel.validationError);
}
//Validate each EMLEntity model
_.each( this.get("entities"), function(entityModel){
if( !entityModel.isValid() ){
if( !errors.entities )
errors.entities = [entityModel.validationError];
else
errors.entities.push(entityModel.validationError);
}
});
//Validate the EML Methods
let emlMethods = this.get("methods");
if( emlMethods ){
if( !emlMethods.isValid() ){
errors.methods = emlMethods.validationError;
}
}
// Validate each EMLAnnotation model
if( this.get("annotations") ){
this.get("annotations").each(function (model) {
if (model.isValid()) {
return;
}
if (!errors.annotations) {
errors.annotations = [];
}
errors.annotations.push(model.validationError);
}, this);
}
//Check the required fields for this MetacatUI configuration
for([field, isRequired] of Object.entries(MetacatUI.appModel.get("emlEditorRequiredFields"))){
//If it's not required, then go to the next field
if(!isRequired) continue;
if(field == "alternateIdentifier"){
if( !this.get("alternateIdentifier").length || _.every(this.get("alternateIdentifier"), function(altId){ return altId.trim() == "" }) )
errors.alternateIdentifier = "At least one alternate identifier is required."
}
else if(field == "generalTaxonomicCoverage"){
if( !this.get("taxonCoverage").length || !this.get("taxonCoverage")[0].get("generalTaxonomicCoverage") )
errors.generalTaxonomicCoverage = "Provide a description of the general taxonomic coverage of this data set.";
}
else if(field == "geoCoverage"){
if(!this.get("geoCoverage").length)
errors.geoCoverage = "At least one location is required.";
}
else if(field == "intellectualRights"){
if( !this.get("intellectualRights") )
errors.intellectualRights = "Select usage rights for this data set.";
}
else if(field == "studyExtentDescription"){
if( !this.get("methods") || !this.get("methods").get("studyExtentDescription") )
errors.studyExtentDescription = "Provide a study extent description.";
}
else if(field == "samplingDescription"){
if( !this.get("methods") || !this.get("methods").get("samplingDescription") )
errors.samplingDescription = "Provide a sampling description.";
}
else if(field == "temporalCoverage"){
if(!this.get("temporalCoverage").length)
errors.temporalCoverage = "Provide the date(s) for this data set.";
}
else if(field == "taxonCoverage"){
if(!this.get("taxonCoverage").length)
errors.taxonCoverage = "At least one taxa rank and value is required.";
}
else if(field == "keywordSets"){
if( !this.get("keywordSets").length )
errors.keywordSets = "Provide at least one keyword.";
}
//The EMLMethods model will validate itself for required fields, but
// this is a rudimentary check to make sure the EMLMethods model was created
// in the first place
else if(field == "methods"){
if(!this.get("methods"))
errors.methods = "At least one method step is required.";
}
else if(field == "funding"){
// Note: Checks for either the funding or award element. award
// element is checked by the project's objectDOM for now until
// EMLProject fully supports the award element
if(!this.get("project") ||
!(this.get("project").get("funding").length ||
(this.get("project").get("objectDOM") &&
this.get("project").get("objectDOM").querySelectorAll &&
this.get("project").get("objectDOM").querySelectorAll("award").length > 0)))
errors.funding = "Provide at least one project funding number or name.";
}
else if(field == "abstract"){
if(!this.get("abstract").length)
errors["abstract"] = "Provide an abstract.";
}
else if(field == "dataSensitivity"){
if( !this.getDataSensitivity() ){
errors["dataSensitivity"] = "Pick the category that best describes the level of sensitivity or restriction of the data.";
}
}
//If this is an EMLParty type, check that there is a party of this type in the model
else if( EMLParty.prototype.partyTypes.map(t=>t.dataCategory).includes(field) ){
//If this is an associatedParty role
if( EMLParty.prototype.defaults().roleOptions?.includes(field) ){
if(!this.get("associatedParty")?.map(p=>p.get("roles")).flat().includes(field)){
errors[field] = "Provide information about the people or organization(s) in the role: " +
EMLParty.prototype.partyTypes.find(t=>t.dataCategory==field)?.label;
}
}
else if( !this.get(field)?.length ){
errors[field] = "Provide information about the people or organization(s) in the role: " +
EMLParty.prototype.partyTypes.find(t=>t.dataCategory==field)?.label;
}
}
else if( !this.get(field) || !this.get(field)?.length ){
errors[field] = "Provide a " + field + ".";
}
}
if( Object.keys(errors).length )
return errors;
else{
return;
}
},
/* Returns a boolean for whether the argument 'value' is a valid
value for EML's yearDate type which is used in a few places.
Note that this method considers a zero-length String to be valid
because the EML211.serialize() method will properly handle a null
or zero-length String by serializing out the current year. */
isValidYearDate: function(value) {
return (value === "" || /^\d{4}$/.test(value) || /^\d{4}-\d{2}-\d{2}$/.test(value));
},
/*
* Sends an AJAX request to fetch the system metadata for this EML object.
* Will not trigger a sync event since it does not use Backbone.Model.fetch
*/
fetchSystemMetadata: function(options){
if(!options) var options = {};
else options = _.clone(options);
var model = this,
fetchOptions = _.extend({
url: MetacatUI.appModel.get("metaServiceUrl") + encodeURIComponent(this.get("id")),
dataType: "text",
success: function(response){
model.set(DataONEObject.prototype.parse.call(model, response));
//Trigger a custom event that the sys meta was updated
model.trigger("sysMetaUpdated");
},
error: function(){
model.trigger('error');
}
}, options);
//Add the authorization header and other AJAX settings
_.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings());
$.ajax(fetchOptions);
},
/*
* Returns the nofde in the given EML document that the given node type
* should be inserted after
*
* Returns false if either the node is not found in the and this should
* be handled by the caller.
*/
getEMLPosition: function(eml, nodeName) {
var nodeOrder = this.get("nodeOrder");
var position = _.indexOf(nodeOrder, nodeName.toLowerCase());
if (position == -1) {
return false;
}
// 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 ($(eml).find("dataset").children(nodeOrder[i]).length) {
return $(eml).find("dataset").children(nodeOrder[i]).last();
}
}
return false;
},
/*
* Checks if this model has updates that need to be synced with the server.
*/
hasUpdates: function(){
if(this.constructor.__super__.hasUpdates.call(this)) return true;
//If nothing else has been changed, then this object hasn't had any updates
return false;
},
/*
Add an entity into the EML 2.1.1 object
*/
addEntity: function(emlEntity, position) {
//Get the current list of entities
var currentEntities = this.get("entities");
if( typeof position == "undefined" || position == -1)
currentEntities.push(emlEntity);
else
//Add the entity model to the entity array
currentEntities.splice(position, 0, emlEntity);
this.trigger("change:entities");
this.trickleUpChange();
return this;
},
/*
Remove an entity from the EML 2.1.1 object
*/
removeEntity: function(emlEntity) {
if(!emlEntity || typeof emlEntity != "object")
return;
//Get the current list of entities
var entities = this.get("entities");
entities = _.without(entities, emlEntity);
this.set("entities", entities);
},
/*
* Find the entity model for a given DataONEObject
*/
getEntity: function(dataONEObj){
//If an EMLEntity model has been found for this object before, then return it
if( dataONEObj.get("metadataEntity") ){
dataONEObj.get("metadataEntity").set("dataONEObject", dataONEObj);
return dataONEObj.get("metadataEntity");
}
var entity = _.find(this.get("entities"), function(e){
//Matches of the checksum or identifier are definite matches
if( e.get("xmlID") == dataONEObj.getXMLSafeID() )
return true;
else if( e.get("physicalMD5Checksum") && (e.get("physicalMD5Checksum") == dataONEObj.get("checksum") && dataONEObj.get("checksumAlgorithm").toUpperCase() == "MD5"))
return true;
else if(e.get("downloadID") && e.get("downloadID") == dataONEObj.get("id"))
return true;
// Get the file name from the EML for this entity
var fileNameFromEML = e.get("physicalObjectName") || e.get("entityName");
// If the EML file name matches the DataONEObject file name
if (fileNameFromEML &&
dataONEObj.get("fileName") &&
((fileNameFromEML.toLowerCase() == dataONEObj.get("fileName").toLowerCase()) ||
(fileNameFromEML.replace(/ /g, "_").toLowerCase() == dataONEObj.get("fileName").toLowerCase()))) {
//Get an array of all the other entities in this EML
var otherEntities = _.without(this.get("entities"), e);
// If this entity name matches the dataone object file name, AND no other dataone object file name
// matches, then we can assume this is the entity element for this file.
var otherMatchingEntity = _.find(otherEntities, function(otherE){
// Get the file name from the EML for the other entities
var otherFileNameFromEML = otherE.get("physicalObjectName") || otherE.get("entityName");
// If the file names match, return true
if( (otherFileNameFromEML == dataONEObj.get("fileName")) || (otherFileNameFromEML.replace(/ /g, "_") == dataONEObj.get("fileName")) )
return true;
});
// If this entity's file name didn't match any other file names in the EML,
// then this entity is a match for the given dataONEObject
if( !otherMatchingEntity )
return true;
}
}, this);
//If we found an entity, give it an ID and return it
if(entity){
//If this entity has been matched to another DataONEObject already, then don't match it again
if( entity.get("dataONEObject") == dataONEObj ){
return entity;
}
//If this entity has been matched to a different DataONEObject already, then don't match it again.
//i.e. We will not override existing entity<->DataONEObject pairings
else if( entity.get("dataONEObject") ){
return;
}
else{
entity.set("dataONEObject", dataONEObj);
}
//Create an XML-safe ID and set it on the Entity model
var entityID = this.getUniqueEntityId(dataONEObj);
entity.set("xmlID", entityID);
//Save a reference to this entity so we don't have to refind it later
dataONEObj.set("metadataEntity", entity);
return entity;
}
//See if one data object is of this type in the package
var matchingTypes = _.filter(this.get("entities"), function(e){
return (e.get("formatName") == (dataONEObj.get("formatId") || dataONEObj.get("mediaType")));
});
if(matchingTypes.length == 1){
//Create an XML-safe ID and set it on the Entity model
matchingTypes[0].set("xmlID", dataONEObj.getXMLSafeID());
return matchingTypes[0];
}
//If this EML is in a DataPackage with only one other DataONEObject,
// and there is only one entity in the EML, then we can assume they are the same entity
if( this.get("entities").length == 1 ){
if( this.get("collections")[0] && this.get("collections")[0].type == "DataPackage" &&
this.get("collections")[0].length == 2 && _.contains(this.get("collections")[0].models, dataONEObj)){
return this.get("entities")[0];
}
}
return false;
},
createEntity: function(dataONEObject){
// Add or append an entity to the parent's entity list
var entityModel = new EMLOtherEntity({
entityName : dataONEObject.get("fileName"),
entityType : dataONEObject.get("formatId") ||
dataONEObject.get("mediaType") ||
"application/octet-stream",
dataONEObject: dataONEObject,
parentModel: this,
xmlID: dataONEObject.getXMLSafeID()
});
this.addEntity(entityModel);
//If this DataONEObject fails to upload, remove the EML entity
this.listenTo(dataONEObject, "errorSaving", function(){
this.removeEntity(dataONEObject.get("metadataEntity"));
//Listen for a successful save so the entity can be added back
this.listenToOnce(dataONEObject, "successSaving", function(){
this.addEntity(dataONEObject.get("metadataEntity"))
});
});
},
/*
* Creates an XML-safe identifier that is unique to this EML document,
* based on the given DataONEObject model. It is intended for EML entity nodes in particular.
*
* @param {DataONEObject} - a DataONEObject model that this EML documents
* @return {string} - an identifier string unique to this EML document
*/
getUniqueEntityId: function(dataONEObject){
var uniqueId = "";
uniqueId = dataONEObject.getXMLSafeID();
//Get the EML string, if there is one, to check if this id already exists
var emlString = this.get("objectXML");
//If this id already exists in the EML...
if(emlString && emlString.indexOf(' id="' + uniqueId + '"')){
//Create a random uuid to use instead
uniqueId = "urn-uuid-" + uuid.v4();
}
return uniqueId;
},
/*
* removeParty - removes the given EMLParty model from this EML211 model's attributes
*/
removeParty: function(partyModel){
//The list of attributes this EMLParty might be stored in
var possibleAttr = ["creator", "contact", "metadataProvider", "publisher", "associatedParty"];
// Iterate over each possible attribute
_.each(possibleAttr, function(attr){
if( _.contains(this.get(attr), partyModel) ){
this.set( attr, _.without(this.get(attr), partyModel) );
}
}, this);
},
/**
* Attempt to move a party one index forward within its sibling models
*
* @param {EMLParty} partyModel: The EMLParty model we're moving
*/
movePartyUp: function(partyModel) {
var possibleAttr = ["creator", "contact", "metadataProvider", "publisher", "associatedParty"];
// Iterate over each possible attribute
_.each(possibleAttr, function(attr){
if (!_.contains(this.get(attr), partyModel)) {
return;
}
// Make a clone because we're going to use splice
var models = _.clone(this.get(attr));
// Find the index of the model we're moving
var index = _.findIndex(models, function(m) {
return m === partyModel;
});
if (index === 0) {
// Already first
return;
}
if (index === -1) {
// Couldn't find the model
return;
}
// Do the move using splice and update the model
models.splice(index - 1, 0, models.splice(index, 1)[0])
this.set(attr, models);
this.trigger("change:" + attr);
}, this);
},
/**
* Attempt to move a party one index forward within its sibling models
*
* @param {EMLParty} partyModel: The EMLParty model we're moving
*/
movePartyDown: function(partyModel) {
var possibleAttr = ["creator", "contact", "metadataProvider", "publisher", "associatedParty"];
// Iterate over each possible attribute
_.each(possibleAttr, function(attr){
if (!_.contains(this.get(attr), partyModel)) {
return;
}
// Make a clone because we're going to use splice
var models = _.clone(this.get(attr));
// Find the index of the model we're moving
var index = _.findIndex(models, function(m) {
return m === partyModel;
});
if (index === -1) {
// Couldn't find the model
return;
}
// Figure out where to put the new model
// Leave it in the same place if the next index doesn't exist
// Move one forward if it does
var newIndex = (models.length <= index + 1) ? index : index + 1;
// Do the move using splice and update the model
models.splice(newIndex, 0, models.splice(index, 1)[0])
this.set(attr, models);
this.trigger("change:" + attr);
}, this);
},
/*
* Adds the given EMLParty model to this EML211 model in the
* appropriate role array in the given position
*
* @param {EMLParty} - The EMLParty model to add
* @param {number} - The position in the role array in which to insert this EMLParty
* @return {boolean} - Returns true if the EMLParty was successfully added, false if it was cancelled
*/
addParty: function(partyModel, position){
//If the EMLParty model is empty, don't add it to the EML211 model
if(partyModel.isEmpty())
return false;
//Get the role of this EMLParty
var role = partyModel.get("type") || "associatedParty";
//If this model already contains this EMLParty, then exit
if( _.contains(this.get(role), partyModel) )
return false;
if( typeof position == "undefined" ){
this.get(role).push(partyModel);
}
else {
this.get(role).splice(position, 0, partyModel);
}
this.trigger("change:" + role);
return true;
},
/**
* getPartiesByType - Gets an array of EMLParty members that have a particular party type or role.
* @param {string} partyType - A string that represents either the role or the party type. For example, "contact", "creator", "principalInvestigator", etc.
* @since 2.15.0
*/
getPartiesByType: function(partyType){
try {
if(!partyType){
return false
}
var associatedPartyTypes = new EMLParty().get("roleOptions"),
isAssociatedParty = associatedPartyTypes.includes(partyType),
parties = [];
// For "contact", "creator", "metadataProvider", "publisher", each party type has it's own
// array in the EML model
if(!isAssociatedParty){
parties = this.get(partyType);
// For "custodianSteward", "principalInvestigator", "collaboratingPrincipalInvestigator", etc.,
// party members are listed in the EML model's associated parties array. Each associated party's
// party type is indicated in the role attribute.
} else {
parties = _.filter(this.get("associatedParty"), function (associatedParty) {
return associatedParty.get("roles").includes(partyType) }
);
}
return parties;
} catch (error) {
console.log("Error trying to find a list of party members in an EML model by type. Error details: " + error);
}
},
createUnits: function(){
this.units.fetch();
},
/* Initialize the object XML for brand spankin' new EML objects */
createXML: function() {
let emlSystem = MetacatUI.appModel.get("emlSystem");
emlSystem = (!emlSystem || typeof emlSystem != "string") ? "knb" : emlSystem;
var xml = "<eml:eml xmlns:eml=\"https://eml.ecoinformatics.org/eml-2.2.0\"></eml:eml>",
eml = $($.parseHTML(xml));
// Set base attributes
eml.attr("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
eml.attr("xmlns:stmml", "http://www.xml-cml.org/schema/stmml-1.1");
eml.attr("xsi:schemaLocation", "https://eml.ecoinformatics.org/eml-2.2.0 https://eml.ecoinformatics.org/eml-2.2.0/eml.xsd");
eml.attr("packageId", this.get("id"));
eml.attr("system", emlSystem);
// Add the dataset
eml.append(document.createElement("dataset"));
eml.find("dataset").append(document.createElement("title"));
var emlString = $(document.createElement("div")).append(eml.clone()).html();
return emlString;
},
/*
Replace elements named "source" with "sourced" due to limitations
with using $.parseHTML() rather than $.parseXML()
@param xmlString The XML string to make the replacement in
*/
cleanUpXML: function(xmlString){
xmlString.replace("<source>", "<sourced>");
xmlString.replace("</source>", "</sourced>");
return xmlString;
},
createTextCopy: function(){
var emlDraftText = "EML draft for " + this.get("id") + "(" + this.get("title") + ") by " +
MetacatUI.appUserModel.get("firstName") + " " + MetacatUI.appUserModel.get("lastName");
if(this.get("uploadStatus") == "e" && this.get("errorMessage")){
emlDraftText += ". This EML had the following save error: `" + this.get("errorMessage") + "` ";
}
else {
emlDraftText += ": ";
}
emlDraftText += this.serialize();
var plainTextEML = new DataONEObject({
formatId: "text/plain",
fileName: "eml_draft_" + (MetacatUI.appUserModel.get("lastName") || "") + ".txt",
uploadFile: new Blob([emlDraftText], {type : 'plain/text'}),
synced: true
});
return plainTextEML;
},
/*
* Cleans up the given text so that it is XML-valid by escaping reserved characters, trimming white space, etc.
*
* @param {string} textString - The string to clean up
* @return {string} - The cleaned up string
*/
cleanXMLText: function(textString){
if( typeof textString != "string" )
return;
textString = textString.trim();
//Check for XML/HTML elements
_.each(textString.match(/<\s*[^>]*>/g), function(xmlNode){
//Encode <, >, and </ substrings
var tagName = xmlNode.replace(/>/g, ">");
tagName = tagName.replace(/</g, "<");
//Replace the xmlNode in the full text string
textString = textString.replace(xmlNode, tagName);
});
//Remove Unicode characters that are not valid XML characters
//Create a regular expression that matches any character that is not a valid XML character
// (see https://www.w3.org/TR/xml/#charsets)
var invalidCharsRegEx = /[^\u0009\u000a\u000d\u0020-\uD7FF\uE000-\uFFFD]/g;
textString = textString.replace(invalidCharsRegEx, "");
return textString;
},
/*
Dereference "reference" elements and replace them with a cloned copy
of the referenced content
@param xmlString The XML string with reference elements to transform
*/
dereference: function(xmlString) {
var referencesList; // the array of references elements in the document
var referencedID; // The id of the referenced element
var referencesParentEl; // The parent of the given references element
var referencedEl; // The referenced DOM to be copied
var xmlDOM = $.parseXML(xmlString);
referencesList = xmlDOM.getElementsByTagName("references");
if (referencesList.length) {
// Process each references elements
_.each(referencesList, function(referencesEl, index, referencesList) {
// Can't rely on the passed referencesEl since the list length changes
// because of the remove() below. Reuse referencesList[0] for every item:
// referencedID = $(referencesEl).text(); // doesn't work
referencesEl = referencesList[0];
referencedID = $(referencesEl).text();
referencesParentEl = ($(referencesEl).parent())[0];
if (typeof referencedID !== "undefined" && referencedID != "") {
referencedEl = xmlDOM.getElementById(referencedID);
if (typeof referencedEl != "undefined") {
// Clone the referenced element and replace the references element
var referencedClone = ($(referencedEl).clone())[0];
$(referencesParentEl)
.children(referencesEl.localName)
.replaceWith($(referencedClone).children());
//$(referencesParentEl).append($(referencedClone).children());
$(referencesParentEl).attr("id", DataONEObject.generateId());
}
}
}, xmlDOM);
}
return (new XMLSerializer()).serializeToString(xmlDOM);
},
/*
* Uses the EML `title` to set the `fileName` attribute on this model.
*/
setFileName: function(){
var title = "";
// Get the title from the metadata
if( Array.isArray(this.get("title")) ){
title = this.get("title")[0];
}
else if( typeof this.get("title") == "string" ){
title = this.get("title");
}
//Max title length
var maxLength = 50;
//trim the string to the maximum length
var trimmedTitle = title.trim().substr(0, maxLength);
//re-trim if we are in the middle of a word
if( trimmedTitle.indexOf(" ") > -1 ){
trimmedTitle = trimmedTitle.substr(0, Math.min(trimmedTitle.length, trimmedTitle.lastIndexOf(" ")));
}
//Replace all non alphanumeric characters with underscores
// and make sure there isn't more than one underscore in a row
trimmedTitle = trimmedTitle.replace(/[^a-zA-Z0-9]/g, "_").replace(/_{2,}/g, "_");
//Set the fileName on the model
this.set("fileName", trimmedTitle + ".xml");
},
trickleUpChange: function(){
if( !MetacatUI.rootDataPackage || !MetacatUI.rootDataPackage.packageModel )
return;
//Mark the package as changed
MetacatUI.rootDataPackage.packageModel.set("changed", true);
},
/**
* Sets the xsi:schemaLocation attribute on the passed-in Element
* depending on the application configuration.
*
* @param {Element} eml: The root eml:eml element to modify
* @return {Element} The element, possibly modified
*/
setSchemaLocation: function(eml) {
if (!MetacatUI || !MetacatUI.appModel) {
return eml;
}
var current = $(eml).attr("xsi:schemaLocation"),
format = MetacatUI.appModel.get("editorSerializationFormat"),
location = MetacatUI.appModel.get("editorSchemaLocation");
// Return now if we can't do anything anyway
if (!format || !location) {
return eml;
}
// Simply add if the attribute isn't present to begin with
if (!current || typeof current !== "string") {
$(eml).attr("xsi:schemaLocation", format + " " + location);
return eml;
}
// Don't append if it's already present
if (current.indexOf(format) >= 0) {
return eml;
}
$(eml).attr("xsi:schemaLocation", current + " " + location);
return eml;
},
createID: function() {
this.set("xmlID", uuid.v4());
},
/**
* Creates and adds an {@link EMLAnnotation} to this EML211 model with the given annotation data in JSON form.
* @param {object} annotationData The attribute data to set on the new {@link EMLAnnotation}. See {@link EMLAnnotation#defaults} for
* details on what attributes can be passed to the EMLAnnotation. In addition, there is an `elementName` property.
* @property {string} [annotationData.elementName] The name of the EML Element that this
annotation should be applied to. e.g. dataset, entity, attribute. Defaults to `dataset`. NOTE: Right now only dataset annotations are supported until
more annotation editing is added to the EML Editor.
* @property {Boolean} [annotationData.allowDuplicates] If false, this annotation will replace all annotations already set with the same propertyURI.
* By default, more than one annotation with a given propertyURI can be added (defaults to true)
*/
addAnnotation: function(annotationData){
try{
if( !annotationData || typeof annotationData != "object" ){
return;
}
//If no element name is provided, default to the dataset element.
let elementName = "";
if( !annotationData.elementName ){
elementName = "dataset"
}
else{
elementName = annotationData.elementName;
}
//Remove the elementName property so it isn't set on the EMLAnnotation model later.
delete annotationData.elementName;
//Check if duplicates are allowed
let allowDuplicates = annotationData.allowDuplicates;
delete annotationData.allowDuplicates;
//Create a new EMLAnnotation model
let annotation = new EMLAnnotation(annotationData);
//Update annotations set on the dataset element
if( elementName == "dataset" ){
let annotations = this.get("annotations");
//If the current annotations set on the EML model are not in Array form, change it to an array
if( !annotations ){
annotations = new EMLAnnotations();
}
if( allowDuplicates === false ){
//Add the EMLAnnotation to the collection, making sure to remove duplicates first
annotations.replaceDuplicateWith(annotation);
}
else{
annotations.add(annotation);
}
//Set the annotations and force the change to be recognized by the model
this.set("annotations", annotations, {silent: true});
this.handleChange(this, { force: true });
}
else{
/** @todo Add annotation support for other EML Elements */
}
}
catch(e){
console.error("Could not add Annotation to the EML: ", e);
}
},
/**
* Finds annotations that are of the `data sensitivity` property from the NCEAS SENSO ontology.
* Returns undefined if none are found. This function returns EMLAnnotation models because the data
* sensitivity is stored in the EML Model as EMLAnnotations and added to EML as semantic annotations.
* @returns {EMLAnnotation[]|undefined}
*/
getDataSensitivity: function(){
try{
let annotations = this.get("annotations");
if(annotations){
let found = annotations.where({ propertyURI: this.get("dataSensitivityPropertyURI") });
if( !found || !found.length ){
return;
}
else{
return found;
}
}
else{
return;
}
}
catch(e){
console.error("Failed to get Data Sensitivity from EML model: ", e);
return;
}
}
});
return EML211;
}
);