diff --git a/src/js/models/AppModel.js b/src/js/models/AppModel.js index bab9d6b1c..593e40a6a 100644 --- a/src/js/models/AppModel.js +++ b/src/js/models/AppModel.js @@ -439,10 +439,14 @@ define(["jquery", "underscore", "backbone"], function ($, _, Backbone) { editorSerializationFormat: "https://eml.ecoinformatics.org/eml-2.2.0", /** - * The XML schema location the dataset editor will use when creating new EML. This should - * correspond with {@link AppConfig#editorSerializationFormat} - * @type {string} - * @default "https://eml.ecoinformatics.org/eml-2.2.0 https://eml.ecoinformatics.org/eml-2.2.0/eml.xsd" + * The XML schema location the dataset editor will use when creating + * new EML. This should correspond with + * {@link AppConfig#editorSerializationFormat}. Note there must be an + * even number of values in this string, with each pair consisting of + * a namespace URI and the schema location URL for that namespace. + * @type {string} + * @default "https://eml.ecoinformatics.org/eml-2.2.0 + * https://eml.ecoinformatics.org/eml-2.2.0/eml.xsd" * @readonly * @since 2.13.0 */ diff --git a/src/js/models/metadata/eml211/EML211.js b/src/js/models/metadata/eml211/EML211.js index 2301e747f..be0cc5954 100644 --- a/src/js/models/metadata/eml211/EML211.js +++ b/src/js/models/metadata/eml211/EML211.js @@ -2167,10 +2167,7 @@ define([ // 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", - ); + this.setSchemaLocation(eml); eml.attr("packageId", this.get("id")); eml.attr("system", emlSystem); @@ -2373,33 +2370,31 @@ define([ * @returns {Element} The element, possibly modified */ setSchemaLocation(eml) { - if (!MetacatUI || !MetacatUI.appModel) { - return eml; - } + if (!MetacatUI || !MetacatUI.appModel) return eml; - const current = $(eml).attr("xsi:schemaLocation"); - const format = MetacatUI.appModel.get("editorSerializationFormat"); const location = MetacatUI.appModel.get("editorSchemaLocation"); // Return now if we can't do anything anyway - if (!format || !location) { - return eml; - } + if (!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) { + const format = MetacatUI.appModel.get("editorSerializationFormat"); + const current = $(eml).attr("xsi:schemaLocation")?.trim(); + const isString = current && typeof current === "string"; + // Must be even: a namespace URI plus the schema location + const hasEvenNumber = current?.split(/\s+/).length % 2 === 0 || false; + + // Don't append if it's already present and valid + if (isString && hasEvenNumber && current?.includes(format)) return eml; + + // If there is already a valid location but is missing this format, + // append it + if (current && isString && hasEvenNumber) { + $(eml).attr("xsi:schemaLocation", `${current} ${location}`); return eml; } - $(eml).attr("xsi:schemaLocation", `${current} ${location}`); - + // In all other cases, set the schema location to just this format + $(eml).attr("xsi:schemaLocation", location); return eml; }, diff --git a/test/js/specs/unit/models/metadata/eml211/EML211.spec.js b/test/js/specs/unit/models/metadata/eml211/EML211.spec.js index 9678fe9b5..3c94392a3 100644 --- a/test/js/specs/unit/models/metadata/eml211/EML211.spec.js +++ b/test/js/specs/unit/models/metadata/eml211/EML211.spec.js @@ -44,5 +44,88 @@ define([ .should.be.instanceof(EMLTaxonCoverage); }); }); + + describe("setSchemaLocation", function () { + const schemaLocation = + "https://example.org/eml-2.2.0 https://example.org/eml-2.2.0/eml.xsd"; + const serializationFormat = "https://example.org/eml-2.2.0"; + let originalMetacatUI; + + function mockMetacatUI(overrides = {}) { + window.MetacatUI = { + appModel: { + get: (attr) => { + if (overrides[attr]) { + return overrides[attr]; + } + if (attr === "editorSchemaLocation") return schemaLocation; + if (attr === "editorSerializationFormat") + return serializationFormat; + return undefined; + }, + }, + }; + } + + function createEmlElement(initialLocation) { + const element = document.createElement("eml"); + if (initialLocation) { + $(element).attr("xsi:schemaLocation", initialLocation); + } + return element; + } + + beforeEach(function () { + originalMetacatUI = window.MetacatUI; + mockMetacatUI(); + }); + + afterEach(function () { + window.MetacatUI = originalMetacatUI; + }); + + it("sets the schema location when missing", function () { + const emlElement = createEmlElement(); + const updated = state.eml.setSchemaLocation(emlElement); + + expect(updated).to.equal(emlElement); + expect($(emlElement).attr("xsi:schemaLocation")).to.equal( + schemaLocation, + ); + }); + + it("appends the configured schema location when other valid entries exist", function () { + const existing = + "http://existing.org/ns http://existing.org/schema.xsd"; + const emlElement = createEmlElement(existing); + + state.eml.setSchemaLocation(emlElement); + + expect($(emlElement).attr("xsi:schemaLocation")).to.equal( + `${existing} ${schemaLocation}`, + ); + }); + + it("does not modify the schema location when the format is already present", function () { + const emlElement = createEmlElement(schemaLocation); + + state.eml.setSchemaLocation(emlElement); + + expect($(emlElement).attr("xsi:schemaLocation")).to.equal( + schemaLocation, + ); + }); + + it("overwrites the schema location when existing is invalid", function () { + const invalidLocation = "invalid-schema-location"; + const emlElement = createEmlElement(invalidLocation); + + state.eml.setSchemaLocation(emlElement); + + expect($(emlElement).attr("xsi:schemaLocation")).to.equal( + schemaLocation, + ); + }); + }); }); });