UNPKG

xsd2jsonschema

Version:

A pure JavaScript library for converting complex XML Schemas into equivalent JSON Schemas.

429 lines (378 loc) 13.3 kB
'use strict'; const debug = require('debug')('xsd2jsonschema:XsdFile'); const DOMParser = require('xmldom').DOMParser; const xpathProcessor = require('xpath'); const URI = require('urijs'); const XsdAttributes = require('./xsdAttributes'); const XsdElements = require('./xsdElements'); const XsdAttributeValues = require('./xsdAttributeValues'); const XsdNodeTypes = require('./xsdNodeTypes'); const Constants = require('../constants'); const uri_NAME = Symbol(); const xmlDoc_NAME = Symbol(); const includeUris_NAME = Symbol(); const importUris_NAME = Symbol(); const namespaces_NAME = Symbol(); const imports_NAME = Symbol(); /** * XML Schema file operations * https://www.w3.org/2001/xml.xsd * TBD (xmldom) */ class XsdFile { constructor(options) { if (options == undefined || typeof options !== 'object') { throw new Error('Parameter "options" is required') } if (options.uri == undefined) { throw new Error('"options.uri" is required'); } if (options.xml == undefined) { throw new Error('"options.xml" is required'); } // Instantiate the URL just in case a string was passed in. this.uri = new URI(options.uri); this.xmlDoc = new DOMParser().parseFromString(options.xml, 'text/xml'); this.namespaces = {}; this.imports = {}; this.initilizeNamespaces(); this.initializeIncludes(); this.initializeImports(); } // Getters/Setters get uri() { return this[uri_NAME]; } set uri(newUri) { this[uri_NAME] = newUri; } get xmlDoc() { return this[xmlDoc_NAME]; } set xmlDoc(newDoc) { this[xmlDoc_NAME] = newDoc; } get includeUris() { return this[includeUris_NAME]; } set includeUris(newIncludeUris) { this[includeUris_NAME] = newIncludeUris; } get importUris() { return this[importUris_NAME]; } set importUris(newIncludeUris) { this[importUris_NAME] = newIncludeUris; } get namespaces() { return this[namespaces_NAME]; } set namespaces(newNamespaces) { this[namespaces_NAME] = newNamespaces; } get imports() { return this[imports_NAME]; } set imports(newImports) { this[imports_NAME] = newImports; } // read-only properties get filename() { return this.uri.filename(); } set filename(unused) { throw new Error('Unsupported operation'); } get directory() { return this.uri.directory(); } set directory(unused) { throw new Error('Unsupported operation'); } get schemaElement() { return this.xmlDoc.documentElement; } set schemaElement(unused) { throw new Error('Unsupported operation'); } get targetNamespace() { return this.xmlDoc.documentElement.getAttribute(XsdAttributes.TARGET_NAMESPACE); } set targetNamespace(unused) { throw new Error('Unsupported operation'); } // 1 Map the XML Schmea Namespase to a prefix such as xsd or xs, and make the target namespace the default namespace. // 2 Map a prefix to the target namespace, and make the XML Schema Namespase the default namespace. // 3 Map prefixes to all the namespaces. initilizeNamespaces() { const attrs = XsdFile.getAttributes(this.schemaElement); Object.keys(attrs).forEach(function (key, index, array) { const attr = attrs[key]; if (attr.nodeType === XsdNodeTypes.ATTRIBUTE_NODE) { const attrValue = attr.value; switch (attr.localName) { case XsdAttributes.TARGET_NAMESPACE: this.namespaces[XsdAttributes.TARGET_NAMESPACE] = attrValue; break; case XsdAttributeValues.XMLNS: this.namespaces[''] = attrValue; break; default: if (attr.prefix === 'xmlns') { const namespace = attr.localName; this.namespaces[namespace] = attrValue; } break; } } }, this); // Ensure values are set for targetNamespace, the default namespace, and the XML Schema Namespace if (this.namespaces[XsdAttributes.TARGET_NAMESPACE] == undefined && this.namespaces[''] == undefined) { this.namespaces[XsdAttributes.TARGET_NAMESPACE] = Constants.NO_NAMESPACE; this.namespaces[''] = Constants.NO_NAMESPACE; } else if (this.namespaces[XsdAttributes.TARGET_NAMESPACE] == undefined && this.namespaces[''] != undefined) { this.namespaces[XsdAttributes.TARGET_NAMESPACE] = this.namespaces['']; } else if (this.namespaces[XsdAttributes.TARGET_NAMESPACE] != undefined && this.namespaces[''] == undefined) { this.namespaces[''] = this.namespaces[XsdAttributes.TARGET_NAMESPACE]; } // If the XML Schema Namespace is missing setup a couple of defaults so we can try to convert the schema. (not a good sign) if (this.isMissingXmlSchemaNamespace()) { this.namespaces[Constants.XML_SCHEMA_DEFAULT_NAMESPACE_NAME_XS] = Constants.XML_SCHEMA_NAMESPACE; this.namespaces[Constants.XML_SCHEMA_DEFAULT_NAMESPACE_NAME_XSD] = Constants.XML_SCHEMA_NAMESPACE; } } isMissingXmlSchemaNamespace() { const keys = Object.keys(this.namespaces); for(const key of keys) { if(this.namespaces[key] == Constants.XML_SCHEMA_NAMESPACE) { return false; } } return true; } resolveNamespace(namespaceName) { return this.namespaces[namespaceName]; } hasIncludes() { return this.includeUris.length > 0; } hasImports() { return this.importUris.length > 0; } initializeIncludes() { if (this.includeUris === undefined) { var includeNodes = this.schemaElement.getElementsByTagName(this.schemaElement.prefix + ':include'); this.includeUris = []; for (let i = 0; i < includeNodes.length; i++) { const includeNode = includeNodes.item(i); this.includeUris.push(includeNode.getAttribute(XsdAttributes.SCHEMA_LOCATION)); } } return this.includeUris; } initializeImports() { if (this.importUris === undefined) { var importNodes = this.schemaElement.getElementsByTagName(this.schemaElement.prefix + ':import'); this.importUris = []; for (let i = 0; i < importNodes.length; i++) { const importNode = importNodes.item(i); this.importUris.push(importNode.getAttribute(XsdAttributes.SCHEMA_LOCATION)); } } return this.importUris; } select(xpath, ns, namespace) { var nodes; try { const select = xpathProcessor.useNamespaces(this.namespaces); nodes = select(xpath, this.xmlDoc); } catch (error) { debug(error); throw error; } return nodes } select1(xpath, ns, namespace) { var node; try { const select = xpathProcessor.useNamespaces(this.namespaces); node = select(xpath, this.xmlDoc, true); } catch (error) { debug(error); throw error; } return node; } toString() { const str = `baseFilename=${this.filename} uri=${this.uri} includeUris=${this.includeUris} namespaces=${JSON.stringify(this.namespaces, null, '\t')} xmlDoc=${this.xmlDoc}` return str; } /** * xml interface * * 1) dumpAttrs * 2) getAttrValue * 3) hasAttr * 4) getAttrValue * 5) getValueAttr * 6) dumpNode * 7) getNodeName * 8) isNamed * 9) isReference * 10) countChildren * 11) buildAttributeMap * 12) getChildNodes * 13) getAttributes */ /* *********************************************************************************** */ static nodeQuickDumpStr(node) { var retval = XsdFile.getNodeName(node) + ' ['; var attrs = node.attributes; if (attrs != undefined) { Object.keys(attrs).forEach(function (attr, index, array) { if (attrs[attr].nodeType === XsdNodeTypes.ATTRIBUTE_NODE) { retval += attrs[attr].localName + '=' + attrs[attr].value + ' '; } }, this); } return retval.trim() + ']'; } static dumpAttrs(node) { var attrs = node.attributes; debug('XML-TAG-Attributes:'); if (attrs != undefined) { Object.keys(attrs).forEach(function (attr, index, array) { if (attrs[attr].nodeType === XsdNodeTypes.ATTRIBUTE_NODE) { debug('\t' + index + ') ' + attrs[attr].localName + '=' + attrs[attr].value); } }, this); } } static convertToNumber(value) { var retval = Number(value); if (isNaN(retval)) { throw new Error(`Unable create a Number from [${value}]`); } return retval; } static getAttrValueAsNumber(node, attrName) { var value; if (this.hasAttribute(node, attrName)) { value = node.getAttribute(attrName); } return this.convertToNumber(value); } static getAttrValue(node, attrName) { var retval; if (this.hasAttribute(node, attrName)) { retval = node.getAttribute(attrName); } return retval; } static hasAttribute(node, attrName) { if (node.hasAttribute !== undefined) { return node.hasAttribute(attrName); } else { return false; } } static getNameAttrValue(node) { return this.getAttrValue(node, XsdAttributes.NAME); } static getValueAttr(node) { return this.getAttrValue(node, XsdAttributes.VALUE); } static getValueAttrAsNumber(node) { if (node == XsdAttributeValues.UNBOUNDED) { return undefined; } return this.getAttrValueAsNumber(node, XsdAttributes.VALUE); } static getTypeNode(node) { let typeNode = node; while (typeNode.parentNode.localName != XsdElements.SCHEMA) { typeNode = typeNode.parentNode; } return typeNode; } static dumpNode(node) { debug('XML-Type= ' + XsdNodeTypes.getTypeName(node.nodeType)); debug('XML-TAG-Name= ' + node.nodeName); debug('XML-TAG-NameSpace= ' + node.namespaceURI + '=' + node.namespaceURI); var text = node.textContent; if (text != undefined) { const trimmed = text.trim(); debug('XML-Text= [' + (trimmed.length > 0 ? trimmed : 'it was all whitespace') + ']'); } this.dumpAttrs(node); debug('__________________________________________'); } static getNodeName(node) { var name; switch (node.nodeType) { case XsdNodeTypes.TEXT_NODE: // 3 name = 'text'; break; case XsdNodeTypes.COMMENT_NODE: // 8 name = 'comment'; break; default: name = node.localName } return name; } static isNamed(node) { return this.hasAttribute(node, XsdAttributes.NAME); } static isReference(node) { return this.hasAttribute(node, XsdAttributes.REF); } static isEmpty(node) { return this.getChildNodes(node).length == 0; } static countChildren(node, tagName) { // return node.childNodes.length; const nodeName = node.prefix + ':' + tagName; var len = node.getElementsByTagName(nodeName).length; return len; } static buildAttributeMap(node) { var map = {}; var attrs = node.attributes; Object.keys(attrs).forEach(function (attr, index, array) { if (attrs[attr].nodeType === XsdNodeTypes.ATTRIBUTE_NODE) { map[attrs[attr].nodeName] = attrs[attr].value; } }, this); return map; } static getChildNodes(node) { var retval = []; var nodelist = node.childNodes; if (nodelist != undefined) { for (let i = 0; i < nodelist.length; i++) { retval.push(nodelist.item(i)); } } return retval; } static getAttributes(node) { return node.attributes; } static getFirstParentWithNameAttribute(node) { if (!this.hasAttribute(node.parentNode, XsdAttributes.NAME)) { return this.getFirstParentWithNameAttribute(node.parentNode); } return node.parentNode } static getNameOfFirstParentWithNameAttribute(node) { const firstParentWithName = this.getFirstParentWithNameAttribute(node); return this.getAttrValue(firstParentWithName, XsdAttributes.NAME); } } module.exports = XsdFile;