Source: src/js/models/metadata/eml211/EML211.js

/* 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, "&gt;");
          tagName = tagName.replace(/</g, "&lt;");

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