import * as convert from 'xml-js' ; import { ParseConformance } from './parseConformance' ; import { XmlHelper } from './xmlHelper' ; import { ParsedProperty } from "./model/parsed-property" ; import { Constants } from "./constants" ; import { XmlElement } from "./convertToXml" ; export class ConvertToJs { private parser : ParseConformance ; constructor ( parser ? ) { this . parser = parser || new ParseConformance ( true ) ; } param { string } xml returns { FHIR . Resource } public convert ( xml ) { const xmlObj = convert . xml2js ( xml ) ; const firstElement = xmlObj . elements . find ( ( element ) => element . type === 'element' ) ; if ( firstElement ) { return this . resourceToJS ( firstElement , null ) ; } } param { string } xml returns { string } public convertToJSON ( xml ) { const xmlObj = convert . xml2js ( xml ) ; if ( xmlObj . elements . length !== 1 ) { return ; } const surroundDecimalsWith = { } ; const jsObj = this . resourceToJS ( xmlObj . elements [ 0 ] , surroundDecimalsWith ) ; const maxDLength = this . maxLengthOfDs ( jsObj ) ; let rpt = '' ; for ( let i = 0 ; i < maxDLength + 5 ; i ++ ) { rpt += 'D' ; } surroundDecimalsWith [ 'str' ] = rpt ; const json = JSON . stringify ( jsObj , null , '\t' ) ; const replaceRegex = new RegExp ( '"?' + surroundDecimalsWith [ 'str' ] + '"?' , 'g' ) ; const json2 = json . replace ( replaceRegex , '' ) ; return json2 ; } private maxLengthOfDs ( obj ) { param { string } str function maxSubstringLengthStr ( str ) { const matches = str . match ( /DDDD+/g ) ; if ( ! matches ) { return 0 ; } const ret = matches . map ( ( substr ) => { return substr . length ; } ) . reduce ( ( p , c ) => { return Math . max ( p , c ) ; } , 0 ) ; return ret ; } /**
         * look through object to find longest sequence of 'D' characters
         * so we can safely wrap decimals
         */ function maxSubstringLength ( currentMax , obj ) { let ret ; if ( typeof ( obj ) === 'string' ) { ret = Math . max ( currentMax , maxSubstringLengthStr ( obj ) ) ; } else if ( typeof ( obj ) === 'object' ) { ret = Object . keys ( obj ) . map ( ( k ) => { return Math . max ( maxSubstringLengthStr ( k ) , maxSubstringLength ( currentMax , obj [ k ] ) ) ; } ) . reduce ( ( p , c ) => { return Math . max ( p , c ) ; } , currentMax ) ; } else { ret = currentMax ; } return ret ; } return maxSubstringLength ( 0 , obj ) ; } param xmlObj returns { * } private private resourceToJS ( xmlObj , surroundDecimalsWith ) { const typeDefinition = this . parser . parsedStructureDefinitions [ xmlObj . name ] ; const resource = { resourceType : xmlObj . name } ; if ( ! typeDefinition ) { throw new Error ( 'Unknown resource type: ' + xmlObj . name ) ; } typeDefinition . _properties . forEach ( ( property ) => { this . propertyToJS ( xmlObj , resource , property , surroundDecimalsWith ) ; } ) ; return resource ; } param relativeType { string } private findReferenceType ( relativeType ) { if ( ! relativeType || ! relativeType . startsWith ( '#' ) ) { return ; } const resourceType = relativeType . substring ( 1 , relativeType . indexOf ( '.' ) ) ; const path = relativeType . substring ( resourceType . length + 2 ) ; const resourceDefinition = this . parser . parsedStructureDefinitions [ resourceType ] ; const pathSplit = path . split ( '.' ) ; if ( ! resourceDefinition ) { throw new Error ( 'Could not find resource definition for ' + resourceType ) ; } let current = < ParsedProperty > < any > resourceDefinition ; for ( let i = 0 ; i < pathSplit . length ; i ++ ) { const nextPath = pathSplit [ i ] ; current = current . _properties . find ( ( property ) => property . _name === nextPath ) ; if ( ! current ) { return ; } } return JSON . parse ( JSON . stringify ( current ) ) ; } param xmlObj param obj param property private private propertyToJS ( xmlObj , obj , property , surroundDecimalsWith ) { const xmlElements = [ ] ; if ( xmlObj . elements ) { for ( let element of xmlObj . elements ) { if ( element . name === property . _name ) { xmlElements . push ( element ) ; } } } const xmlAttributes = [ ] ; if ( xmlObj . attributes ) { const attributeKeys = Object . keys ( xmlObj . attributes ) ; for ( let attributeKey of attributeKeys ) { if ( attributeKey === property . _name ) { xmlAttributes . push ( { name : attributeKey , type : 'attribute' , attributes : { value : xmlObj . attributes [ attributeKey ] } } ) ; } } } const xmlProperty = xmlElements . concat ( xmlAttributes ) ; if ( ! xmlProperty || xmlProperty . length === 0 ) { return ; } if ( property . _type && property . _type . indexOf ( '#' ) === 0 ) { const relativeType = this . findReferenceType ( property . _type ) ; if ( ! relativeType ) { throw new Error ( 'Could not find reference to element definition ' + relativeType ) ; } relativeType . _name = property . _name ; relativeType . _multiple = property . _multiple ; relativeType . _required = property . _required ; property = relativeType ; } const addExtra = ( element , index ) => { const hasId = element . attributes && element . attributes . id ; const hasExtensions = ! ! ( element . elements || [ ] ) . find ( ( next ) => next . name === 'extension' ) ; if ( hasId || hasExtensions ) { if ( ! obj [ '_' + property . _name ] ) { obj [ '_' + property . _name ] = obj [ property . _name ] instanceof Array ? [ ] : { } ; } } const dest = obj [ '_' + property . _name ] ; if ( hasId || hasExtensions ) { if ( dest instanceof Array ) { if ( dest . length < index + 1 ) { for ( let i = 0 ; i < index ; i ++ ) { if ( ! dest [ i ] ) { dest [ i ] = null ; } } } dest [ index ] = { } ; } } if ( hasId ) { if ( dest instanceof Array ) { dest [ index ] . id = element . attributes . id ; } else { dest . id = element . attributes . id ; } } if ( hasExtensions ) { const extensionProperty = { _name : 'extension' , _type : 'Extension' , _multiple : true , _required : false } ; this . propertyToJS ( element , dest instanceof Array ? dest [ index ] : dest , extensionProperty , surroundDecimalsWith ) ; } } ; const pushValue = ( value , index ) => { if ( ! value ) return ; switch ( property . _type ) { case 'string' : case 'base64Binary' : case 'code' : case 'id' : case 'markdown' : case 'uri' : case 'url' : case 'canonical' : case 'oid' : case 'date' : case 'dateTime' : case 'time' : case 'instant' : addExtra ( value , index ) ; if ( value . attributes && value . attributes [ 'value' ] ) { if ( obj [ property . _name ] instanceof Array ) { obj [ property . _name ] . push ( value . attributes [ 'value' ] ) ; } else { obj [ property . _name ] = value . attributes [ 'value' ] ; } } break ; case 'decimal' : addExtra ( value , index ) ; if ( value . attributes [ 'value' ] ) { if ( obj [ property . _name ] instanceof Array ) { obj [ property . _name ] . push ( convertDecimal ( value . attributes [ 'value' ] , surroundDecimalsWith ) ) ; } else { obj [ property . _name ] = convertDecimal ( value . attributes [ 'value' ] , surroundDecimalsWith ) ; } } break ; case 'boolean' : addExtra ( value , index ) ; if ( value . attributes [ 'value' ] ) { if ( obj [ property . _name ] instanceof Array ) { obj [ property . _name ] . push ( toBoolean ( value . attributes [ 'value' ] ) ) ; } else { obj [ property . _name ] = toBoolean ( value . attributes [ 'value' ] ) ; } } break ; case 'integer' : case 'unsignedInt' : case 'positiveInt' : addExtra ( value , index ) ; if ( value . attributes && value . attributes [ 'value' ] ) { if ( obj [ property . _name ] instanceof Array ) { obj [ property . _name ] . push ( toNumber ( value . attributes [ 'value' ] ) ) ; } else { obj [ property . _name ] = toNumber ( value . attributes [ 'value' ] ) ; } } break ; case 'xhtml' : if ( value . elements && value . elements . length > 0 ) { const div = convert . js2xml ( { elements : [ XmlHelper . escapeInvalidCharacters ( value ) ] } ) ; if ( obj [ property . _name ] instanceof Array ) { obj [ property . _name ] . push ( div ) ; } else { obj [ property . _name ] = div ; } } break ; case 'Element' : case 'BackboneElement' : const newValue = { } ; for ( let x in property . _properties ) { const nextProperty = property . _properties [ x ] ; this . propertyToJS ( value , newValue , nextProperty , surroundDecimalsWith ) ; } if ( obj [ property . _name ] instanceof Array ) { obj [ property . _name ] . push ( newValue ) ; } else { obj [ property . _name ] = newValue ; } break ; case 'Resource' : if ( value . elements . length === 1 ) { if ( obj [ property . _name ] instanceof Array ) { obj [ property . _name ] . push ( this . resourceToJS ( value . elements [ 0 ] , surroundDecimalsWith ) ) ; } else { obj [ property . _name ] = this . resourceToJS ( value . elements [ 0 ] , surroundDecimalsWith ) ; } } break ; default : const nextType = this . parser . parsedStructureDefinitions [ property . _type ] ; if ( ! nextType ) { console . log ( 'do something' ) ; } else { const newValue = { } ; nextType . _properties . forEach ( nextProperty => { this . propertyToJS ( value , newValue , nextProperty , surroundDecimalsWith ) ; } ) ; if ( obj [ property . _name ] instanceof Array ) { obj [ property . _name ] . push ( newValue ) ; } else { obj [ property . _name ] = newValue ; } } break ; } } ; function toBoolean ( value ) { if ( value === "true" ) { return true ; } else if ( value === "false" ) { return false ; } else { throw new Error ( "Value should be a boolean but got: " + value ) ; } } function toNumber ( value ) { if ( /^-?\d+$/ . test ( value ) == false ) { throw new Error ( "Value should be a number but got: " + value ) ; } return parseInt ( value , 10 ) ; } function convertDecimal ( value , surroundDecimalsWith ) { if ( /^-?([0]|([1-9][0-9]*))(\.[0-9]+)?$/ . test ( value ) == false ) { throw new Error ( "Value should be a decimal number but got: " + value ) ; } if ( surroundDecimalsWith ) { return { value : value , toJSON : function ( ) { return surroundDecimalsWith . str + value + surroundDecimalsWith . str ; } } ; } else { return value ; } } if ( property . _multiple ) { obj [ property . _name ] = [ ] ; } for ( let i = 0 ; i < xmlProperty . length ; i ++ ) { let xmlCommentElements ; let nextXmlComment ; const xmlPropertyIndex = xmlObj . elements . indexOf ( xmlProperty [ i ] ) ; while ( nextXmlComment != null || ! xmlCommentElements ) { if ( ! xmlCommentElements ) { xmlCommentElements = [ ] ; } const nextIndex = xmlCommentElements . length + 1 ; if ( ( xmlPropertyIndex - nextIndex ) < 0 ) break ; nextXmlComment = xmlPropertyIndex > 0 && xmlObj . elements [ xmlPropertyIndex - nextIndex ] . type === 'comment' ? xmlObj . elements [ xmlPropertyIndex - nextIndex ] : null ; if ( nextXmlComment ) { xmlCommentElements . push ( nextXmlComment ) ; } } const extraPropertyName = '_' + property . _name ; pushValue ( xmlProperty [ i ] , i ) ; if ( xmlCommentElements && xmlCommentElements . length > 0 ) { if ( Constants . PrimitiveTypes . indexOf ( property . _type ) >= 0 ) { if ( property . _multiple ) { if ( ! obj [ extraPropertyName ] ) { obj [ extraPropertyName ] = [ ] ; } if ( ! obj [ extraPropertyName ] [ i ] ) { obj [ extraPropertyName ] [ i ] = { } ; } obj [ extraPropertyName ] [ i ] . fhir_comments = xmlCommentElements . reverse ( ) . map ( c => c . comment . trim ( ) ) ; } else { if ( ! obj [ extraPropertyName ] ) { obj [ extraPropertyName ] = { } ; } obj [ extraPropertyName ] . fhir_comments = xmlCommentElements . reverse ( ) . map ( c => c . comment . trim ( ) ) ; } } else { if ( property . _multiple ) { if ( ! obj [ property . _name ] ) { obj [ property . _name ] = [ ] ; } if ( ! obj [ property . _name ] [ i ] ) { obj [ property . _name ] [ i ] = { } ; } obj [ property . _name ] [ i ] . fhir_comments = xmlCommentElements . reverse ( ) . map ( c => c . comment . trim ( ) ) ; } else { if ( ! obj [ property . _name ] ) { obj [ property . _name ] = { } ; } obj [ property . _name ] . fhir_comments = xmlCommentElements . reverse ( ) . map ( c => c . comment . trim ( ) ) ; } } } } } }