Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 43 additions & 27 deletions serverless/src/shared/Iso19115DomEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,34 @@ const blockScheme = (config) => (editor, correction) => editor.updateBlockNode(c
*/
const leafScheme = (config) => (editor, correction) => editor.updateLeafNode(correction, config)

/**
* Builds an ISO keyword path, using NONE only for gaps before a later populated level.
*
* @param {string[]} fieldKeys Ordered keyword hierarchy fields.
* @param {Object} keywordObject Keyword values keyed by hierarchy field.
* @returns {string} Serialized ISO keyword path.
*/
const buildKeywordPath = (fieldKeys, keywordObject = {}) => {
const values = fieldKeys.map((key) => String(keywordObject[key] || '').trim())
const lastValueIndex = values.reduce(
(lastIndex, value, index) => (value ? index : lastIndex),
-1
)

return values
.slice(0, lastValueIndex + 1)
.map((value) => value || 'NONE')
.join(' > ')
}

/**
* Factory to generate standardized keyword block editors.
* @param {string} type - The 'codeListValue' for the MD_KeywordTypeCode.
* @param {Object} options - Configuration options.
* @param {Array} [options.fieldKeys] - Array of keys representing the structure of the keyword object.
* @param {Array} [options.matchKeys] - Array of keys used to match existing keywords.
* @param {Function} [options.getValue] - Optional custom function to format the string value before saving.
* @param {Array} [options.additionalPaths] - Optional array of XPath strings or objects for secondary sync.
* @param {Array<{path: string, getValue: Function}>} [options.additionalPaths] Secondary sync paths.
* @param {string} [options.nodeXPath] - Optional custom XPath string to identify the keyword node, overrides default.
*/
const createKeywordBlock = (type, {
Expand Down Expand Up @@ -56,29 +76,19 @@ const createKeywordBlock = (type, {
fieldPath: ({ node, editor }) => (editor.selectNodes('./gmx:Anchor', node).length > 0 ? 'gmx:Anchor' : 'gco:CharacterString'),
source: {
type: 'computed',
getValue: getValue || (({ correction }) => fieldKeys
.map((k) => correction.newKeywordObject[k] || 'NONE')
.join(' > ')
)
getValue: getValue || (({ correction }) => buildKeywordPath(
fieldKeys,
correction.newKeywordObject
))
}
},
// Dynamically add secondary paths for synchronization
// Each path can be a string or an object with { path, getValue }
...additionalPaths.map((pathConfig) => {
const isObject = typeof pathConfig === 'object' && pathConfig.path
const path = isObject ? pathConfig.path : pathConfig
const pathGetValue = isObject ? pathConfig.getValue : null

return {
fieldPath: path,
source: {
type: 'computed',
getValue: pathGetValue || getValue || (({ correction }) => fieldKeys
.map((k) => correction.newKeywordObject[k] || 'NONE')
.join(' > '))
}
...additionalPaths.map(({ path, getValue: pathGetValue }) => ({
fieldPath: path,
source: {
type: 'computed',
getValue: pathGetValue
}
})
}))
]
})

Expand Down Expand Up @@ -123,9 +133,15 @@ const createProductLevelIdEditor = () => leafScheme({
},
delete: [
// Remove the entire processingLevel parent wrapper (in identificationInfo)
{ path: '//gmd:identificationInfo//gmd:processingLevel[gmd:MD_Identifier/gmd:codeSpace/gco:CharacterString="gov.nasa.esdis.umm.processinglevelid"]' },
{
path: '//gmd:identificationInfo//gmd:processingLevel[gmd:MD_Identifier/gmd:codeSpace/gco:CharacterString="gov.nasa.esdis.umm.processinglevelid"]',
matchValuePath: 'gmd:MD_Identifier/gmd:code/gco:CharacterString'
},
// Remove the entire processingLevelCode parent wrapper (in contentInfo)
{ path: '//gmd:contentInfo//gmd:processingLevelCode[gmd:MD_Identifier/gmd:codeSpace/gco:CharacterString="gov.nasa.esdis.umm.processinglevelid"]' }
{
path: '//gmd:contentInfo//gmd:processingLevelCode[gmd:MD_Identifier/gmd:codeSpace/gco:CharacterString="gov.nasa.esdis.umm.processinglevelid"]',
matchValuePath: 'gmd:MD_Identifier/gmd:code/gco:CharacterString'
}
],
replace: [
{
Expand Down Expand Up @@ -254,7 +270,7 @@ export const ISO_19115_SCHEME_EDITORS = {
// Otherwise use split format (NSIDC/NOAA style)
additionalPaths: [
{
path: '//gmi:acquisitionInformation/gmi:MI_AcquisitionInformation/gmi:instrument/eos:EOS_Instrument/gmi:identifier/gmd:MD_Identifier/gmd:code/gco:CharacterString',
path: '//gmi:acquisitionInformation/gmi:MI_AcquisitionInformation//gmi:instrument/eos:EOS_Instrument/gmi:identifier/gmd:MD_Identifier/gmd:code/gco:CharacterString',
getValue: ({ correction, node }) => {
const existingValue = node?.textContent || ''
if (existingValue.includes(' > ')) {
Expand All @@ -268,7 +284,7 @@ export const ISO_19115_SCHEME_EDITORS = {
}
},
{
path: '//gmi:acquisitionInformation/gmi:MI_AcquisitionInformation/gmi:instrument/gmi:MI_Instrument/gmi:identifier/gmd:MD_Identifier/gmd:code/gco:CharacterString',
path: '//gmi:acquisitionInformation/gmi:MI_AcquisitionInformation//gmi:instrument/gmi:MI_Instrument/gmi:identifier/gmd:MD_Identifier/gmd:code/gco:CharacterString',
getValue: ({ correction, node }) => {
const existingValue = node?.textContent || ''
if (existingValue.includes(' > ')) {
Expand All @@ -282,7 +298,7 @@ export const ISO_19115_SCHEME_EDITORS = {
}
},
{
path: '//gmi:acquisitionInformation/gmi:MI_AcquisitionInformation/gmi:instrument/eos:EOS_Instrument/gmi:identifier/gmd:MD_Identifier/gmd:description/gco:CharacterString',
path: '//gmi:acquisitionInformation/gmi:MI_AcquisitionInformation//gmi:instrument/eos:EOS_Instrument/gmi:identifier/gmd:MD_Identifier/gmd:description/gco:CharacterString',
getValue: ({ correction, node, editor }) => {
const codeNode = editor.selectNodes('../gmd:code/gco:CharacterString', node.parentNode)[0]
const codeValue = codeNode?.textContent || ''
Expand All @@ -294,7 +310,7 @@ export const ISO_19115_SCHEME_EDITORS = {
}
},
{
path: '//gmi:acquisitionInformation/gmi:MI_AcquisitionInformation/gmi:instrument/gmi:MI_Instrument/gmi:identifier/gmd:MD_Identifier/gmd:description/gco:CharacterString',
path: '//gmi:acquisitionInformation/gmi:MI_AcquisitionInformation//gmi:instrument/gmi:MI_Instrument/gmi:identifier/gmd:MD_Identifier/gmd:description/gco:CharacterString',
getValue: ({ correction, node, editor }) => {
const codeNode = editor.selectNodes('../gmd:code/gco:CharacterString', node.parentNode)[0]
const codeValue = codeNode?.textContent || ''
Expand Down
34 changes: 28 additions & 6 deletions serverless/src/shared/Iso19115MetadataPathEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,15 @@ export class Iso19115MetadataPathEditor extends XmlMetadataPathEditor {
if (config.delete && config.delete.length > 0) {
config.delete.forEach((delConfig) => {
const nodesToDelete = this.selectNodes(delConfig.path, this.document)
nodesToDelete.forEach((node) => {
const matchingNodes = delConfig.matchValuePath
? nodesToDelete.filter((node) => {
const valueNode = this.selectNodes(`./${delConfig.matchValuePath}`, node)[0]

return (valueNode?.textContent || '').toLowerCase().trim() === oldVal
})
: nodesToDelete

matchingNodes.forEach((node) => {
if (node && node.parentNode) {
node.parentNode.removeChild(node)
}
Expand Down Expand Up @@ -329,9 +337,16 @@ export class Iso19115MetadataPathEditor extends XmlMetadataPathEditor {
// Check if we found a platform or instrument node
if (localName === 'EOS_Platform' || localName === 'MI_Platform'
|| localName === 'EOS_Instrument' || localName === 'MI_Instrument') {
// Found the acquisition wrapper node, remove it entirely
if (currentNode.parentNode) {
currentNode.parentNode.removeChild(currentNode)
// Remove the property wrapper with its value to avoid leaving
// empty gmi:platform or gmi:instrument elements.
const propertyNode = currentNode.parentNode
const nodeToDelete = propertyNode
&& (propertyNode.localName === 'platform' || propertyNode.localName === 'instrument')
? propertyNode
: currentNode

if (nodeToDelete.parentNode) {
nodeToDelete.parentNode.removeChild(nodeToDelete)
}

break
Expand Down Expand Up @@ -371,8 +386,15 @@ export class Iso19115MetadataPathEditor extends XmlMetadataPathEditor {
let currentNode = identifier.parentNode
while (currentNode) {
if (currentNode.localName === 'MI_Operation') {
if (currentNode.parentNode) {
currentNode.parentNode.removeChild(currentNode)
// Remove the operation property with its value so the
// acquisition block does not retain an empty gmi:operation.
const propertyNode = currentNode.parentNode
const nodeToDelete = propertyNode?.localName === 'operation'
? propertyNode
: currentNode

if (nodeToDelete.parentNode) {
nodeToDelete.parentNode.removeChild(nodeToDelete)
}

break
Expand Down
66 changes: 53 additions & 13 deletions serverless/src/shared/XmlMetadataPathEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,22 @@ export class XmlMetadataPathEditor {
* @param {string} fieldPath Absolute `//...` field path.
* @param {Object} [options={}] Absolute-path resolution options.
* @param {boolean} [options.createIfMissing=false] Create missing descendants for simple paths.
* @param {string} [options.expectedText] Select the node whose text matches this value.
* @returns {Element|null} Matching or newly created absolute target element.
*/
resolveAbsoluteFieldElement(fieldPath, { createIfMissing = false } = {}) {
const matchedNode = this.selectNodes(fieldPath)[0] || null
if (matchedNode || !createIfMissing || !isSimpleFieldPath(fieldPath)) {
resolveAbsoluteFieldElement(fieldPath, { createIfMissing = false, expectedText } = {}) {
const matchedNodes = this.selectNodes(fieldPath)
const matchedNode = expectedText === undefined
? matchedNodes[0] || null
: matchedNodes.find((node) => (
normalizeComparableText(this.getElementText(node)) === normalizeComparableText(expectedText)
)) || null

const cannotCreate = expectedText !== undefined
|| !createIfMissing
|| !isSimpleFieldPath(fieldPath)

if (matchedNode || cannotCreate) {
return matchedNode
}

Expand Down Expand Up @@ -431,6 +442,8 @@ export class XmlMetadataPathEditor {
*
* @param {Element} node Target element.
* @param {string} value Replacement text value.
* @param {Object} [options={}] Nested-field selection options.
* @param {string} [options.expectedText] Existing text required for an absolute path.
*
* @example
* editor.setElementText(shortNameNode, 'SPOT-4-UPDATED')
Expand All @@ -452,9 +465,12 @@ export class XmlMetadataPathEditor {
* editor.setNestedText(platformNode, 'Long_Name', 'Systeme Observation de la Terre-4')
* // undefined
*/
setNestedText(node, fieldPath, value) {
setNestedText(node, fieldPath, value, options = {}) {
if (this.isAbsoluteFieldPath(fieldPath)) {
const target = this.resolveAbsoluteFieldElement(fieldPath, { createIfMissing: true })
const target = this.resolveAbsoluteFieldElement(fieldPath, {
createIfMissing: true,
...options
})

if (target) {
this.setElementText(target, value)
Expand All @@ -472,14 +488,16 @@ export class XmlMetadataPathEditor {
*
* @param {Element} node Starting element.
* @param {string} fieldPath Slash-delimited child path.
* @param {Object} [options={}] Nested-field selection options.
* @param {string} [options.expectedText] Existing text required for an absolute path.
*
* @example
* editor.removeNestedElement(relatedUrlNode, 'URL_Content_Type/Subtype')
* // undefined
*/
removeNestedElement(node, fieldPath) {
removeNestedElement(node, fieldPath, options = {}) {
if (this.isAbsoluteFieldPath(fieldPath)) {
this.removeNode(this.resolveAbsoluteFieldElement(fieldPath))
this.removeNode(this.resolveAbsoluteFieldElement(fieldPath, options))

return
}
Expand Down Expand Up @@ -706,6 +724,15 @@ export class XmlMetadataPathEditor {

if (action === 'delete') {
const { parentNode } = targetNode

config.delete?.forEach(({ fieldPath, condition, matchOldValueKey }) => {
if (this.shouldApplyFieldCondition(correction, condition, targetNode)) {
this.removeNestedElement(targetNode, fieldPath, matchOldValueKey
? { expectedText: correction.oldKeywordObject?.[matchOldValueKey] }
: {})
}
})

this.removeNode(targetNode)

if (config.removeEmptyParent) {
Expand All @@ -720,15 +747,22 @@ export class XmlMetadataPathEditor {
}

if (action === 'replace') {
config.replace.forEach(({ fieldPath, source, condition }) => {
config.replace.forEach(({
fieldPath,
source,
condition,
matchOldValueKey
}) => {
if (!this.shouldApplyFieldCondition(correction, condition, targetNode)) {
return
}

const value = this.getReplacementValue(correction, source, targetNode)

if (value.length > 0) {
this.setNestedText(targetNode, fieldPath, value)
this.setNestedText(targetNode, fieldPath, value, matchOldValueKey
? { expectedText: correction.oldKeywordObject?.[matchOldValueKey] }
: {})
} else {
this.removeNestedElement(targetNode, fieldPath)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
Expand Down Expand Up @@ -803,9 +837,9 @@ export class XmlMetadataPathEditor {
/**
* Applies a replace/delete correction to a single scalar XML field.
*
* Scalar fields are different from block and leaf updates: they do not locate a target by
* `oldKeywordObject`. Instead, they operate on the single field selected by `nodeXPath`, or
* create that field under the DIF root when `replace` is requested and the field is absent.
* Scalar fields normally operate on the field selected by `nodeXPath`. When an old value is
* supplied, the matching field is selected so duplicate scalar nodes cannot cause the wrong
* occurrence to be replaced or deleted. A missing field can be created for `replace` actions.
*
* `tagName` is only needed for that create-on-miss case. XPath tells us how to find the node
* when it already exists, but it does not give us the literal element name to pass to
Expand All @@ -831,7 +865,13 @@ export class XmlMetadataPathEditor {
*/
updateScalarNode(correction, config) {
const action = trimString(String(correction.action || 'replace')).toLowerCase()
const targetNode = this.selectNodes(config.nodeXPath)[0] || null
const nodes = this.selectNodes(config.nodeXPath)
const oldValue = getScalarKeywordText(correction?.oldKeywordObject)
const targetNode = oldValue.length > 0
? nodes.find((node) => (
normalizeComparableText(this.getElementText(node)) === normalizeComparableText(oldValue)
)) || null
: nodes[0] || null

if (action === 'delete') {
if (!targetNode) {
Expand Down
Loading