UNPKG

@itwin/ecschema-metadata

Version:

ECObjects core concepts in typescript

885 lines • 56.4 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ import { assert } from "@itwin/core-bentley"; import { parsePrimitiveType, PrimitiveType, primitiveTypeToString, StrengthDirection, strengthDirectionToString } from "../ECObjects"; import { ECSchemaError, ECSchemaStatus } from "../Exception"; import { Enumeration } from "../Metadata/Enumeration"; import { ECName } from "../ECName"; import { AbstractParser } from "./AbstractParser"; import { SchemaReadHelper } from "./Helper"; const NON_ITEM_SCHEMA_ELEMENTS = ["ECSchemaReference", "ECCustomAttributes"]; const ECXML_URI = "http://www\\.bentley\\.com/schemas/Bentley\\.ECXML"; /** @internal */ export class XmlParser extends AbstractParser { _rawSchema; _schemaName; _schemaReferenceNames; _schemaAlias; _schemaVersion; _xmlNamespace; _currentItemFullName; _schemaItems; _mapIsPopulated; constructor(rawSchema) { super(); this._rawSchema = rawSchema; const schemaInfo = rawSchema.documentElement; const schemaName = schemaInfo.getAttribute("schemaName"); if (schemaName) this._schemaName = schemaName; this._schemaAlias = ""; const schemaAlias = schemaInfo.getAttribute("alias"); if (schemaAlias) this._schemaAlias = schemaAlias; this._schemaReferenceNames = new Map(); const schemaVersion = schemaInfo.getAttribute("version"); if (schemaVersion) this._schemaVersion = schemaVersion; const xmlNamespace = schemaInfo.getAttribute("xmlns"); if (xmlNamespace) { this._xmlNamespace = xmlNamespace; this._ecSpecVersion = XmlParser.parseXmlNamespace(this._xmlNamespace); } this._schemaItems = new Map(); this._mapIsPopulated = false; } get getECSpecVersion() { return this._ecSpecVersion; } parseSchema() { const schemaMetadata = this._rawSchema.documentElement; if ("ECSchema" !== schemaMetadata.nodeName) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, "An ECSchema is missing the required metadata."); const schemaDefDuplicates = this.getElementChildrenByTagName(schemaMetadata, "ECSchema"); if (schemaDefDuplicates.length > 1) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, "An ECSchema has more than one ECSchema definition. Only one is allowed."); if (this._schemaName === undefined) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `An ECSchema is missing a required 'schemaName' attribute`); if (this._schemaVersion === undefined) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The ECSchema ${this._schemaName} is missing a required 'version' attribute`); if (this._xmlNamespace === undefined) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The ECSchema ${this._schemaName} is missing a required 'xmlns' attribute`); if (this._ecSpecVersion === undefined) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The ECSchema ${this._schemaName} has an invalid 'xmlns' attribute`); const alias = this.getRequiredAttribute(schemaMetadata, "alias", `The ECSchema ${this._schemaName} is missing a required 'alias' attribute`); const description = this.getOptionalAttribute(schemaMetadata, "description"); const displayLabel = this.getOptionalAttribute(schemaMetadata, "displayLabel"); const schemaProps = { name: this._schemaName, $schema: this._xmlNamespace, version: this._schemaVersion, alias, label: displayLabel, description, ecSpecMajorVersion: this._ecSpecVersion.readVersion, ecSpecMinorVersion: this._ecSpecVersion.writeVersion, }; return schemaProps; } *getReferences() { const schemaReferences = this.getElementChildrenByTagName(this._rawSchema.documentElement, "ECSchemaReference"); for (const ref of schemaReferences) { yield this.getSchemaReference(ref); } } *getItems() { if (!this._mapIsPopulated) { const schemaItems = this.getSchemaChildren(); for (const item of schemaItems) { let rawItemType = item.nodeName; if (NON_ITEM_SCHEMA_ELEMENTS.includes(rawItemType)) continue; // Differentiate a Mixin from an EntityClass const customAttributesResult = this.getElementChildrenByTagName(item, "ECCustomAttributes"); if (customAttributesResult.length > 0) { const customAttributes = customAttributesResult[0]; const isMixinResult = this.getElementChildrenByTagName(customAttributes, "IsMixin"); if (isMixinResult.length > 0) rawItemType = "Mixin"; } const itemType = this.getSchemaItemType(rawItemType); if (itemType === undefined) { if (SchemaReadHelper.isECSpecVersionNewer(this._ecSpecVersion)) continue; throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `A SchemaItem in ${this._schemaName} has an invalid type. '${rawItemType}' is not a valid SchemaItem type.`); } const itemName = this.getRequiredAttribute(item, "typeName", `A SchemaItem in ${this._schemaName} is missing the required 'typeName' attribute.`); if (!ECName.validate(itemName)) throw new ECSchemaError(ECSchemaStatus.InvalidECName, `A SchemaItem in ${this._schemaName} has an invalid 'typeName' attribute. '${itemName}' is not a valid ECName.`); this._currentItemFullName = `${this._schemaName}.${itemName}`; this._schemaItems.set(itemName, [itemType, item]); yield [itemName, itemType, item]; } this._mapIsPopulated = true; } else { for (const [itemName, [itemType, item]] of this._schemaItems) { this._currentItemFullName = `${this._schemaName}.${itemName}`; yield [itemName, itemType, item]; } } } findItem(itemName) { if (!this._mapIsPopulated) { for (const item of this.getItems()) { if (item[0] === itemName) { this._currentItemFullName = `${this._schemaName}.${itemName}`; return item; } } } else { const values = this._schemaItems.get(itemName); if (undefined !== values) { const [itemType, item] = values; this._currentItemFullName = `${this._schemaName}.${itemName}`; return [itemName, itemType, item]; } } return undefined; } parseEntityClass(xmlElement) { const classProps = this.getClassProps(xmlElement); const baseClasses = this.getElementChildrenByTagName(xmlElement, "BaseClass"); let mixinElements; const mixins = new Array(); // if it has just one BaseClass we assume it is a 'true' base class not a mixin if (baseClasses.length > 1) { mixinElements = baseClasses.slice(1); for (const mixin of mixinElements) { if (mixin.textContent) { const typeName = this.getQualifiedTypeName(mixin.textContent); mixins.push(typeName); } } } const entityClassProps = { ...classProps, mixins, }; return entityClassProps; } parseMixin(xmlElement) { const classProps = this.getClassProps(xmlElement); const baseClasses = this.getElementChildrenByTagName(xmlElement, "BaseClass"); // Mixins can only have one base class if (baseClasses.length > 1) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The Mixin ${this._currentItemFullName} has more than one base class which is not allowed.`); const customAttributesResult = this.getElementChildrenByTagName(xmlElement, "ECCustomAttributes"); if (customAttributesResult.length < 1) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The Mixin ${this._currentItemFullName} is missing the required 'IsMixin' tag.`); const customAttributes = customAttributesResult[0]; const isMixinResult = this.getElementChildrenByTagName(customAttributes, "IsMixin"); if (isMixinResult.length < 1) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The Mixin ${this._currentItemFullName} is missing the required 'IsMixin' tag.`); const mixinAttributes = isMixinResult[0]; const appliesToResult = this.getElementChildrenByTagName(mixinAttributes, "AppliesToEntityClass"); if (appliesToResult.length < 1) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The Mixin ${this._currentItemFullName} is missing the required 'AppliesToEntityClass' tag.`); const appliesToElement = appliesToResult[0]; let appliesTo = appliesToElement.textContent; if (appliesTo === null || appliesTo.length === 0) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The Mixin ${this._currentItemFullName} is missing the required 'AppliesToEntityClass' tag.`); appliesTo = this.getQualifiedTypeName(appliesTo); const mixinProps = { ...classProps, appliesTo, }; return mixinProps; } parseStructClass(xmlElement) { return this.getClassProps(xmlElement); } parseCustomAttributeClass(xmlElement) { const classProps = this.getClassProps(xmlElement); const appliesTo = this.getRequiredAttribute(xmlElement, "appliesTo", `The CustomAttributeClass ${this._currentItemFullName} is missing the required 'appliesTo' attribute.`); const customAttributeClassProps = { ...classProps, appliesTo, }; return customAttributeClassProps; } parseRelationshipClass(xmlElement) { const classProps = this.getClassProps(xmlElement); const strength = this.getRequiredAttribute(xmlElement, "strength", `The RelationshipClass ${this._currentItemFullName} is missing the required 'strength' attribute.`); let strengthDirection = this.getOptionalAttribute(xmlElement, "strengthDirection"); if (!strengthDirection) strengthDirection = strengthDirectionToString(StrengthDirection.Forward); const sourceResult = this.getElementChildrenByTagName(xmlElement, "Source"); if (sourceResult.length !== 1) { if (sourceResult.length === 0) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The RelationshipClass ${this._currentItemFullName} is missing the required Source constraint tag.`); else throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The RelationshipClass ${this._currentItemFullName} has more than one Source constraint tag. Only one is allowed.`); } const source = this.getRelationshipConstraintProps(sourceResult[0], true); const targetResult = this.getElementChildrenByTagName(xmlElement, "Target"); if (targetResult.length !== 1) { if (targetResult.length === 0) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The RelationshipClass ${this._currentItemFullName} is missing the required Target constraint tag.`); else throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The RelationshipClass ${this._currentItemFullName} has more than one Target constraint tag. Only one is allowed.`); } const target = this.getRelationshipConstraintProps(targetResult[0], false); return { ...classProps, strength, strengthDirection, source, target, }; } parseEnumeration(xmlElement) { const itemProps = this.getSchemaItemProps(xmlElement); const enumType = this.getRequiredAttribute(xmlElement, "backingTypeName", `The Enumeration ${this._currentItemFullName} is missing the required 'backingTypeName' attribute.`); // TODO: This shouldn't be verified here. It's for the deserialize method to handle. The only reason it's currently done here so that the xml // value can be put in the correct type, number or string. let tempBackingType; if (/int/i.test(enumType)) { tempBackingType = PrimitiveType.Integer; } else if (/string/i.test(enumType)) { tempBackingType = PrimitiveType.String; } else { if (SchemaReadHelper.isECSpecVersionNewer(this._ecSpecVersion)) tempBackingType = PrimitiveType.String; else throw new ECSchemaError(ECSchemaStatus.InvalidECJson, `The Enumeration ${this._currentItemFullName} has an invalid 'backingTypeName' attribute. It should be either "int" or "string".`); } let isStrictString = this.getOptionalAttribute(xmlElement, "isStrict"); if (isStrictString === undefined) isStrictString = "true"; const isStrict = this.parseBoolean(isStrictString, `The Enumeration ${this._currentItemFullName} has an invalid 'isStrict' attribute. It should either be "true" or "false".`); const enumeratorElements = this.getElementChildrenByTagName(xmlElement, "ECEnumerator"); const enumerators = new Array(); for (const element of enumeratorElements) { const name = this.getRequiredAttribute(element, "name", `The Enumeration ${this._currentItemFullName} has an enumerator that is missing the required attribute 'name'.`); const valueString = this.getRequiredAttribute(element, "value", `The Enumeration ${this._currentItemFullName} has an enumerator that is missing the required attribute 'value'.`); let value = valueString; if (PrimitiveType.Integer === tempBackingType) { const numericValue = parseInt(valueString, 10); if (isNaN(numericValue)) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The Enumeration ${this._currentItemFullName} of type "int" has an enumerator with a non-integer value.`); value = numericValue; } const label = this.getOptionalAttribute(element, "displayLabel"); const description = this.getOptionalAttribute(element, "description"); enumerators.push({ name, value, label, description, }); } return { ...itemProps, type: enumType, isStrict, enumerators, originalECSpecMajorVersion: this._ecSpecVersion?.readVersion, originalECSpecMinorVersion: this._ecSpecVersion?.writeVersion, }; } parseKindOfQuantity(xmlElement) { const itemProps = this.getSchemaItemProps(xmlElement); const relativeErrorString = this.getRequiredAttribute(xmlElement, "relativeError", `The KindOfQuantity ${this._currentItemFullName} is missing the required 'relativeError' attribute.`); const relativeError = parseFloat(relativeErrorString); if (isNaN(relativeError)) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The KindOfQuantity ${this._currentItemFullName} has an invalid 'relativeError' attribute. It should be a numeric value.`); const presentationUnitsString = this.getOptionalAttribute(xmlElement, "presentationUnits"); let presentationUnits; if (presentationUnitsString) presentationUnits = this.getQualifiedPresentationUnits(presentationUnitsString.split(";")); let persistenceUnit = this.getRequiredAttribute(xmlElement, "persistenceUnit", `The KindOfQuantity ${this._currentItemFullName} is missing the required 'persistenceUnit' attribute.`); persistenceUnit = this.getQualifiedTypeName(persistenceUnit); return { ...itemProps, relativeError, presentationUnits, persistenceUnit, }; } parsePropertyCategory(xmlElement) { const itemProps = this.getSchemaItemProps(xmlElement); const priorityString = this.getRequiredAttribute(xmlElement, "priority", `The PropertyCategory ${this._currentItemFullName} is missing the required 'priority' attribute.`); const priority = parseInt(priorityString, 10); if (isNaN(priority)) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The PropertyCategory ${this._currentItemFullName} has an invalid 'priority' attribute. It should be a numeric value.`); return { ...itemProps, priority, }; } parseUnit(xmlElement) { const itemProps = this.getSchemaItemProps(xmlElement); let phenomenon = this.getRequiredAttribute(xmlElement, "phenomenon", `The Unit ${this._currentItemFullName} is missing the required 'phenomenon' attribute.`); let unitSystem = this.getRequiredAttribute(xmlElement, "unitSystem", `The Unit ${this._currentItemFullName} is missing the required 'unitSystem' attribute.`); const definition = this.getRequiredAttribute(xmlElement, "definition", `The Unit ${this._currentItemFullName} is missing the required 'definition' attribute.`); const numerator = this.getOptionalFloatAttribute(xmlElement, "numerator", `The Unit ${this._currentItemFullName} has an invalid 'numerator' attribute. It should be a numeric value.`); const denominator = this.getOptionalFloatAttribute(xmlElement, "denominator", `The Unit ${this._currentItemFullName} has an invalid 'denominator' attribute. It should be a numeric value.`); const offset = this.getOptionalFloatAttribute(xmlElement, "offset", `The Unit ${this._currentItemFullName} has an invalid 'offset' attribute. It should be a numeric value.`); phenomenon = this.getQualifiedTypeName(phenomenon); unitSystem = this.getQualifiedTypeName(unitSystem); return { ...itemProps, phenomenon, unitSystem, definition, numerator, denominator, offset, }; } parseInvertedUnit(xmlElement) { const itemProps = this.getSchemaItemProps(xmlElement); let invertsUnit = this.getRequiredAttribute(xmlElement, "invertsUnit", `The InvertedUnit ${this._currentItemFullName} is missing the required 'invertsUnit' attribute.`); let unitSystem = this.getRequiredAttribute(xmlElement, "unitSystem", `The InvertedUnit ${this._currentItemFullName} is missing the required 'unitSystem' attribute.`); invertsUnit = this.getQualifiedTypeName(invertsUnit); unitSystem = this.getQualifiedTypeName(unitSystem); return { ...itemProps, invertsUnit, unitSystem, }; } parseConstant(xmlElement) { const itemProps = this.getSchemaItemProps(xmlElement); let phenomenon = this.getRequiredAttribute(xmlElement, "phenomenon", `The Constant ${this._currentItemFullName} is missing the required 'phenomenon' attribute.`); const definition = this.getRequiredAttribute(xmlElement, "definition", `The Constant ${this._currentItemFullName} is missing the required 'definition' attribute.`); const numerator = this.getOptionalFloatAttribute(xmlElement, "numerator", `The Constant ${this._currentItemFullName} has an invalid 'numerator' attribute. It should be a numeric value.`); const denominator = this.getOptionalFloatAttribute(xmlElement, "denominator", `The Constant ${this._currentItemFullName} has an invalid 'denominator' attribute. It should be a numeric value.`); phenomenon = this.getQualifiedTypeName(phenomenon); return { ...itemProps, phenomenon, definition, numerator, denominator, }; } parsePhenomenon(xmlElement) { const itemProps = this.getSchemaItemProps(xmlElement); const definition = this.getRequiredAttribute(xmlElement, "definition", `The Phenomenon ${this._currentItemFullName} is missing the required 'definition' attribute.`); return { ...itemProps, definition, }; } parseFormat(xmlElement) { const itemProps = this.getSchemaItemProps(xmlElement); const formatType = this.getRequiredAttribute(xmlElement, "type", `The Format ${this._currentItemFullName} is missing the required 'type' attribute.`); const precision = this.getOptionalIntAttribute(xmlElement, "precision", `The Format ${this._currentItemFullName} has an invalid 'precision' attribute. It should be a numeric value.`); const roundFactor = this.getOptionalFloatAttribute(xmlElement, "roundFactor", `The Format ${this._currentItemFullName} has an invalid 'roundFactor' attribute. It should be a numeric value.`); const minWidth = this.getOptionalIntAttribute(xmlElement, "minWidth", `The Format ${this._currentItemFullName} has an invalid 'minWidth' attribute. It should be a numeric value.`); const showSignOption = this.getOptionalAttribute(xmlElement, "showSignOption"); const formatTraitsString = this.getRequiredAttribute(xmlElement, "formatTraits", `The Format ${this._currentItemFullName} is missing the required 'formatTraits' attribute.`); const formatTraits = formatTraitsString.split("|"); const decimalSeparator = this.getOptionalAttribute(xmlElement, "decimalSeparator"); const thousandSeparator = this.getOptionalAttribute(xmlElement, "thousandSeparator"); const uomSeparator = this.getOptionalAttribute(xmlElement, "uomSeparator"); const scientificType = this.getOptionalAttribute(xmlElement, "scientificType"); const stationOffsetSize = this.getOptionalIntAttribute(xmlElement, "stationOffsetSize", `The Format ${this._currentItemFullName} has an invalid 'stationOffsetSize' attribute. It should be a numeric value.`); const stationSeparator = this.getOptionalAttribute(xmlElement, "stationSeparator"); let composite; const compositeResult = this.getElementChildrenByTagName(xmlElement, "Composite"); if (compositeResult.length > 0) { const compositeElement = compositeResult[0]; const spacer = this.getOptionalAttribute(compositeElement, "spacer"); const includeZeroString = this.getOptionalAttribute(compositeElement, "includeZero"); let includeZero; if (includeZeroString) { includeZero = this.parseBoolean(includeZeroString, `The Format ${this._currentItemFullName} has a Composite with an invalid 'includeZero' attribute. It should be either "true" or "false".`); } const units = new Array(); const unitsResult = this.getElementChildrenByTagName(compositeElement, "Unit"); if (unitsResult.length < 1) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The Format ${this._currentItemFullName} has an invalid 'Composite' element. It should have 1-4 Unit elements.`); for (const unit of unitsResult) { let name = unit.textContent; if (null === name || 0 === name.length) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The Format ${this._currentItemFullName} has a Composite with an invalid Unit. One of the Units is missing the required 'name' attribute.`); const label = this.getOptionalAttribute(unit, "label"); name = this.getQualifiedTypeName(name); units.push({ name, label }); } composite = { spacer, includeZero, units, }; } return { ...itemProps, type: formatType, precision, roundFactor, minWidth, showSignOption, formatTraits, decimalSeparator, thousandSeparator, uomSeparator, scientificType, stationOffsetSize, stationSeparator, composite, }; } parseUnitSystem(xmlElement) { return this.getClassProps(xmlElement); } *getProperties(xmlElement, itemName) { const propertyTagRegex = /EC((Struct(Array)?)|Array|Navigation)?Property/; const children = this.getElementChildrenByTagName(xmlElement, propertyTagRegex); for (const child of children) { const childType = child.nodeName; const propertyName = this.getRequiredAttribute(child, "propertyName", `An ECProperty in ${itemName} is missing the required 'propertyName' attribute.`); const propertyType = this.getPropertyType(childType); // This may not be needed, just a failsafe if the regex is faulty if (propertyType === undefined) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The ECProperty ${itemName}.${propertyName} has an invalid type. ${childType} is not a valid ECProperty type.`); yield [propertyName, propertyType, child]; } } parsePrimitiveProperty(xmlElement) { const typeName = this.getPropertyTypeName(xmlElement); const propertyProps = this.getPrimitiveOrEnumPropertyBaseProps(xmlElement, typeName); const primitivePropertyProps = { ...propertyProps, typeName }; return primitivePropertyProps; } parseStructProperty(xmlElement) { const propertyProps = this.getPropertyProps(xmlElement); const typeName = this.getPropertyTypeName(xmlElement); const structPropertyProps = { ...propertyProps, typeName }; return structPropertyProps; } parsePrimitiveArrayProperty(xmlElement) { const typeName = this.getPropertyTypeName(xmlElement); const propertyProps = this.getPrimitiveOrEnumPropertyBaseProps(xmlElement, typeName); const minAndMaxOccurs = this.getPropertyMinAndMaxOccurs(xmlElement); return { ...propertyProps, ...minAndMaxOccurs, typeName, }; } parseStructArrayProperty(xmlElement) { const propertyProps = this.getPropertyProps(xmlElement); const typeName = this.getPropertyTypeName(xmlElement); const minAndMaxOccurs = this.getPropertyMinAndMaxOccurs(xmlElement); return { ...propertyProps, ...minAndMaxOccurs, typeName, }; } parseNavigationProperty(xmlElement) { const propName = this.getPropertyName(xmlElement); const propertyProps = this.getPropertyProps(xmlElement); let relationshipName = this.getRequiredAttribute(xmlElement, "relationshipName", `The ECNavigationProperty ${this._currentItemFullName}.${propName} is missing the required 'relationshipName' property.`); const direction = this.getRequiredAttribute(xmlElement, "direction", `The ECNavigationProperty ${this._currentItemFullName}.${propName} is missing the required 'direction' property.`); relationshipName = this.getQualifiedTypeName(relationshipName); return { ...propertyProps, relationshipName, direction, }; } getSchemaCustomAttributeProviders() { return this.getCustomAttributeProviders(this._rawSchema.documentElement, "Schema", this._schemaName); } getClassCustomAttributeProviders(xmlElement) { return this.getCustomAttributeProviders(xmlElement, "ECClass", this._currentItemFullName); } getPropertyCustomAttributeProviders(xmlElement) { const propName = this.getPropertyName(xmlElement); return this.getCustomAttributeProviders(xmlElement, "ECProperty", `${this._currentItemFullName}.${propName}`); } getRelationshipConstraintCustomAttributeProviders(xmlElement) { const sourceResult = this.getElementChildrenByTagName(xmlElement, "Source"); if (sourceResult.length < 1) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The RelationshipClass ${this._currentItemFullName} is missing the required Source constraint tag.`); const sourceElement = sourceResult[0]; const sourceCustomAttributes = this.getCustomAttributeProviders(sourceElement, "Source Constraint of", this._currentItemFullName); const targetResult = this.getElementChildrenByTagName(xmlElement, "Target"); if (targetResult.length < 1) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The RelationshipClass ${this._currentItemFullName} is missing the required Target constraint tag.`); const targetElement = targetResult[0]; const targetCustomAttributes = this.getCustomAttributeProviders(targetElement, "Source Constraint of", this._currentItemFullName); return [sourceCustomAttributes, targetCustomAttributes]; } getElementChildren(xmlElement) { // NodeListOf<T> does not define [Symbol.iterator] const children = Array.from(xmlElement.childNodes).filter((child) => { // (node.nodeType === 1) implies instanceof Element // https://developer.mozilla.org/en-US/docs/Web/API/ParentNode/children#Polyfill return child.nodeType === 1; }); return children; } getElementChildrenByTagName(xmlElement, tagName) { const children = this.getElementChildren(xmlElement); if ("*" === tagName) return children; let result; if (typeof tagName === "string") { result = children.filter((child) => { return tagName.toLowerCase() === child.nodeName.toLowerCase(); }); } else { result = children.filter((child) => { return tagName.test(child.nodeName); }); } return result; } getOptionalAttribute(xmlElement, attributeName) { if (!xmlElement.hasAttribute(attributeName)) return undefined; const result = xmlElement.getAttribute(attributeName); // The typings for the return value of getAttribute do not match that of xmldom // xmldom returns an empty string instead of null // However Typescript will still treat result as a union type without this check // Hence this is needed for tsc to compile if (result === null) return undefined; return result; } getOptionalFloatAttribute(xmlElement, attributeName, parseErrorMsg) { const resultString = this.getOptionalAttribute(xmlElement, attributeName); let result; if (resultString) { result = parseFloat(resultString); if (isNaN(result)) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, parseErrorMsg); } return result; } getOptionalIntAttribute(xmlElement, attributeName, parseErrorMsg) { const resultString = this.getOptionalAttribute(xmlElement, attributeName); let result; if (resultString) { result = parseInt(resultString, 10); if (isNaN(result)) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, parseErrorMsg); } return result; } parseBoolean(text, parseErrorMsg) { const textString = text.toLowerCase(); if ("true" === textString) return true; else if ("false" === textString) return false; else throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, parseErrorMsg); } getRequiredAttribute(xmlElement, attributeName, errorMsg) { if (!xmlElement.hasAttribute(attributeName)) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, errorMsg); const result = xmlElement.getAttribute(attributeName); if (result === null) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, errorMsg); return result; } getSchemaReference(xmlElement) { const alias = this.getRequiredAttribute(xmlElement, "alias", `The schema ${this._schemaName} has an invalid ECSchemaReference attribute. One of the references is missing the required 'alias' attribute.`); const name = this.getRequiredAttribute(xmlElement, "name", `The schema ${this._schemaName} has an invalid ECSchemaReference attribute. One of the references is missing the required 'name' attribute.`); const version = this.getRequiredAttribute(xmlElement, "version", `The schema ${this._schemaName} has an invalid ECSchemaReference attribute. One of the references is missing the required 'version' attribute.`); if (!this._schemaReferenceNames.has(alias.toLowerCase())) this._schemaReferenceNames.set(alias.toLowerCase(), name); return { name, version, }; } getSchemaItemType(rawType) { switch (rawType.toLowerCase()) { case "ecentityclass": return "EntityClass"; case "mixin": return "Mixin"; case "ecstructclass": return "StructClass"; case "eccustomattributeclass": return "CustomAttributeClass"; case "ecrelationshipclass": return "RelationshipClass"; case "ecenumeration": return "Enumeration"; case "kindofquantity": return "KindOfQuantity"; case "propertycategory": return "PropertyCategory"; case "unit": return "Unit"; case "invertedunit": return "InvertedUnit"; case "constant": return "Constant"; case "phenomenon": return "Phenomenon"; case "unitsystem": return "UnitSystem"; case "format": return "Format"; } return undefined; } getSchemaChildren() { const schemaMetadata = this._rawSchema.documentElement; return this.getElementChildren(schemaMetadata); } getSchemaItemProps(xmlElement) { const displayLabel = this.getOptionalAttribute(xmlElement, "displayLabel"); const description = this.getOptionalAttribute(xmlElement, "description"); return { description, label: displayLabel, }; } getClassProps(xmlElement) { const itemProps = this.getSchemaItemProps(xmlElement); const modifier = this.getOptionalAttribute(xmlElement, "modifier"); let baseClass = null; const baseClasses = this.getElementChildrenByTagName(xmlElement, "BaseClass"); if (baseClasses.length > 0) { // We are assuming here that the first BaseClass is the 'real' one - the rest are mixins // This is not a finalized approach as this could lead to unsupported schemas baseClass = baseClasses[0].textContent; } baseClass = baseClass ? this.getQualifiedTypeName(baseClass) : undefined; return { ...itemProps, modifier, baseClass, originalECSpecMajorVersion: this._ecSpecVersion?.readVersion, originalECSpecMinorVersion: this._ecSpecVersion?.writeVersion, }; } getRelationshipConstraintProps(xmlElement, isSource) { const constraintName = `${(isSource) ? "Source" : "Target"} Constraint of ${this._currentItemFullName}`; const multiplicity = this.getRequiredAttribute(xmlElement, "multiplicity", `The ${constraintName} is missing the required 'multiplicity' attribute.`); const roleLabel = this.getRequiredAttribute(xmlElement, "roleLabel", `The ${constraintName} is missing the required 'roleLabel' attribute.`); const polymorphicString = this.getRequiredAttribute(xmlElement, "polymorphic", `The ${constraintName} is missing the required 'polymorphic' attribute.`); const polymorphic = this.parseBoolean(polymorphicString, `The ${constraintName} has an invalid 'polymorphic' attribute. It should either be "true" or "false".`); let abstractConstraint = this.getOptionalAttribute(xmlElement, "abstractConstraint"); if (undefined !== abstractConstraint) abstractConstraint = this.getQualifiedTypeName(abstractConstraint); const constraintClasses = new Array(); const constraintClassesResult = this.getElementChildrenByTagName(xmlElement, "Class"); if (constraintClassesResult.length < 1) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The ${constraintName} is missing the required Class tags.`); for (const constraintClass of constraintClassesResult) { let constraintClassId = constraintClass.getAttribute("class"); if (null === constraintClassId || 0 === constraintClassId.length) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The ${constraintName} has a Class that is missing the required 'class' attribute.`); constraintClassId = this.getQualifiedTypeName(constraintClassId); constraintClasses.push(constraintClassId); } return { multiplicity, roleLabel, polymorphic, abstractConstraint, constraintClasses, }; } getPropertyType(propType) { switch (propType) { case "ECNavigationProperty": return "navigationproperty"; case "ECStructProperty": return "structproperty"; case "ECArrayProperty": return "primitivearrayproperty"; case "ECStructArrayProperty": return "structarrayproperty"; case "ECProperty": return "primitiveproperty"; default: return undefined; } } getPropertyName(xmlElement) { return this.getRequiredAttribute(xmlElement, "propertyName", `An ECProperty in ${this._currentItemFullName} is missing the required 'propertyName' attribute.`); } getPropertyProps(xmlElement) { const propName = this.getPropertyName(xmlElement); const propType = this.getPropertyType(xmlElement.nodeName); if (propType === undefined) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The ECProperty ${this._currentItemFullName}.${propName} has an invalid type. ${propType} is not a valid ECProperty type.`); const label = this.getOptionalAttribute(xmlElement, "displayLabel"); const description = this.getOptionalAttribute(xmlElement, "description"); const readOnlyString = this.getOptionalAttribute(xmlElement, "readOnly"); let isReadOnly; if (readOnlyString) { isReadOnly = this.parseBoolean(readOnlyString, `The ECProperty ${this._currentItemFullName}.${propName} has an invalid 'readOnly' attribute. It should be either "true" or "false".`); } let category = this.getOptionalAttribute(xmlElement, "category"); const priority = this.getOptionalIntAttribute(xmlElement, "priority", `The ECProperty ${this._currentItemFullName}.${propName} has an invalid 'priority' attribute. It should be a numeric value.`); const inheritedString = this.getOptionalAttribute(xmlElement, "inherited"); let inherited; if (inheritedString) { inherited = this.parseBoolean(inheritedString, `The ECProperty ${this._currentItemFullName}.${propName} has an invalid 'inherited' attribute. It should be either "true" or "false".`); } let kindOfQuantity = this.getOptionalAttribute(xmlElement, "kindOfQuantity"); if (kindOfQuantity) kindOfQuantity = this.getQualifiedTypeName(kindOfQuantity); if (category) category = this.getQualifiedTypeName(category); return { name: propName, type: propType, description, label, isReadOnly, category, priority, inherited, kindOfQuantity, }; } getPropertyTypeName(xmlElement) { const propName = this.getPropertyName(xmlElement); const rawTypeName = this.getRequiredAttribute(xmlElement, "typeName", `The ECProperty ${this._currentItemFullName}.${propName} is missing the required 'typeName' attribute.`); // If not a primitive type, we must prepend the schema name. const primitiveType = parsePrimitiveType(rawTypeName); if (primitiveType) return rawTypeName; return this.getQualifiedTypeName(rawTypeName); } getPrimitiveOrEnumPropertyBaseProps(xmlElement, typeName) { const primitiveType = parsePrimitiveType(typeName); const propertyProps = this.getPropertyProps(xmlElement); const propName = propertyProps.name; const extendedTypeName = this.getOptionalAttribute(xmlElement, "extendedTypeName"); const minLength = this.getOptionalIntAttribute(xmlElement, "minimumLength", `The ECProperty ${this._currentItemFullName}.${propName} has an invalid 'minimumLength' attribute. It should be a numeric value.`); const maxLength = this.getOptionalIntAttribute(xmlElement, "maximumLength", `The ECProperty ${this._currentItemFullName}.${propName} has an invalid 'maximumLength' attribute. It should be a numeric value.`); let minValue; let maxValue; if (primitiveType === PrimitiveType.Double || primitiveType === PrimitiveType.Long) { minValue = this.getOptionalFloatAttribute(xmlElement, "minimumValue", `The ECProperty ${this._currentItemFullName}.${propName} has an invalid 'minimumValue' attribute. It should be a numeric value.`); maxValue = this.getOptionalFloatAttribute(xmlElement, "maximumValue", `The ECProperty ${this._currentItemFullName}.${propName} has an invalid 'maximumValue' attribute. It should be a numeric value.`); } else { minValue = this.getOptionalIntAttribute(xmlElement, "minimumValue", `The ECProperty ${this._currentItemFullName}.${propName} has an invalid 'minimumValue' attribute. It should be a numeric value.`); maxValue = this.getOptionalIntAttribute(xmlElement, "maximumValue", `The ECProperty ${this._currentItemFullName}.${propName} has an invalid 'maximumValue' attribute. It should be a numeric value.`); } return { ...propertyProps, extendedTypeName, minLength, maxLength, minValue, maxValue, }; } getPropertyMinAndMaxOccurs(xmlElement) { const propName = this.getPropertyName(xmlElement); const minOccurs = this.getOptionalIntAttribute(xmlElement, "minOccurs", `The ECProperty ${this._currentItemFullName}.${propName} has an invalid 'minOccurs' attribute. It should be a numeric value.`); const maxOccursStr = this.getOptionalAttribute(xmlElement, "maxOccurs"); let maxOccurs; if ("unbounded" === maxOccursStr) maxOccurs = 2147483647; // TODO: This should be using the INT32_MAX variable. else if (undefined !== maxOccursStr) { maxOccurs = parseInt(maxOccursStr, 10); if (isNaN(maxOccurs)) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `The ECProperty ${this._currentItemFullName}.${propName} has an invalid 'maxOccurs' attribute. It should be a numeric value.`); } return { minOccurs, maxOccurs }; } *getCustomAttributeProviders(xmlElement, type, _name) { const customAttributesResult = this.getElementChildrenByTagName(xmlElement, "ECCustomAttributes"); if (customAttributesResult.length < 1) return; const attributes = this.getElementChildren(customAttributesResult[0]); for (const attribute of attributes) { if ("ECClass" === type && "IsMixin" === attribute.tagName) continue; yield this.getCustomAttributeProvider(attribute); } } getCustomAttributeProvider(xmlCustomAttribute) { assert(this._ecSpecVersion !== undefined); let ns = xmlCustomAttribute.getAttribute("xmlns"); if (!ns) { assert(this._schemaName !== undefined); assert(this._schemaVersion !== undefined); ns = `${this._schemaName}.${this._schemaVersion}`; } if (null === ns || !this.isSchemaFullNameValidForVersion(ns, this._ecSpecVersion)) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `Custom attribute namespaces must contain a valid 3.2 full schema name in the form <schemaName>.RR.ww.mm.`); const schemaNameParts = ns.split("."); const className = `${schemaNameParts[0]}.${xmlCustomAttribute.tagName}`; const properties = this.getElementChildren(xmlCustomAttribute); const provider = (caClass) => { return this.addCAPropertyValues(caClass, properties); }; return [className, provider]; } addCAPropertyValues(caClass, propertyElements) { const instance = { className: caClass.fullName }; for (const propertyElement of propertyElements) { const value = this.readPropertyValue(propertyElement, caClass); if (value !== undefined) instance[propertyElement.tagName] = value; } return instance; } readPropertyValue(propElement, parentClass) { const propertyClass = parentClass.getPropertySync(propElement.tagName); if (!propertyClass) return; if (propertyClass.isArray()) return this.readArrayPropertyValue(propElement, propertyClass); let enumeration; if (propertyClass.isPrimitive()) { if (propertyClass.isEnumeration() && propertyClass.enumeration) { enumeration = propertyClass.schema.lookupItemSync(propertyClass.enumeration.fullName, Enumeration); if (!enumeration) throw new ECSchemaError(ECSchemaStatus.ClassNotFound, `The Enumeration class '${propertyClass.enumeration.fullName}' could not be found.`); } const primitiveType = enumeration && enumeration.type ? enumeration.type : (propertyClass).primitiveType; return this.readPrimitivePropertyValue(propElement, primitiveType); } if (propertyClass.isStruct()) return this.readStructPropertyValue(propElement, propertyClass.structClass); return undefined; } readArrayPropertyValue(propElement, propertyClass) { if (propertyClass.isPrimitive()) return this.readPrimitiveArrayValues(propElement, propertyClass.primitiveType); if (propertyClass.isStruct()) return this.readStructArrayValues(propElement, propertyClass); return undefined; } readPrimitiveArrayValues(propElement, primitiveType) { const typeName = primitiveTypeToString(primitiveType); const children = this.getElementChildrenByTagName(propElement, typeName); const values = []; for (const child of children) { const value = this.readPrimitivePropertyValue(child, primitiveType); values.push(value); } return values; } readStructArrayValues(propElement, propertyClass) { const children = this.getElementChildren(propElement); const values = []; for (const child of children) { const value = this.readStructPropertyValue(child, propertyClass.structClass); values.push(value); } return values; } readStructPropertyValue(propElement, structClass) { const structObj = {}; const children = this.getElementChildren(propElement); for (const child of children) { const value = this.readPropertyValue(child, structClass); if (value !== undefined) structObj[child.tagName] = value; } return structObj; } readPrimitivePropertyValue(propElement, primitiveType) { if (undefined === propElement.textContent || null === propElement.textContent) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `Primitive property '${propElement.tagName}' has an invalid property value.`); if (propElement.textContent === "" && primitiveType !== PrimitiveType.String) throw new ECSchemaError(ECSchemaStatus.InvalidSchemaXML, `Primitive property '${propElement.tagName}' has an invalid property value.`); // TODO: Mapping all primitive types to string, number and boolean // for now. Need to review with IModelJs. switch (primitiveType) { case PrimitiveType.String: case PrimitiveType.Binary: /** TODO - Currently treated as strings */ case PrimitiveType.IGeometry: /** TODO - Currently treated as strings */ return propElement.textContent; case PrimitiveType.DateTime: return this.getDatePropertyValue(propElement.textContent, propElement.tagName); case PrimitiveType.Point2d: return this.getPoint2DPropertyValue(propElement.textContent, propElement.tagName); case PrimitiveType.Point3d: return this.getPoint3DPropertyValue(propElement.textCon