/* 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'], function($, _, Backbone, uuid, Units, ScienceMetadata, DataONEObject, EMLGeoCoverage, EMLKeywordSet, EMLTaxonCoverage, EMLTemporalCoverage, EMLDistribution, EMLEntity, EMLDataTable, EMLOtherEntity, EMLParty, EMLProject, EMLText, EMLMethods) { /** * @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/.", onlineDist: [], // array of EMLOnlineDist objects offlineDist: [], // array of EMLOfflineDist 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: null, // An EMLMethods objects project: null, // An EMLProject object 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 */ 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.sysMeta) 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"], emlText = ["abstract", "additionalinfo"], emlMethods = ["methods"]; var nodes = datasetEl.children(), modelJSON = {}; for(var i=0; i 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 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 node if it's empty if(coverageNode.children().length == 0){ coverageNode.remove(); } //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 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("" + this.get("intellectualRights") + "") else{ this.getEMLPosition(eml, "intellectualrights").after( $(document.createElement("intellectualRights")) .html("" + this.get("intellectualRights") + "")); } } //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"); //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 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, sysMeta: 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(); //Send this exception to Google Analytics if(MetacatUI.appModel.get("googleAnalyticsKey") && (typeof ga !== "undefined")){ ga('send', 'exception', { 'exDescription': "EML save error: " + errorMsg + " | Id: " + model.get("id") + " | v. " + MetacatUI.metacatUIVersion + " | EML draft: " + copy.get("id"), 'exFatal': 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() { var 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); } }); //Check the required fields for this MetacatUI configuration if(MetacatUI.appModel.get("emlEditorRequiredFields")){ _.each(Object.keys(MetacatUI.appModel.get("emlEditorRequiredFields")), function(key){ var isRequired = MetacatUI.appModel.get("emlEditorRequiredFields")[key]; //If it's not required, then exit if(!isRequired) return; if(key == "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(key == "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(key == "geoCoverage"){ if(!this.get("geoCoverage").length) errors.geoCoverage = "At least one location is required."; } else if(key == "intellectualRights"){ if( !this.get("intellectualRights") ) errors.intellectualRights = "Select usage rights for this data set."; } else if(key == "studyExtentDescription"){ if( !this.get("methods") || !this.get("methods").get("studyExtentDescription") ) errors.studyExtentDescription = "Provide a study extent description."; } else if(key == "samplingDescription"){ if( !this.get("methods") || !this.get("methods").get("samplingDescription") ) errors.samplingDescription = "Provide a sampling description."; } else if(key == "temporalCoverage"){ if(!this.get("temporalCoverage").length) errors.temporalCoverage = "Provide the date(s) for this data set."; } else if(key == "taxonCoverage"){ if(!this.get("taxonCoverage").length) errors.taxonCoverage = "At least one taxa rank and value is required."; } else if(key == "keywordSets"){ if( !this.get("keywordSets").length ) errors.keywordSets = "Provide at least one keyword."; } else if(key == "methods"){ if(!this.get("methods")) errors.methods = "At least one method step is required."; } else if(key == "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(key == "abstract"){ if(!this.get("abstract").length) errors["abstract"] = "Provide an abstract."; } else if( !this.get(key) || (Array.isArray(this.get(key)) && !this.get(key).length) ){ errors[key] = "Provide a " + key + "."; } }, this); } 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)); }, 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").find(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 && ((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; }, createUnits: function(){ this.units.fetch(); }, /* Initialize the object XML for brand spankin' new EML objects */ createXML: function() { var xml = "", 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", "knb"); // We could make this configurable at some point // Add the dataset eml.append(document.createElement("dataset")); eml.find("dataset").append(document.createElement("title")); 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("", ""); xmlString.replace("", ""); 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 /g, ">"); tagName = tagName.replace(/ -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; } }); return EML211; } );