/**
 * Created by Eddy Spreeuwers at 11 march 2018
 */ import { ClassDefinition , createFile , EnumDefinition , FileDefinition } from "ts-code-generator" ; import { DOMParser } from "xmldom-reborn" ; import { ASTNode , getFieldType , NEWLINE } from "./parsing" ; import { capFirst , log } from "./xml-utils" ; import { regexpPattern2typeAlias , A2Z } from './regexp2aliasType' ; import { XsdGrammar } from "./xsd-grammar" ; let XMLNS = 'xmlns' ; let definedTypes ; const GROUP_PREFIX = 'group_' ; const XSD_NS = "http://www.w3.org/2001/XMLSchema" ; const CLASS_PREFIX = "." ; const defaultSchemaName = 'Schema' ; const groups = { } ; const ns2modMap = { } as Map < string , string > ; const primitive = /(string|number)/i ; const namespaces = { default : "" , xsd : "xs" } ; let targetNamespace = 's1' ; function a2z ( p ) { return ( p . toLowerCase ( ) == p ) ? A2Z . toLowerCase ( ) : A2Z ; } function capfirst ( s = "" ) { return s [ 0 ] ?. toUpperCase ( ) + s ?. substring ( 1 ) ; } function lowfirst ( s = "" ) { return s [ 0 ] ?. toLowerCase ( ) + s ?. substring ( 1 ) ; } function choiceBody ( m , names ) { const name = m . attr . ref || m . attr . fieldName ; const result = names . filter ( ( n ) => n !== name ) . map ( ( n ) => `delete((this as any).${ n });` ) . join ( '\n' ) ; return result + `\n(this as any).${ name } = arg;\n` ; } function addNewImport ( fileDef , ns ) { if ( fileDef . imports . filter ( i => i . starImportName === ns ) . length === 0 ) { const modulePath = ns2modMap [ ns ] ; if ( modulePath ) { log ( 'addNewImport: ' , ns , modulePath ) ; fileDef . addImport ( { moduleSpecifier : modulePath , starImportName : ns } ) ; } } } function addClassForASTNode ( fileDef , astNode , indent = '' ) { const c = fileDef . addClass ( { name : capfirst ( astNode . name ) } ) ; if ( astNode . nodeType === 'Group' ) { c . isAbstract = true ; } if ( astNode . attr ?. base ) { let superClass = '' ; let [ ns , qname ] = astNode . attr . base . split ( ':' ) ; if ( ns === targetNamespace ) { superClass = capfirst ( qname ) ; } else if ( qname ) { superClass = ns . toLowerCase ( ) + '.' + capfirst ( qname ) ; } else { superClass = capfirst ( ns ) ; } c . addExtends ( superClass ) ; } log ( indent + 'created: ' , astNode . name , ', fields: ' , astNode ?. children ?. length ) ; let fields = ( astNode . children || [ ] ) . filter ( ( f ) => f ) ; fields . filter ( ( f ) => f . nodeType === "Fields" ) . forEach ( ( f ) => { log ( indent + 'adding named fields:' , f . name ) ; let superClass = '' ; if ( f . attr . ref . indexOf ( ':' ) >= 0 ) { const [ ns , qname ] = f . attr . ref . split ( ':' ) ; log ( indent + 'split ns, qname: ' , ns , qname ) ; if ( ns === targetNamespace ) { superClass = capfirst ( qname ) ; } else { superClass = ns . toLowerCase ( ) + '.' + capfirst ( qname ) ; addNewImport ( fileDef , ns ) ; } } else { superClass = capfirst ( f . attr . ref ) ; } c . addExtends ( superClass ) ; } ) ; fields . filter ( ( f ) => f . nodeType === "Reference" ) . forEach ( ( f ) => { log ( indent + 'adding fields for Reference: ' , f . attr . ref ) ; const typePostFix = ( f . attr . array ) ? "[]" : "" ; const namePostFix = ( f . attr . array ) ? "?" : "" ; const [ ns , localName ] = ( /:/ . test ( f . attr . ref ) ) ? f . attr . ref ?. split ( ':' ) : [ null , f . attr . ref ] ; const refName = localName + namePostFix ; let refType = '' ; if ( ns === targetNamespace ) { refType = capfirst ( localName + typePostFix ) ; } else { refType = ( ( ns ) ? ns + '.' : '' ) + capfirst ( localName + typePostFix ) ; } const classType = fileDef . getClass ( refType ) ; c . addProperty ( { name : refName , type : refType , scope : "protected" } ) ; } ) ; fields . filter ( ( f ) => f . nodeType === "choice" ) . forEach ( ( f ) => { const names = f . children ?. map ( ( i ) => i . attr . fieldName || i . attr . ref ) ; log ( indent + 'adding methods for choice' , names . join ( ',' ) ) ; f . children ?. forEach ( ( m ) => { const methodName = m . attr . fieldName || m . attr . ref ; const method = c . addMethod ( { name : methodName , returnType : 'void' , scope : 'protected' } ) ; method . addParameter ( { name : 'arg' , type : m . attr . fieldType || capfirst ( m . attr . ref ) } ) ; method . onWriteFunctionBody = ( w ) => { w . write ( choiceBody ( m , names ) ) ; } ; method . onBeforeWrite = ( w ) => w . write ( '//choice\n' ) ; } ) ; log ( indent + 'added methods' , c . methods . map ( ( m ) => m . name ) . join ( ',' ) ) ; } ) ; fields . filter ( ( f ) => f . nodeType === "Field" ) . forEach ( ( f ) => { log ( indent + 'adding field:' , { name : f . attr . fieldName , type : f . attr . fieldType } ) ; let xmlns = "" ; let fldType = f . attr . fieldType ; const typeParts = f . attr . fieldType . split ( '.' ) ; if ( typeParts . length === 2 ) { xmlns = typeParts [ 0 ] ; fldType = typeParts [ 1 ] ; if ( xmlns !== targetNamespace ) { addNewImport ( fileDef , xmlns ) ; } } const undefinedType = definedTypes . indexOf ( fldType ) < 0 ; log ( 'defined: ' , fldType , undefinedType ) ; if ( undefinedType && namespaces . default && namespaces . default !== XSD_NS && 'xmlns' !== targetNamespace ) { fldType = getFieldType ( f . attr . type , ( 'xmlns' !== targetNamespace ) ? XMLNS : null ) ; } const classType = fileDef . getClass ( fldType ) ; c . addProperty ( { name : f . attr . fieldName , type : fldType , scope : "protected" } ) ; log ( indent + 'nested class' , f . attr . fieldName , JSON . stringify ( f . attr . nestedClass ) ) ; if ( f . attr . nestedClass ) { addClassForASTNode ( fileDef , f . attr . nestedClass , indent + ' ' ) ; } } ) ; return c ; } export class ClassGenerator { public types : string [ ] = [ ] ; public schemaName = "schema" ; public xmlnsName = "xmlns" ; private fileDef = createFile ( { classes : [ ] } ) ; private verbose = false ; private pluralPostFix = 's' ; private dependencies : Map < string , string > ; private importMap : string [ ] = [ ] ; private targetNamespace = 's1' ; constructor ( depMap ? , private classPrefix = CLASS_PREFIX ) { this . dependencies = depMap || { } as Map < string , string > ; ( Object as any ) . assign ( ns2modMap , depMap ) ; log ( JSON . stringify ( this . dependencies ) ) ; } public generateClassFileDefinition ( xsd , pluralPostFix = 's' , verbose ? ) { const fileDef = createFile ( ) ; this . verbose = verbose ; this . pluralPostFix = pluralPostFix ; this . log ( '--------------------generating classFile definition for----------------------------------' ) ; this . log ( '' ) ; this . log ( xsd ) ; this . log ( '' ) ; this . log ( '-------------------------------------------------------------------------------------' ) ; if ( ! xsd ) { return fileDef ; } const ast = this . parseXsd ( xsd ) ; if ( ! ast ) { return fileDef ; } XMLNS = this . xmlnsName ; const xsdNsAttr = Object . keys ( ast . attr || [ ] ) . filter ( n => ast . attr [ n ] === XSD_NS ) . shift ( ) ; const xsdNs = xsdNsAttr . replace ( /^\w+:/ , '' ) ; const defNs = ast . attr . xmlns ; targetNamespace = Object . keys ( ast . attr || [ ] ) . filter ( n => ast . attr [ n ] === ast . attr . targetNamespace && ( n != "targetNamespace" ) ) . shift ( ) ; targetNamespace = targetNamespace ?. replace ( /^\w+:/ , '' ) ; log ( 'xsd namespace:' , xsdNs ) ; log ( 'def namespace:' , defNs ) ; log ( 'xsd targetnamespace:' , targetNamespace ) ; const typeAliases = { } ; namespaces . xsd = xsdNs ; namespaces . default = defNs ; if ( defNs && ( defNs !== XSD_NS ) ) addNewImport ( fileDef , XMLNS ) ; Object . keys ( groups ) . forEach ( ( key ) => delete ( groups [ key ] ) ) ; log ( 'AST:\n' , JSON . stringify ( ast , null , 3 ) ) ; const schemaClass = createFile ( ) . addClass ( { name : capfirst ( ast ?. name || defaultSchemaName ) } ) ; const children = ast ?. children || [ ] ; definedTypes = children . map ( c => c . name ) ; log ( 'definedTypes: ' , JSON . stringify ( definedTypes ) ) ; children . filter ( ( t ) => t . nodeType === 'AliasType' ) . forEach ( ( t ) => { let aliasType = getFieldType ( t . attr . type , null ) ; log ( 'alias type: ' , t . name , ': ' , t . attr . type , '->' , aliasType , '\tattribs:' , t . attr ) ; if ( t . attr . pattern ) { aliasType = regexpPattern2typeAlias ( t . attr . pattern , aliasType , t . attr ) ; } if ( t . attr . minInclusive && t . attr . maxInclusive ) { const x1 = parseInt ( t . attr . minInclusive ) ; const x2 = parseInt ( t . attr . maxInclusive ) ; const nrs = [ ] ; if ( ( x2 - x1 ) < 100 ) { for ( let n = x1 ; n <= x2 ; n ++ ) { nrs . push ( n ) ; } aliasType = nrs . join ( '|' ) ; } } const [ ns , localName ] = aliasType . split ( '.' ) ; if ( targetNamespace === ns && t . name === localName ) { log ( 'skipping alias:' , aliasType ) ; } else { if ( ns === targetNamespace ) { aliasType = capfirst ( localName ) ; } log ( 'circular refs:' , aliasType , t . name . toLowerCase ( ) === aliasType . toLowerCase ( ) ) ; if ( t . name . toLowerCase ( ) !== aliasType . toLowerCase ( ) ) { if ( primitive . test ( aliasType ) ) { aliasType = aliasType . toLowerCase ( ) ; } typeAliases [ capfirst ( t . name ) ] = aliasType ; } } if ( t . attr . element ) { schemaClass . addProperty ( { name : lowfirst ( t . name ) , type : capfirst ( t . name ) } ) ; } } ) ; fileDef . classes . push ( schemaClass ) ; children . filter ( ( t ) => t . nodeType === 'Group' ) . forEach ( ( t ) => { groups [ t . name ] = t ; log ( 'storing group:' , t . name ) ; addClassForASTNode ( fileDef , t ) ; } ) ; children . filter ( ( t ) => t . nodeType === 'Class' ) . forEach ( ( t ) => { const c = addClassForASTNode ( fileDef , t ) ; if ( t . attr . element ) { schemaClass . addProperty ( { name : lowfirst ( t . name ) , type : capfirst ( t . name ) } ) ; } } ) ; children . filter ( ( t ) => t . nodeType === 'Enumeration' ) . forEach ( ( t ) => { const enumDef = fileDef . addEnum ( { name : capFirst ( t . name ) } ) ; t . attr . values . forEach ( ( m ) => { enumDef . addMember ( { name : m . attr . value . replace ( '+' , '_' ) , value : `"${ m . attr . value }"` as any } ) ; } ) ; if ( t . attr . element ) { schemaClass . addProperty ( { name : lowfirst ( t . name ) , type : capfirst ( t . name ) } ) ; } } ) ; const tmp = this . makeSortedFileDefinition ( fileDef . classes , fileDef ) ; Object . keys ( typeAliases ) . forEach ( k => { fileDef . addTypeAlias ( { name : k , type : typeAliases [ k ] , isExported : true } ) ; } ) ; fileDef . classes = tmp . classes ; return fileDef ; } private findAttrValue ( node , attrName ) { return node ?. attributes ?. getNamedItem ( attrName ) ?. value ; } private nodeName ( node ) { return this . findAttrValue ( node , 'name' ) ; } private findChildren ( node ) { const result = [ ] ; let child = node ?. firstChild ; while ( child ) { if ( ! /function Text/ . test ( "" + child . constructor ) ) { result . push ( child as HTMLElement ) ; } child = child . nextSibling ; } return result ; } private findFirstChild ( node ) { return this . findChildren ( node ) [ 0 ] ; } private parseXsd ( xsd ) { const xsdGrammar = new XsdGrammar ( this . schemaName ) ; const xmlDom = new DOMParser ( ) . parseFromString ( xsd , 'application/xml' ) ; const xmlNode = xmlDom ?. documentElement ; return xsdGrammar . parse ( xmlNode ) ; } private log ( message ? , ... optionalParams ) { if ( this . verbose ) { console . log . apply ( console , [ message ] . concat ( optionalParams ) ) ; } } private makeSortedFileDefinition ( sortedClasses , fileDef ) { const outFile = createFile ( { classes : [ ] } ) ; for ( const ns in this . importMap ) { log ( 'addImport: ' , ns , this . importMap [ ns ] ) ; outFile . addImport ( { moduleSpecifier : this . importMap [ ns ] , starImportName : ns } ) ; } let depth = 0 ; let max_depth = 1 ; log ( 'makeSortedFileDefinition, max_depth ' , max_depth ) ; let redundantArrayClasses = [ ] ; while ( depth <= max_depth ) { sortedClasses . forEach ( ( c ) => { const hDepth = this . findHierachyDepth ( c , fileDef ) ; if ( hDepth > max_depth ) { max_depth = hDepth ; } this . log ( '--DEPTH:' , c . name + '\t' + hDepth ) ; if ( hDepth === depth ) { if ( c . name . indexOf ( GROUP_PREFIX ) === 0 ) { } outFile . addClass ( { name : c . name } ) ; const classDef = outFile . getClass ( c . name ) ; classDef . methods = c . methods ; classDef . isExported = true ; classDef . isAbstract = c . isAbstract ; c . extendsTypes . forEach ( ( t ) => classDef . addExtends ( t . text ) ) ; c . getPropertiesAndConstructorParameters ( ) . forEach ( ( prop ) => { const ct = sortedClasses . filter ( cd => cd . name === prop . type . text . replace ( '[]' , '' ) ) [ 0 ] ; if ( ct && ct . properties . length === 1 && ct . properties [ 0 ] . type . text . indexOf ( '[]' ) > 0 ) { prop . type . text = ct . properties [ 0 ] . type . text ; log ( 'array construct detected:' , ct . name , prop . name , ct . properties [ 0 ] . type . text , prop . type . text ) ; redundantArrayClasses . push ( ct . name ) ; } else { } this . addProtectedPropToClass ( classDef , prop ) ; } ) ; this . makeConstructor ( classDef , c , outFile ) ; } } ) ; depth ++ ; } log ( 'ready' ) ; log ( 'redundantArrayClasses' , redundantArrayClasses ) ; outFile . classes = outFile . classes . filter ( c => redundantArrayClasses . indexOf ( c . name ) < 0 ) ; log ( 'Classes' , outFile . classes . map ( c => c . name ) ) ; return outFile ; } private makeConstructor ( classDef , c , outFile ) { const constructor = classDef . addMethod ( { name : 'constructor' } ) ; constructor . scope = "protected" ; constructor . addParameter ( { name : "props?" , type : c . name } ) ; constructor . onWriteFunctionBody = ( writer ) => { if ( c . extendsTypes . length ) { if ( outFile . getClass ( c . extendsTypes [ 0 ] . text ) !== null ) { writer . write ( `super(props);\n` ) ; } else { writer . write ( `super();\n` ) ; } } writer . write ( `this["@class"] = "${ this . classPrefix }${ c . name }";\n` ) ; const codeLines = [ ] ; classDef . getPropertiesAndConstructorParameters ( ) . forEach ( ( prop ) => { const propName = prop . name . replace ( '?' , '' ) ; if ( outFile . getClass ( prop . type . text ) != null ) { codeLines . push ( `\tthis.${ propName } = (props.${ propName }) ? new ${ prop . type . text }(props.${ propName }): undefined;` ) ; } else if ( prop . type . text . indexOf ( '[]' ) >= 0 ) { const arrayType = prop . type . text . replace ( '[]' , '' ) ; const expr = ( outFile . getClass ( arrayType ) != null ) ? `new ${ arrayType }(o)` : 'o' ; codeLines . push ( `\tthis.${ propName } = props.${ propName }?.map(o => ${ expr });` ) ; } else { codeLines . push ( `\tthis.${ propName } = props.${ propName };` ) ; } } ) ; if ( codeLines . length > 0 ) { writer . write ( '\nif (props) {\n' ) ; writer . write ( codeLines . join ( '\n' ) ) ; writer . write ( '\n}' ) ; } } ; } private addProtectedPropToClass ( classDef , prop ) { const type = prop . type . text ; if ( /^group_/ . test ( type ) ) { const c = this . fileDef . getClass ( type ) ; if ( c ) { c . getPropertiesAndConstructorParameters ( ) . forEach ( ( p ) => { this . addProtectedPropToClass ( classDef , p ) ; } ) ; return ; } } classDef . addProperty ( { defaultExpression : ( prop . defaultExpression ) ? prop . defaultExpression . text : null , name : prop . name , scope : "protected" , type : prop . type . text , } ) ; } private findHierachyDepth ( c , f ) { let result = 0 ; let superClassName = ( c . extendsTypes [ 0 ] ) ? c . extendsTypes [ 0 ] . text : '' ; while ( superClassName ) { result ++ ; c = f . getClass ( superClassName ) ; superClassName = c ?. extendsTypes [ 0 ] ?. text ; } return result ; } }