Browse Source

xml parser/exporter: additional fields, bugfixes, refactoring

Martin Kunz 1 month ago
parent
commit
90af92d06c

+ 5 - 5
src/components/TheDynamics.vue

@@ -63,7 +63,7 @@ function getInstanceDecl(nid: string, mrType: string){
     if(aggStrings.includes(item.referenceType) && item.isForward){
       let mr = item.toNode.getModellingRule()||"";
       if(mr == mrType){
-        res.push(item.toNode.displayName||"");
+        res.push(item.toNode.browseName||"");
       }
     }
   })
@@ -76,9 +76,9 @@ function getNodeVersion(nid:string){
     return "";
   let refs = node.references;
   refs.forEach((ref)=>{
-    if(ref.toNode.displayName == "NodeVersion" && ref.isForward){
+    if(ref.toNode.browseName == "NodeVersion" && ref.isForward){
       return ref.toNode.nodeId.toString()      
-    }else if(ref.fromNode.displayName == "NodeVersion" && !ref.isForward){
+    }else if(ref.fromNode.browseName == "NodeVersion" && !ref.isForward){
       return ref.fromNode.nodeId.toString()
     }
   })
@@ -161,7 +161,7 @@ defineExpose({ okPressed })
           </div>
           <select class="form-select" aria-label="Default select example">
             <option v-for="option in getRefTypes()" :value="option.nodeId.toString()" v-bind:key="option.nodeId.toString()">
-              {{ option.displayName }}
+              {{ option.browseName }}
             </option>
           </select>
         </div>
@@ -184,7 +184,7 @@ defineExpose({ okPressed })
           </div>
           <select v-model="typedef" class="form-select" aria-label="Default select example" >
             <option v-for="option1 in getObjTypes('ns=0;i='+ ObjectIds.VariableTypesFolder)" :value="option1.nodeId.toString()" v-bind:key="option1.nodeId.toString()">
-              {{ option1.displayName }}
+              {{ option1.browseName }}
             </option>
           </select>
         </div>

+ 10 - 1
src/components/TheInstance.vue

@@ -12,6 +12,15 @@ const nameSpaceName = computed(() => {
   return store.addressSpace?.nst.getUri(nsIdx);
 })
 
+const displayName=computed(() => {
+  if(!selectedNode.value)
+    return "";
+  for( const dn of selectedNode.value.displayName) { //TODO: locale handling
+    return dn.text;
+  }
+  return "";
+})
+
 function okPressed() {
   console.log('TODO: Handle OK Button');
 }
@@ -25,7 +34,7 @@ defineExpose({ okPressed })
         <div class="input-group-prepend">
           <span class="input-group-text" id="inputGroup-sizing-default">DisplayName</span>
         </div>
-        <input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" v-model="selectedNode.displayName">
+        <input type="text" class="form-control" aria-label="Default" aria-describedby="inputGroup-sizing-default" v-model="displayName">
       </div>
       <div class="input-group mb-3">
         <div class="input-group-prepend">

+ 2 - 2
src/components/TheParent.vue

@@ -58,7 +58,7 @@ const refTypes = computed(():UABaseNode[]  => {
           <span class="input-group-text" id="inputGroup-sizing-default">Name</span>
         </div>
         <input readonly type="text" class="form-control" aria-label="Default"
-          aria-describedby="inputGroup-sizing-default" :value="selectedParent.displayName">
+          aria-describedby="inputGroup-sizing-default" :value="selectedParent.browseName">
           <button class="btn btn-light" @click="parentDialogOpen = true">...</button>
       </div>
       <div class="input-group mb-3">
@@ -67,7 +67,7 @@ const refTypes = computed(():UABaseNode[]  => {
         </div>
         <select class="form-select" aria-label="Selected ref. type" v-model="selectedRefType" >
           <option v-for="option in refTypes" :value="option.browseName" v-bind:key="option.browseName">
-            {{ option.displayName }}
+            {{ option.browseName }}
           </option>
         </select>
       </div>

+ 1 - 1
src/components/TreeItem.vue

@@ -57,7 +57,7 @@ function selectNode(node:UABaseNode) {
       <span v-if="isFolder" @click="toggle">[{{ isOpen ? '-' : '+' }}]</span>
       <span @click.stop="selectNode(model)"  class="itemtext" :class="[(isFiltered?'disabled':''), 
          selected?'selected':'' ]">
-        {{ model.displayName }}
+        {{ model.browseName }}
       </span>
     </div>
     <ul v-show="isOpen" v-if="isFolder && model">

+ 90 - 42
src/ua/UABaseNode.ts

@@ -4,42 +4,33 @@ import { UAReference } from "./UAReference";
 import { assert } from "@/util/assert";
 import { XMLElem, type IToXML } from "@/util/XmlElem";
 import { UARolePermission } from "./UARolePermission";
-import { UAExtension } from "./UAExtension";
-import { UAUserWriteMask } from "./UAUserWriteMask";
-import { UAWriteMask } from "./UAWriteMask";
-import { UAAccessRestriction } from "./UAAccessRestriction";
 import { type IAddressSpace } from "./IAddressSpace";
 import { ReferenceTypeIds } from "./opcua_node_ids";
+import { UALocalizedText } from "./UALocalizedText";
 export class UABaseNode implements IToXML{
 
     public static nullBaseNode=new UABaseNode({browseName: "", addressSpace: {} as IAddressSpace, nodeId: NodeId.nullNodeId});
 
-
-    public nodeId: NodeId;
+    //Default values from xsd
+    public nodeId!: NodeId;
     public nodeClass="UABaseNode";
-    public browseName: string;
-    public addressSpace: IAddressSpace
-    public displayName?: string; //LocText
-    public description?: string; //LocText
-    public symbolicName?: string; //SymbolicName
-    public releaseStatus?: string; //ReleaseStatus
-    public hasNoPermissions?: boolean; 
-    public writeMask?: UAWriteMask; 
-    public userWriteMask?: UAUserWriteMask;
-    public category?: string;
+    public browseName!: string;
+    public addressSpace!: IAddressSpace
+    public displayName: UALocalizedText[]=[]; 
+    public description: UALocalizedText[]=[];
+    public symbolicName?: string;
+    public releaseStatus: string="Released";
+    public hasNoPermissions: boolean=false; 
+    public writeMask: number=0; 
+    public userWriteMask: number=0;
+    public category: string[]=[];
     public documentation?: string;
-    public accessRestriction?: UAAccessRestriction; 
+    public accessRestriction?: number; 
     public rolePermissions: UARolePermission[]=[]; 
-    public extensions: UAExtension[]=[];
     public references: UAReference[]=[];
-    public opts: string[]=[]; 
     
     constructor(options: UABaseNodeOptions) {
-        this.nodeId=options.nodeId;
-        this.browseName=options.browseName;
-        this.addressSpace=options.addressSpace;
-        this.displayName=options.displayName;
-        this.references=options.references||[];     
+        Object.assign(this, options);  
     }
 
 
@@ -130,7 +121,7 @@ export class UABaseNode implements IToXML{
     getModellingRule(): string|undefined{
         this.references.forEach((ref)=>{
             if(ref.referenceType == "HasModellingRule"){
-                return ref.toNode.displayName;
+                return ref.toNode.browseName;
             }
         });
         return undefined;
@@ -154,28 +145,77 @@ export class UABaseNode implements IToXML{
         }
     }
 
-
-
     static fromXML(xmlObject: any, addressSpace: IAddressSpace): UABaseNode{
-        const xmlReferences=xmlObject['References']||[];
         const references:UAReference[]=[];
         const nodeId=coerceNodeId(xmlObject['@_NodeId']);
-        for(const xmlref of xmlReferences.Reference||[]) {
+        for(const xmlref of xmlObject['References']?.['Reference']||[]) {
             references.push(UAReference.fromXML(xmlref, nodeId));
         }
-        const ua=new UABaseNode({nodeId: nodeId,
+        const displayNames:UALocalizedText[]=[];
+        for(const xmldn of xmlObject['DisplayName']||[]) {
+            displayNames.push(UALocalizedText.fromXML("DisplayName", xmldn));
+        }
+        const descriptions:UALocalizedText[]=[];
+        for(const desc of xmlObject['Description']||[]) {
+            descriptions.push(UALocalizedText.fromXML("Description", desc));
+        }        
+        const permissions:UARolePermission[]=[];
+        for(const perm of xmlObject['RolePermissions']?.['RolePermission']||[]) {
+            permissions.push(UARolePermission.fromXML(perm));
+        }
+        const ua=new UABaseNode({addressSpace: addressSpace,
+                                nodeId: nodeId,
                                 browseName: xmlObject['@_BrowseName'], 
-                                displayName: xmlObject['DisplayName']['#text'], 
+                                writeMask: Number(xmlObject['@_WriteMask']||0),
+                                userWriteMask: Number(xmlObject['@_UserWriteMask']||0),
+                                accessRestriction: xmlObject['@_AccessRestrictions'],
+                                hasNoPermissions: xmlObject['@_HasNoPermissions']||false,
+                                symbolicName: xmlObject['@_SymbolicName'],
+                                releaseStatus: xmlObject['@_ReleaseStatus']||"Released",
+                                displayName:  displayNames, 
+                                description: descriptions,
+                                category: xmlObject['Category'],
+                                documentation: xmlObject['Documentation']?.['#text'],
                                 references: references,
-                                addressSpace: addressSpace
-                            }); 
-
-
+                                rolePermissions: permissions,
+                            });
         return ua;
     }
 
-    toXML(_lnst: NamespaceTable, _gnst: NamespaceTable): XMLElem {
-        throw new Error("UABaseNode has no xml rep; implement in subtype.");
+
+    toXML(lnst: NamespaceTable, gnst: NamespaceTable): XMLElem {
+        const nid=UABaseNode.localNodeId(this.nodeId, lnst, gnst);
+        const elem =new XMLElem('UAVariable');
+        elem.attr('NodeId', nid.toString())
+            .attr('BrowseName', this.browseName)
+            .attr('WriteMask', this.writeMask.toString())
+            .attr('UserWriteMask', this.userWriteMask.toString())
+            .attr('AccessRestriction', this.accessRestriction?.toString())
+            .attr('HasNoPermissions', this.hasNoPermissions)
+            .attr('SymbolicName', this.symbolicName)
+            .attr('ReleaseStatus',this.releaseStatus)
+            .attr('Documentation', this.documentation);
+        
+        for(const c in this.category) {
+            elem.attr('Category', c);
+        }
+        const refs=new XMLElem('References');
+        for(const ref of this.references) {
+            if(ref.fromNode.nodeId.toString()==this.nodeId.toString()) //on load resolveReferences() duplicates references to both sides. skip them for export
+                refs.add(ref.toXML(lnst, gnst));
+        }
+        if(refs.elements.length>0)
+            elem.add(refs);
+        for(const dn of this.displayName)
+            elem.add(dn.toXML());
+        for(const desc of this.description)
+            elem.add(desc.toXML());
+        const perms=new XMLElem('RolePermissions')
+        for(const perm of this.rolePermissions)
+                perms.add(perm.toXML());
+        if(perms.elements.length>0)
+            elem.add(perms);
+        return elem;
     }
 
     static localNodeId(nodeId: NodeId, lnst: NamespaceTable, gnst: NamespaceTable) {
@@ -191,11 +231,19 @@ export class UABaseNode implements IToXML{
 
 
 export interface UABaseNodeOptions {
-    browseName: string;
+    addressSpace: IAddressSpace;
     nodeId: NodeId;
-    namespace?: string;
+    browseName: string;
+    writeMask?: number; 
+    userWriteMask?: number;
+    accessRestriction?: number; 
+    hasNoPermissions?: boolean; 
+    symbolicName?: string;
+    releaseStatus?: string;
+    displayName?: UALocalizedText[];
+    description?: UALocalizedText[];
+    category?: string[];
+    documentation?: string;
     references?: UAReference[];
-    displayName?: string;
-    description?: string;
-    addressSpace: IAddressSpace;
+    rolePermissions?: UARolePermission[]; 
 }

+ 0 - 3
src/ua/UAExtension.ts

@@ -1,3 +0,0 @@
-export class UAExtension  {
-
-}

+ 13 - 0
src/ua/UALocalizedText.ts

@@ -0,0 +1,13 @@
+import { XMLElem, type IToXML } from "@/util/XmlElem";
+export class UALocalizedText implements IToXML{
+    constructor(public tagName: string, 
+                public text: string,
+                public locale: string) {
+    }
+    toXML(): XMLElem {
+        return new XMLElem(this.tagName, this.text).attr('Locale', this.locale);  
+    }
+    static fromXML(tagName: string, xmlObject: any): UALocalizedText {
+        return new UALocalizedText(tagName, xmlObject['#text'], xmlObject['@_Locale']);
+    }
+}

+ 3 - 16
src/ua/UAMethod.ts

@@ -11,24 +11,11 @@ export class UAMethod extends UABaseNode {
 
     static  fromXML(uaMethod: any, addressSpace: IAddressSpace): UAMethod{
         const bn=super.fromXML(uaMethod, addressSpace)
-        return new UAMethod({nodeId: bn.nodeId, 
-                            browseName: bn.browseName, 
-                            displayName: bn.displayName,
-                            references: bn.references,
-                            addressSpace: addressSpace});
+        return new UAMethod(bn as UABaseNodeOptions);
     }
 
     toXML(lnst:NamespaceTable, gnst:NamespaceTable): XMLElem {
-        const nid=UABaseNode.localNodeId(this.nodeId, lnst, gnst);
-        const elem =new XMLElem('UAMethod');
-        elem.attr('NodeId', nid.toString())
-            .attr('BrowseName', this.browseName)
-            .elem('DisplayName', this.displayName);
-        const refs=elem.add(new XMLElem('References'))
-        for(const ref of this.references) {
-            if(ref.fromNode.nodeId.toString()==this.nodeId.toString()) //on load resolveReferences() duplicates references to both sides. skip them for export
-                refs.add(ref.toXML(lnst, gnst));
-        }
+        const elem =super.toXML(lnst, gnst);
         return elem;
     }
 
@@ -36,7 +23,7 @@ export class UAMethod extends UABaseNode {
         let res:any = "";
         this.references.forEach((ref)=>{
             if(ref.referenceType == "HasModellingRule"){
-                res = ref.toNode.displayName;
+                res = ref.toNode.browseName;
             }
         })
         return res;

+ 24 - 0
src/ua/UANodeSet.ts

@@ -85,16 +85,40 @@ export class UANodeSet implements IToXML{
                     case 'UANodeSet.Models.Model':
                     case 'UANodeSet.UAObject':
                     case 'UANodeSet.UAObject.References.Reference':
+                    case 'UANodeSet.UAObject.DisplayName':
+                    case 'UANodeSet.UAObject.Description':
+                    case 'UANodeSet.UAObject.Category':
+                    case 'UANodeSet.UAObject.RolePermissions.RolePermission':
                     case 'UANodeSet.UAObjectType':
                     case 'UANodeSet.UAObjectType.References.Reference':
+                    case 'UANodeSet.UAObjectType.DisplayName':
+                    case 'UANodeSet.UAObjectType.Description':
+                    case 'UANodeSet.UAObjectType.Category':
+                    case 'UANodeSet.UAObjectType.RolePermissions.RolePermission':
                     case 'UANodeSet.UAMethod':
                     case 'UANodeSet.UAMethod.References.Reference':
+                    case 'UANodeSet.UAMethod.DisplayName':
+                    case 'UANodeSet.UAMethod.Description':
+                    case 'UANodeSet.UAMethod.Category':
+                    case 'UANodeSet.UAMethod.RolePermissions.RolePermission':
                     case 'UANodeSet.UAVariable':
                     case 'UANodeSet.UAVariable.References.Reference':
+                    case 'UANodeSet.UAVariable.DisplayName':
+                    case 'UANodeSet.UAVariable.Description':
+                    case 'UANodeSet.UAVariable.Category':
+                    case 'UANodeSet.UAVariable.RolePermissions.RolePermission':
                     case 'UANodeSet.UAVariableType':
                     case 'UANodeSet.UAVariableType.References.Reference':
+                    case 'UANodeSet.UAVariableType.DisplayName':
+                    case 'UANodeSet.UAVariableType.Description':
+                    case 'UANodeSet.UAVariableType.Category':
+                    case 'UANodeSet.UAVariableType.RolePermissions.RolePermission':
                     case 'UANodeSet.UAReferenceType':
                     case 'UANodeSet.UAReferenceType.References.Reference':
+                    case 'UANodeSet.UAReferenceType.DisplayName':
+                    case 'UANodeSet.UAReferenceType.Description':
+                    case 'UANodeSet.UAReferenceType.Category':
+                    case 'UANodeSet.UAReferenceType.RolePermissions.RolePermission':
                         return true;
                     default:
                         return false;

+ 2 - 16
src/ua/UAObject.ts

@@ -14,27 +14,13 @@ export class UAObject extends UABaseNode {
 
     static  fromXML(uaObject: any, addressSpace: IAddressSpace): UAObject{
         const bn=super.fromXML(uaObject, addressSpace)
-        return new UAObject({nodeId: bn.nodeId, 
-                            browseName: bn.browseName, 
-                            displayName: bn.displayName,
-                            references: bn.references,
-                            addressSpace: addressSpace});
+        return new UAObject(bn as UABaseNodeOptions);
     }
 
     toXML(lnst:NamespaceTable, gnst:NamespaceTable): XMLElem {
-        const nid=UABaseNode.localNodeId(this.nodeId, lnst, gnst);
-        const elem =new XMLElem('UAObject');
-        elem.attr('NodeId', nid.toString())
-            .attr('BrowseName', this.browseName)
-            .elem('DisplayName', this.displayName);
-        const refs=elem.add(new XMLElem('References'))
-        for(const ref of this.references) {
-            if(ref.fromNode.nodeId.toString()==this.nodeId.toString()) //on load resolveReferences() duplicates references to both sides. skip them for export
-                refs.add(ref.toXML(lnst, gnst));
-        }
+        const elem =super.toXML(lnst, gnst);
         return elem;
     }
-    
 }
 
 export interface UAObjectNodeOptions extends UABaseNodeOptions{

+ 3 - 21
src/ua/UAObjectType.ts

@@ -6,7 +6,6 @@ import type { IAddressSpace } from "./IAddressSpace";
 export class UAObjectType extends UABaseNode{
     public isAbstract: boolean;
   
-
     constructor(options: UAObjectTypeOptions) {
         super(options)
         this.isAbstract=options.isAbstract||false;
@@ -14,31 +13,14 @@ export class UAObjectType extends UABaseNode{
 
     static  fromXML(xmlObjType: any, addressSpace: IAddressSpace): UAObjectType{
         const bn=super.fromXML(xmlObjType, addressSpace)
-        return new UAObjectType({nodeId: bn.nodeId, 
-                            browseName: bn.browseName, 
-                            displayName: bn.displayName,
-                            references: bn.references,
-                            isAbstract: xmlObjType['@_IsAbstract']==='true',
-                            addressSpace: addressSpace});
+        return new UAObjectType(bn as UABaseNodeOptions);
     }
 
     toXML(lnst:NamespaceTable, gnst:NamespaceTable): XMLElem {
-        const nid=UABaseNode.localNodeId(this.nodeId, lnst, gnst);
-        const elem =new XMLElem('UAObjectType');
-        elem.attr('NodeId', nid.toString())
-            .attr('BrowseName', this.browseName)
-            .attr('IsAbstract', this.isAbstract)
-            .elem('DisplayName', this.displayName);
-        const refs=elem.add(new XMLElem('References'))
-        for(const ref of this.references) {
-            if(ref.fromNode.nodeId.toString()==this.nodeId.toString()) //on load resolveReferences() duplicates references to both sides. skip them for export
-                refs.add(ref.toXML(lnst, gnst));
-        }
+        const elem =super.toXML(lnst, gnst);
+        elem.attr('IsAbstract', this.isAbstract);
         return elem;
     }
-
-
-
 }
 
 export interface UAObjectTypeOptions extends UABaseNodeOptions{

+ 3 - 17
src/ua/UAReferenceType.ts

@@ -13,26 +13,12 @@ export class UAReferenceType extends UABaseNode{
 
     static  fromXML(xmlRefType: any, addressSpace: IAddressSpace): UAReferenceType{
         const bn=super.fromXML(xmlRefType, addressSpace)
-        return new UAReferenceType({nodeId: bn.nodeId, 
-                            browseName: bn.browseName, 
-                            displayName: bn.displayName,
-                            references: bn.references,
-                            isAbstract: xmlRefType['@_IsAbstract']==='true',
-                            addressSpace: addressSpace});
+        return new UAReferenceType(bn as UABaseNodeOptions);
     }
 
     toXML(lnst:NamespaceTable, gnst:NamespaceTable): XMLElem {
-        const nid=UABaseNode.localNodeId(this.nodeId, lnst, gnst);
-        const elem =new XMLElem('UAReferenceType');
-        elem.attr('NodeId', nid.toString())
-            .attr('BrowseName', this.browseName)
-            .attr('IsAbstract', this.isAbstract)
-            .elem('DisplayName', this.displayName);
-        const refs=elem.add(new XMLElem('References'))
-        for(const ref of this.references) {
-            if(ref.fromNode.nodeId.toString()==this.nodeId.toString()) //on load resolveReferences() duplicates references to both sides. skip them for export
-                refs.add(ref.toXML(lnst, gnst));
-        }
+        const elem=super.toXML(lnst, gnst);
+        elem.attr('IsAbstract', this.isAbstract);
         return elem;
     }
 }

+ 12 - 6
src/ua/UARolePermission.ts

@@ -1,6 +1,12 @@
-
-
-export class UARolePermission  {
-
-    
-}
+import { XMLElem, type IToXML } from "@/util/XmlElem";
+export class UARolePermission implements IToXML{
+    constructor(public nodeId: string,
+                public rolePermission: number) {
+    }
+    toXML(): XMLElem {
+        return new XMLElem('RolePermission', this.nodeId).attr('Permissions', this.rolePermission.toString());  
+    }
+    static fromXML(xmlObject: any): UARolePermission {
+        return new UARolePermission( xmlObject['@_Permissions'], Number(xmlObject['#text']));
+    }
+}

+ 0 - 3
src/ua/UAUserWriteMask.ts

@@ -1,3 +0,0 @@
-
-
-export class UAUserWriteMask  {}

+ 2 - 15
src/ua/UAVariable.ts

@@ -15,24 +15,11 @@ export class UAVariable extends UABaseNode {
 
     static  fromXML(uaObject: any, addressSpace: IAddressSpace): UAVariable{
         const bn=super.fromXML(uaObject, addressSpace)
-        return new UAVariable({nodeId: bn.nodeId, 
-                                browseName: bn.browseName, 
-                                displayName: bn.displayName, 
-                                references: bn.references,
-                                addressSpace: addressSpace});
+        return new UAVariable(bn as UABaseNodeOptions);
     }
 
     toXML(lnst:NamespaceTable, gnst:NamespaceTable): XMLElem {
-        const nid=UABaseNode.localNodeId(this.nodeId, lnst, gnst);
-        const elem =new XMLElem('UAVariable');
-        elem.attr('NodeId', nid.toString())
-            .attr('BrowseName', this.browseName)
-            .elem('DisplayName', this.displayName);
-        const refs=elem.add(new XMLElem('References'))
-        for(const ref of this.references) {
-            if(ref.fromNode.nodeId.toString()==this.nodeId.toString()) //on load resolveReferences() duplicates references to both sides. skip them for export
-                refs.add(ref.toXML(lnst, gnst));
-        }
+        const elem=super.toXML(lnst, gnst);
         return elem;
     }
     

+ 5 - 21
src/ua/UAVariableType.ts

@@ -9,36 +9,20 @@ export class UAVariableType extends UABaseNode{
     constructor(options: UAVariableTypeOptions) {
         super(options)
         this.isAbstract=options.isAbstract||false;
-       
- 
     }
 
     static  fromXML(xmlObjType: any, addressSpace: IAddressSpace): UAVariableType{
         const bn=super.fromXML(xmlObjType, addressSpace)
-        return new UAVariableType({nodeId: bn.nodeId, 
-                            browseName: bn.browseName, 
-                            displayName: bn.displayName,
-                            references: bn.references,
-                            isAbstract: xmlObjType['@_IsAbstract']==='true',
-                            addressSpace: addressSpace});
+        const uavt= new UAVariableType(bn as UABaseNodeOptions);
+        uavt.isAbstract= xmlObjType['@_IsAbstract']==='true';
+        return uavt;
     }
 
     toXML(lnst:NamespaceTable, gnst:NamespaceTable): XMLElem {
-        const nid=UABaseNode.localNodeId(this.nodeId, lnst, gnst);
-        const elem =new XMLElem('UAVariableType');
-        elem.attr('NodeId', nid.toString())
-            .attr('BrowseName', this.browseName)
-            .attr('IsAbstract', this.isAbstract)
-            .elem('DisplayName', this.displayName);
-        const refs=elem.add(new XMLElem('References'))
-        for(const ref of this.references) {
-            if(ref.fromNode.nodeId.toString()==this.nodeId.toString()) //on load resolveReferences() duplicates references to both sides. skip them for export
-                refs.add(ref.toXML(lnst, gnst));
-        }
+        const elem =super.toXML(lnst, gnst);
+        elem.attr('IsAbstract', this.isAbstract);
         return elem;
     }
-
-    
 }
 
 export interface UAVariableTypeOptions extends UABaseNodeOptions{

+ 0 - 1
src/ua/UAWriteMask.ts

@@ -1 +0,0 @@
-export class UAWriteMask  {}

+ 3 - 1
src/util/XmlElem.ts

@@ -23,9 +23,11 @@ export class XMLElem {
         return this;
     }
 
-    public elem(name:string, value: string|boolean|undefined|null): XMLElem {
+    public elem(name:string, value: XMLElem|string|boolean|undefined|null): XMLElem {
         if(value===undefined||value===null)
             return this; //skip undefined/null values
+        if(value instanceof XMLElem)
+            this.elements.push(value);
         this.elements.push(new XMLElem(name, value?.toString()));
         return this;
     }