xquery version "3.1";
(:
    ART-DECOR® STANDARD COPYRIGHT AND LICENSE NOTE
    Copyright © ART-DECOR Expert Group and ART-DECOR Open Tools GmbH
    see https://docs.art-decor.org/copyright and https://docs.art-decor.org/licenses

    This file is part of the ART-DECOR® tools suite.
:)

module namespace art                    = "http://art-decor.org/ns/art";
import module namespace get             = "http://art-decor.org/ns/art-decor-settings" at "art-decor-settings.xqm";
import module namespace decor           = "http://art-decor.org/ns/decor" at "../api/api-decor.xqm";
import module namespace vs              = "http://art-decor.org/ns/decor/valueset" at "../api/api-decor-valueset.xqm";
import module namespace aduser          = "http://art-decor.org/ns/art-decor-users" at "../api/api-user-settings.xqm";
import module namespace adserver        = "http://art-decor.org/ns/art-decor-server" at "../api/api-server-settings.xqm";
import module namespace schxslt         = "https://doi.org/10.5281/zenodo.1495494" at "../../schxslt/content/schxslt.xqm";

declare namespace request       = "http://exist-db.org/xquery/request";
declare namespace response      = "http://exist-db.org/xquery/response";
declare namespace datetime      = "http://exist-db.org/xquery/datetime";
declare namespace hl7           = "urn:hl7-org:v3";
declare namespace xhtml         = "http://www.w3.org/1999/xhtml";
declare namespace util          = "http://exist-db.org/xquery/util";
declare namespace sch           = "http://purl.oclc.org/dsdl/schematron";
declare namespace xforms        = "http://www.w3.org/2002/xforms";
declare namespace expath        = "http://expath.org/ns/pkg";
declare namespace xsi           = "http://www.w3.org/2001/XMLSchema-instance";
declare namespace error         = "http://art-decor.org/ns/decor/error";
declare namespace json          = "http://www.json.org";

(:~ Source ItemStatusCodeLifeCycle in DECOR-datatypes.xsd :)
declare variable $art:itemstatusmap     := 
        map:merge((
            map:entry('',           ('new', 'draft')) ,
            map:entry('new',        ('new', 'draft', 'cancelled')) ,
            map:entry('draft',      ('draft', 'pending', 'final', 'cancelled', 'rejected')) ,
            map:entry('cancelled',  ('cancelled', 'draft')) ,
            map:entry('pending',    ('draft', 'pending', 'final')) ,
            map:entry('final',      ('final', 'deprecated')) ,
            map:entry('rejected',   ('rejected')) ,
            map:entry('deprecated', ('deprecated'))
        ));
(:~ Source CodedConceptStatusCodeLifeCycle in DECOR-datatypes.xsd :)
declare variable $art:codedconceptstatusmap     := 
        map:merge((
            map:entry('',           ('draft', 'active', 'deprecated', 'retired', 'cancelled', 'rejected', 'experimental')) ,
            map:entry('draft',      ('draft', 'active', 'deprecated', 'retired', 'cancelled', 'rejected', 'experimental')),
            map:entry('active',     ('active', 'deprecated')),
            map:entry('deprecated', ('deprecated', 'retired')),
            map:entry('retired',    ('retired')),
            map:entry('cancelled',  ('cancelled')),
            map:entry('rejected',   ('rejected')),
            map:entry('experimental',('experimental', 'active'))
        ));
(:~ Source TemplateStatusCodeLifeCycle in DECOR-datatypes.xsd :)
declare variable $art:templatestatusmap := 
        map:merge((
            map:entry('',           ('draft')) ,
            map:entry('draft',      ('draft', 'pending', 'active', 'cancelled', 'rejected')) ,
            map:entry('cancelled',  ('cancelled', 'draft')) ,
            map:entry('pending',    ('pending', 'draft', 'active', 'rejected')) ,
            map:entry('active',     ('active', 'review', 'retired')) ,
            map:entry('review',     ('review', 'active', 'retired')) ,
            map:entry('rejected',   ('rejected')) ,
            map:entry('retired',    ('retired'))
        ));
(:~ Source ReleaseStatusCodeLifeCycle in DECOR-datatypes.xsd :)
declare variable $art:releasestatusmap := 
        map:merge((
            map:entry('',           'draft'),
            map:entry('draft',      ('draft', 'pending', 'active', 'cancelled', 'failed')),
            map:entry('pending',    ('pending', 'draft', 'active', 'cancelled', 'retired', 'failed')),
            map:entry('active',     ('active', 'retired')),
            map:entry('retired',    'retired'),
            map:entry('cancelled',  ('cancelled', 'draft')),
            map:entry('failed',     'failed')
        ));
(:~ Source IssueStatusCodeLifeCycle in DECOR-datatypes.xsd :)
declare variable $art:issuestatusmap := 
        map:merge((
            map:entry('',           'new') ,
            map:entry('new',        'open'),
            map:entry('open',       ('open', 'inprogress', 'feedback', 'closed', 'rejected', 'deferred', 'cancelled')),
            map:entry('inprogress', ('inprogress', 'feedback', 'closed', 'rejected', 'deferred', 'cancelled')),
            map:entry('feedback',   ('feedback', 'inprogress', 'closed', 'rejected', 'deferred', 'cancelled')),
            map:entry('closed',     ('closed', 'inprogress', 'feedback', 'rejected', 'deferred', 'cancelled')),
            map:entry('rejected',   ('rejected', 'inprogress', 'feedback', 'closed', 'deferred', 'cancelled')),
            map:entry('deferred',   ('deferred', 'inprogress', 'feedback', 'closed', 'rejected', 'cancelled')),
            map:entry('cancelled',  ('cancelled', 'inprogress'))
        ));

declare variable $art:strDecorServicesURL       := art:getServerURLServices();

(:~ Return if a new statusCode is allowable given the old statusCode and the object type :)
declare function art:isStatusChangeAllowable($object as element(), $newStatus as xs:string) as xs:boolean {
    if (string($object/@statusCode) = string($newStatus)) then true() else (
        switch (name($object))
        case 'template' return map:get($art:templatestatusmap, string($object/@statusCode)) = $newStatus
        case 'release' return map:get($art:releasestatusmap, string($object/@statusCode)) = $newStatus
        case 'version' return map:get($art:releasestatusmap, string($object/@statusCode)) = $newStatus
        case 'codedConcept' return map:get($art:codedconceptstatusmap, string($object/@statusCode)) = $newStatus
        default return map:get($art:itemstatusmap, string($object/@statusCode)) = $newStatus
    )
};

(:~  Helper function to recursively create a collection hierarchy.
    @author Gerrit Boers
:)
declare function art:mkcol-recursive($collection, $components) {
    if (exists($components)) then
        let $newColl := concat($collection, "/", $components[1])
        return (
            xmldb:create-collection($collection, $components[1]),
            art:mkcol-recursive($newColl, subsequence($components, 2))
        )
    else
        ()
};

(:~ Helper function to recursively create a collection hierarchy.
    @author Gerrit Boers
:)
declare function art:mkcol($collection, $path) {
    art:mkcol-recursive($collection, tokenize($path, "/"))
};

(:  Returns SVRL version of schematron
    
    Input:  schematron grammar
    Output: SVRL for schematron grammar
    
    @author Kai Heitmann, Alexander Henket, Marc de Graauw 
    @since 2014
:)
declare function art:get-iso-schematron-svrl($grammar as item()) as element() {
    let $xsltParameters :=
        <parameters>
            <param name="allow-foreign" value="'true'"/>
            <param name="generate-fired-rule" value="'true'"/>
            <param name="generate-paths" value="'true'"/>
            <param name="diagnose" value="'yes'"/>
        </parameters>
    return art:get-iso-schematron-svrl($grammar, $xsltParameters)
};

(:~ Returns SVRL version of schematron
    
    Input:  schematron grammar
            xsltParameters 
    Output: SVRL for schematron grammar
    
    @author Kai Heitmann, Alexander Henket, Marc de Graauw 
    @since 2014
:)
declare function art:get-iso-schematron-svrl($grammar as item(), $xsltParameters as node()) as element() {
    
    schxslt:compile($grammar, $xsltParameters, '2.0')

    (:let $isoschematrons := $get:strUtilISOSCH2SVRL
    
    let $path2schematroninclude     := concat($isoschematrons, "/iso_dsdl_include.xsl") cast as xs:anyURI
    let $path2schematronabstract    := concat($isoschematrons, "/iso_abstract_expand.xsl") cast as xs:anyURI
    let $path2schematronxsl         := concat($isoschematrons, "/iso_svrl_for_xslt2.xsl") cast as xs:anyURI
    
    let $includetransform           := transform:transform($grammar, doc($path2schematroninclude), $xsltParameters)
    let $abstracttransform          := transform:transform($includetransform, doc($path2schematronabstract), $xsltParameters) 
    let $grammartransform           := transform:transform($abstracttransform, doc($path2schematronxsl), $xsltParameters)
    
    return $grammartransform:)
};

declare function art:getSchematronFromSchema($xmlschema as element(xs:schema)) as element(sch:schema) {
    let $xmlschemaBase              := util:collection-name($xmlschema)
    let $schematronExtractionXSL    := 
    <xsl:stylesheet 
        xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
        xmlns:xs="http://www.w3.org/2001/XMLSchema"
        xmlns:sch="http://purl.oclc.org/dsdl/schematron"
        xmlns:sqf="http://www.schematron-quickfix.com/validator/process"
        exclude-result-prefixes="#all"
        version="2.0">
        <xsl:output indent="yes"/>
        <xsl:template match="/">
            <sch:schema 
                xml:lang="en-US"
                xmlns="http://purl.oclc.org/dsdl/schematron" 
                xmlns:sqf="http://www.schematron-quickfix.com/validator/process" 
                xmlns:sch="http://purl.oclc.org/dsdl/schematron" queryBinding="xslt2">
                <sch:ns uri="http://purl.oclc.org/dsdl/schematron" prefix="sch"/>
                <xsl:copy-of select="//sch:pattern"/>
                <xsl:copy-of select="//sqf:fixes"/>
                <xsl:for-each select="*/xs:include">
                    <xsl:variable name="includedoc" select="document(concat('xmldb:exist://{$xmlschemaBase}/',@schemaLocation))"/>
                    <xsl:copy-of select="$includedoc//sch:pattern"/>
                    <xsl:copy-of select="$includedoc//sqf:fixes"/>
                </xsl:for-each>
            </sch:schema>
        </xsl:template>
    </xsl:stylesheet>
    
    return
    transform:transform($get:docDecorSchema, $schematronExtractionXSL, ())
};

(:~ Returns a full dataset tree for either a transaction/@id or dataset/@id,
    or when called with a specific conceptId, it returns this concept and child concepts
    
    Input:  id                      - transaction/@id or dataset/@id
            effectiveDate           - transaction/@effectiveDate or dataset/@effectiveDate
                                        If @param effectiveDate is not xs:dateTime, will act as dynamic and pick newest
            conceptId               - concept/@id
            conceptEffectiveDate    - concept/@effectiveDate
                                        If @param conceptEffectiveDate is not xs:dateTime, will act as dynamic and pick newest
            language, used to filter names
    Output: dataset
        - filtered for language
        - inherits resolved
        - conceptList/@ref resolved
        - no history
        - concept/valueDomain[@type='code'] are provided with conceptList/valueSet 
            with enhanced valueSet (valueSet with names from conceptList where terminologyAssociations are present)
        - concept contains implementation element with @shortName (usable as SQL, XML name) and @xpath
    
    In a number of cases like RetrieveTransaction and template mapping we do not care about absent concepts to just omit those when $fullTree=false()
    Note: only active concepts are traversed. A concept with an absent parent is not retrieved. This situation should never occur.
    
    @author Alexander Henket 
    @since 2013
:)
declare function art:getFullDataset($id as xs:string, $effectiveDate as xs:string?, $conceptId as xs:string?, $conceptEffectiveDate as xs:string?, $language as xs:string?, $xpathDoc as node()?, $fullTree as xs:boolean, $oidnamemap as item()?) as element() {
    let $datasetOrTransaction   := art:getDataset($id, $effectiveDate) | art:getTransaction($id, $effectiveDate)
    
    return 
    art:getFullDatasetTree($datasetOrTransaction, $conceptId, $conceptEffectiveDate, $language, $xpathDoc, $fullTree, $oidnamemap, ())
};
declare function art:getFullDatasetTree($datasetOrTransaction as element(), $conceptId as xs:string?, $conceptEffectiveDate as xs:string?, $language as xs:string?, $xpathDoc as node()?, $fullTree as xs:boolean, $oidnamemap as item()?) as element() {
    art:getFullDatasetTree($datasetOrTransaction, $conceptId, $conceptEffectiveDate, $language, $xpathDoc, $fullTree, $oidnamemap, ())
};
declare function art:getFullDatasetTree($datasetOrTransaction as element(), $conceptId as xs:string?, $conceptEffectiveDate as xs:string?, $language as xs:string?, $xpathDoc as node()?, $fullTree as xs:boolean, $oidnamemap as item()?, $communityName as xs:string*) as element() {
    let $isTransaction      := $datasetOrTransaction[self::transaction]
    let $decor              := $datasetOrTransaction/ancestor::decor
    let $projectId          := $decor/project/@id
    let $prefix             := $decor/project/@prefix
    let $defaultLanguage    := $decor/project/@defaultLanguage
    let $language           := if (string-length($language)=0) then $defaultLanguage else ($language)
    
    let $representingTemplate       := $datasetOrTransaction/representingTemplate
    let $dataset                    := 
        if ($datasetOrTransaction[self::dataset]) then $datasetOrTransaction else 
        if ($representingTemplate[@sourceDataset]) then
            art:getDataset($representingTemplate/@sourceDataset, $representingTemplate/@sourceDatasetFlexibility)
        else
        if ($datasetOrTransaction[@datasetId]) then
            art:getDataset($datasetOrTransaction/@datasetId, $datasetOrTransaction/@datasetEffectiveDate)
        else ()
    let $datasetDecor               := $dataset/ancestor::decor
    let $datasetDecorProjectId      := $datasetDecor/project/@id
    let $datasetDecorProjectPrefix  := $datasetDecor/project/@prefix
    
    let $check                      := 
        if ($dataset) then () else (
            error(xs:QName('error:NoDataset'),concat(name($datasetOrTransaction), ' id=''', $datasetOrTransaction/@id, ''' effectiveDate=''', $datasetOrTransaction/@effectiveDate, 
                ' name ''', data($datasetOrTransaction/name[1]), ''' is not a dataset and is not associated with a dataset, so a full dataset cannot be given.'))
        )
    
    (:let $datasetTree    := 
        if ($isTransaction) then 
            art:getDatasetTree((), (), $datasetOrTransaction/@id, $datasetOrTransaction/@effectiveDate) 
        else (
            art:getDatasetTree($datasetOrTransaction/@id, $datasetOrTransaction/@effectiveDate, (), ())
        ):)
    (: only for transactions: if the xpaths are provided, insert xpath and hl7 datatype :)
    let $xpaths         := if ($xpathDoc and $isTransaction) then $xpathDoc//transactionXpaths[@ref = $datasetOrTransaction/@id][1] else ()

    let $name           := 
        if ($datasetOrTransaction/name[@language=$language]) 
        then $datasetOrTransaction/name[@language=$language][1] 
        else <name language="{if ($language = '*') then $datasetOrTransaction/name[1]/@language else $language}">{$datasetOrTransaction/name[1]/node()}</name>
    
    (: build map of oid names. is more costly when you do that deep down :)
    let $oidnamemap             :=
        if (empty($oidnamemap)) then
            map:merge((
                let $oidName    := art:getNameForOID($datasetOrTransaction/@id, $language, $decor)
                return
                    if (string-length($oidName) = 0) then () else map:entry($datasetOrTransaction/@id, $oidName)
                ,
                for $oid in distinct-values($dataset//@id | $dataset//@ref)
                let $oidName    := if ($datasetOrTransaction[@id = $oid]) then () else art:getNameForOID($oid, $language, $datasetDecor)
                return
                    if (string-length($oidName) = 0) then () else map:entry($oid, $oidName)
            ))
        else ($oidnamemap)
    let $transactionconceptmap  :=
        if ($isTransaction) then
            map:merge((
                map:entry('transaction', <transaction>{$datasetOrTransaction/@*}</transaction>)
                ,
                for $concept in $representingTemplate/concept[@ref]
                let $de-id      := $concept/@ref
                group by $de-id
                return
                    map:entry($de-id[1], $concept[1])
            ))
        else ()
    
    (: get communities for current project, but also get communities from project the dataset is from, if that is not the same project. :)
    let $communities            := 
        if (empty($projectId)) then () else if ($datasetOrTransaction//community) then () else (
            decor:getDecorCommunity($communityName, $projectId, $decor/@versionDate, false()),
            if ($prefix = $datasetDecorProjectPrefix) then () else (
                if ($datasetDecor)
                then decor:getDecorCommunity($communityName, $datasetDecorProjectId, $datasetDecor/@versionDate, false())
                else ()
            )
        )
    let $communityIdMap         :=
        map:merge(
            if (empty($datasetDecorProjectId) or $projectId = $datasetDecorProjectId) then () else map:entry($datasetDecorProjectId, ($datasetDecorProjectPrefix))
        )
    
    let $conceptTree            :=
        (: only children with non-absent descendants, or everything if this is a plain dataset expand :)
        (: the conceptnamemap is for performance on datasets with many sibling concepts so we calculate names only once :)
        if (empty($transactionconceptmap)) then
            if (empty($conceptId)) then (
                let $conceptset         := $dataset/concept
                let $conceptnamemap     := art:getConceptNameMap($conceptset, $language)
                
                for $concept in $dataset/concept
                return art:getFullConcept($datasetDecor, $concept, $language, $xpaths, (), $oidnamemap, $transactionconceptmap, $conceptnamemap)
            )
            else (
                let $conceptset         := $dataset/concept[(@id|@ref) = $conceptId]
                let $conceptnamemap     := art:getConceptNameMap($conceptset, $language)
                
                for $concept in $dataset//concept[(@id|@ref) = $conceptId]
                return art:getFullConcept($datasetDecor, $concept, $language, $xpaths, (), $oidnamemap, $transactionconceptmap, $conceptnamemap)
            )
        else (
            if (empty($conceptId)) then (
                let $conceptset         := $dataset/concept[.[map:contains($transactionconceptmap, @id)] | descendant::concept[map:contains($transactionconceptmap, @id)]]
                let $conceptnamemap     := art:getConceptNameMap($conceptset, $language)
                
                for $concept in $conceptset
                return art:getFullConcept($datasetDecor, $concept, $language, $xpaths, (), $oidnamemap, $transactionconceptmap, $conceptnamemap)
            )
            else (
                let $conceptset         := $dataset//concept[.[map:contains($transactionconceptmap, @id)] | descendant::concept[map:contains($transactionconceptmap, @id)]]
                let $conceptnamemap     := art:getConceptNameMap($conceptset, $language)
                
                for $concept in $conceptset
                return art:getFullConcept($datasetDecor, $concept, $language, $xpaths, (), $oidnamemap, $transactionconceptmap, $conceptnamemap)
            )
        )
        
    let $fullDatasetTree        := 
        <dataset>
        {
            $dataset/@id, $dataset/@effectiveDate, $dataset/@statusCode, $dataset/@versionLabel, $dataset/@expirationDate, 
            $dataset/@officialReleaseDate, $dataset/@canonicalUri, $dataset/@lastModifiedDate, $decor/@versionDate, $prefix, 
            if ($datasetDecorProjectPrefix = $prefix) then () else attribute datasetPrefix {$datasetDecorProjectPrefix}
            ,
            if ($isTransaction) then (
                attribute transactionId {$datasetOrTransaction/@id}, 
                attribute transactionEffectiveDate {$datasetOrTransaction/@effectiveDate},
                attribute transactionStatusCode {$datasetOrTransaction/@statusCode},
                if ($datasetOrTransaction/@expirationDate) then
                    attribute transactionExpirationDate {$datasetOrTransaction/@expirationDate}
                else (),
                if ($datasetOrTransaction/@versionLabel) then
                    attribute transactionVersionLabel {$datasetOrTransaction/@versionLabel}
                else (),
                if ($datasetOrTransaction/@canonicalUri) then
                    attribute transactionCanonicalUri {$datasetOrTransaction/@canonicalUri}
                else (),
                if ($datasetOrTransaction/@type) then
                    attribute transactionType {$datasetOrTransaction/@type}
                else ()
                ,
                let $iddisplay  := map:get($oidnamemap, $datasetOrTransaction/@id)
                let $iddisplay  := if (empty($iddisplay)) then art:getNameForOID($datasetOrTransaction/@id, $language, $decor) else ($iddisplay)
                return
                    attribute transactionIddisplay {$iddisplay}
                ,
                if ($representingTemplate[@ref]) then (
                    attribute templateId {$representingTemplate/@ref},
                    if ($representingTemplate[@flexibility]) then
                        attribute templateEffectiveDate {$representingTemplate/@flexibility}
                    else ()
                ) else ()
                ,
                if ($representingTemplate[@representingQuestionnaire]) then (
                    attribute questionnaireId {$representingTemplate/@representingQuestionnaire},
                    if ($representingTemplate[@representingQuestionnaireFlexibility]) then
                        attribute questionnaireEffectiveDate {$representingTemplate/@representingQuestionnaireFlexibility}
                    else ()
                ) else ()
            ) else ()
            ,
            attribute shortName {art:shortName($name)}, 
            let $iddisplay  := if ($representingTemplate or not($isTransaction)) then map:get($oidnamemap, $dataset/@id) else ()
            let $iddisplay  := if (empty($iddisplay)) then art:getNameForOID($dataset/@id, $language, $datasetDecor) else ($iddisplay)
            return
                attribute iddisplay {$iddisplay}
            ,
            attribute url {adserver:getServerURLServices()},
            attribute ident {$prefix},
            if ($conceptTree//@inheritanceIssues) then attribute inheritanceIssues {count($conceptTree//@inheritanceIssues)} else (),
            if ($conceptTree//@linkedartefactmissing) then attribute linkedartefactmissing {count($conceptTree//@linkedartefactmissing)} else ()
        }
        {
            comment {concat(
                '&#10; This is a view of the ', if ($isTransaction) then 'transaction' else 'dataset', ' "', $name, '" containing DECOR specifications for a transaction or dataset in a single, hierarchical view. ',
                '&#10; All inheritances are resolved, for transactions the dataset is filtered for those concepts occurring in the transaction.',
                '&#10; Valuesets are contained within the concept for easy reference. ',
                '&#10; Xpaths are calculated on a best effort basis. The should be considered a starting point for application logic, not an endpoint. '
            )}
        }
        {
            if ($language = '*') then (
                $datasetOrTransaction/name,
                $datasetOrTransaction/desc
            )
            else (
                $name,
                if ($datasetOrTransaction/desc[@language = $language]) 
                then $datasetOrTransaction/desc[@language = $language][1] 
                else <desc language="{if ($language = '*') then $datasetOrTransaction/desc[1]/@language else $language}">{$datasetOrTransaction/desc[1]/node()}</desc>
            ), 
            (: new ... 2021-05-21 :)
            if ($datasetOrTransaction/publishingAuthority) then $datasetOrTransaction/publishingAuthority else $dataset/publishingAuthority
            ,
            (: new since 2015-04-21, updated 2021-05-21 :)
            if ($datasetOrTransaction/property) then $datasetOrTransaction/property else $dataset/property
            ,
            (: new ... 2021-05-21 :)
            if ($datasetOrTransaction/copyright) then $datasetOrTransaction/copyright else $dataset/copyright
            ,
            (: new since 2015-04-21 :)
            for $relationship in $dataset/relationship
            let $referredDataset    :=  
                if (string-length($relationship/@ref)=0) then () else (
                    art:getDataset($relationship/@ref, $relationship/@flexibility)
                )
            return
                <relationship type="{$relationship/@type}" ref="{$relationship/@ref}" flexibility="{$relationship/@flexibility}">
                {
                    if ($referredDataset) then (
                        $referredDataset/ancestor::decor/project/@prefix,
                        attribute iStatusCode {$referredDataset/@statusCode}, 
                        if ($referredDataset/@expirationDate) then attribute iExpirationDate {$referredDataset/@expirationDate} else (),
                        if ($referredDataset/@versionLabel) then attribute iVersionLabel {$referredDataset/@versionLabel} else (),
                        let $iddisplay  := 
                            if (map:contains($oidnamemap, $relationship/@ref)) then map:get($oidnamemap, $relationship/@ref) else (
                                art:getNameForOID($relationship/@ref, $language, $referredDataset/ancestor::decor)
                            )
                        return attribute refdisplay {$iddisplay},
                        attribute localInherit {$referredDataset/ancestor::decor/project/@prefix = $prefix},
                        if ($language = '*') then ($referredDataset/name) else
                        if ($referredDataset/name[@language = $language]) then ($referredDataset/name[@language = $language][1]) else (
                            <name language="{if ($language = '*') then $referredDataset/name[1]/@language else $language}">{data($referredDataset/name[1])}</name>
                        )
                    ) else ()
                }
                </relationship>
        }
        {
            $conceptTree
        }
        </dataset>
    
    let $fullDatasetTree :=
        if ($communities) then art:mergeDatasetTreeWithCommunity($fullDatasetTree, $communities, $communityIdMap) else ($fullDatasetTree)
    
    return $fullDatasetTree
};

declare function art:getConceptNameMap($conceptset as element(concept)*, $language as xs:string?) as item() {
    map:merge(
        for $c in $conceptset
        let $oc         := art:getOriginalForConcept($c)
        let $cname      := $oc/name[@language = $language][.//text()]
        let $cname      := if ($cname) then $cname else <name language="{$language}">{$oc/name[.//text()][1]/node()}</name>
        let $shortName  := art:shortName($cname)
        return
            map:entry($c/@id, $shortName)
    )
};

(:~  Returns a full concept for getFullDatasetTree 
    Input:  id 
            language, used to filter names
    Output: full concept, see description of getFullDatasetTree
    
    @author Marc de Graauw, Alexander Henket 
    @since 2013
:)
declare function art:getFullConcept($decor as element(decor), $concept as element(), $language as xs:string, $xpaths as node()?, $parentXpath as xs:string?, $oidnamemap as item()?, $transactionconceptmap as item()?) as element(concept) {
    art:getFullConcept($decor, $concept, $language, $xpaths, $parentXpath, $oidnamemap, $transactionconceptmap, ())
};
declare function art:getFullConcept($decor as element(decor), $concept as element(), $language as xs:string, $xpaths as node()?, $parentXpath as xs:string?, $oidnamemap as item()?, $transactionconceptmap as item()?, $conceptnamemap as item()?) as element(concept) {
    (:let $projectPrefix      := $decor/project/@prefix:)
    (:let $defaultLanguage    := $decor/project/@defaultLanguage:)
    
    (: build map of oid names. is more costly when you do that deep down :)
    let $oidnamemap         :=
        if (empty($oidnamemap)) then 
            map:merge(
                for $oid in distinct-values($concept//@id | $concept//@ref)
                let $oidName    := art:getNameForOID($oid, $language, $decor)
                return
                    if (string-length($oidName) = 0) then () else map:entry($oid, $oidName)
            )
        else ($oidnamemap)
    
    let $conceptnamemap     :=
        if (empty($conceptnamemap)) then
            art:getConceptNameMap($concept/preceding-sibling::concept, $language)
        else (
            $conceptnamemap
        )
    
    let $transactionconcept := if (empty($transactionconceptmap)) then () else map:get($transactionconceptmap, ($concept/@id | $concept/@ref)[1])
    
    (:  usually you inherit from the original, but if you inherit from something that inherits, then these two will differ
        originalConcept has the type and the associations. The rest comes from the inheritConcept :)
    let $inheritConcept     := if ($concept[inherit]) then art:getConcept($concept/inherit/@ref, $concept/inherit/@effectiveDate) else ()
    (:  normally we inherit directly from the source, but if we don't we need to know if the final thing before the original has a 
        contains relationship with the original. If it does we want to add that contains element into the compiled/fullConcept. The 
        function art:getOriginalConcept will get the hierarchy of nested inherit/contains elements all the way up to the originalConcept :)
    let $finalInherit       := if ($inheritConcept[inherit]) then art:getOriginalConcept($inheritConcept/inherit)/descendant-or-self::inherit[last()] else ()
    let $finalInherit       := if ($finalInherit) then art:getConcept($finalInherit/@ref, $finalInherit/@effectiveDate) else ()
    
    let $originalConcept    := art:getOriginalForConcept($concept)
    let $shortName          := map:get($conceptnamemap, $concept/@id)
    
    (: when multiple names of the same type exist on the same level, we need to make it unique. We postfix with _<leaf node of id>. 
        The leaf node should be more stable than just the sequence in the group :)
    let $matchingPreviousShortNames := false()
    let $matchingPreviousShortNames := 
        for $c in $concept/preceding-sibling::concept/@id
        return if (map:get($conceptnamemap, $c) = $shortName) then true() else ()
    let $shortName          :=
        if ($matchingPreviousShortNames = true()) then concat($shortName,'_',tokenize(($concept/@id, $concept/@ref)[1],'\.')[last()]) else $shortName
    
    (: Calculate the right xpath expression for this concept :)
    (: A single row in xpaths will look like:
    <concept ref="..." effectiveDate="..." elementId="..." xpath="/hl7:ClinicalDocument... etc..." valueLocation="@extension" hl7Type="II"/>
    :)
    (: Find the elements in xpaths which contain a concept corresponding to concept/@id :) 
    (: If no xpaths file is available (it's a dataset, xpaths not generated yet, no xpath for this concept (usually no corresponding elementId in templateAssociation)), then empty :) 
    let $xpathRows := if ($xpaths) then ($xpaths//concept[@ref=$concept/@id]/../..) else ()

    (: Try to find the Xpath :)
    let $xpathRow :=
        if (empty($xpathRows)) then 
            ((:<concept message="Warning: Unable to calculate xpath."/>:)) 
        else
        
        (: If there's only one xpath for this concept, that's the one :)
        if (count($xpathRows) = 1) 
        then $xpathRows[1]
        
        (: If there's more than one, see if we have (exactly) one which starts with our parent's xpath, and pick that one :)
        else (
            if (empty($parentXpath)) 
            then 
                <concept message="Warning: More than one xpath for concept, but no xpath for parent.">
                {for $xpathRow in $xpathRows return <xpath>{data($xpathRow/@xpath)}</xpath>}
                </concept> 
            else if (count($xpathRows[starts-with(@xpath, $parentXpath)]) = 1) 
            then $xpathRows[starts-with(@xpath, $parentXpath)][1]
            (: If there is more than one xpath for a concept, and the parent's xpath is not the first part of one of those, then this warning will be hit :)
            else 
                <concept message="Warning: More than one xpath, unable to calculate">
                {for $xpathRow in $xpathRows return <xpath>{data($xpathRow/@xpath)}</xpath>}
                </concept>
        )
    (:                             art:getConceptAssociations($concept, art:getOriginalForConcept(art:getConcept($concept/@ref, $concept/@flexibility)), $mode, true()):)
    let $conceptAssociations    := 
        if ($transactionconcept) then (
            let $tr     := map:get($transactionconceptmap, 'transaction')
            let $tc     := <transaction>{$tr/@*, $transactionconcept}</transaction>
            return art:getConceptAssociations($tc/concept, $originalConcept, 'normal', true())
        )
        else (
            art:getConceptAssociations($concept, $originalConcept, 'normal', true())
        )
    
    let $valueDomainSets            :=
        for $valueDomain in $originalConcept/valueDomain
        let $conceptLists :=
            for $conceptList in $valueDomain/conceptList
            return art:getOriginalConceptList($conceptList)
        return (
            <valueDomain>
            {
                $valueDomain/@*,
                for $conceptList in $conceptLists
                return
                    <conceptList>
                    {
                        $conceptList/@*,
                        for $conceptListItem in $conceptList/concept 
                        return (
                            <concept>
                            {
                                $conceptListItem/@*,
                                if ($language = '*') then (
                                    $conceptListItem/name,
                                    $conceptListItem/synonym,
                                    $conceptListItem/desc
                                ) else (
                                    $conceptListItem/name[@language=$language][1],
                                    $conceptListItem/synonym[@language=$language],
                                    $conceptListItem/desc[@language=$language][1]
                                )
                            }
                            </concept>
                        )
                    }
                    </conceptList>
                ,
                $valueDomain/property[@*[string-length() gt 0]]
                ,
                for $ex in $valueDomain/example
                return
                    <example type="{($ex/@type, 'neutral')[1]}">{$ex/@caption, $ex/node()}</example>
            }
            </valueDomain>
            ,
            (: add enhanced valueSet :)
            for $conceptList in $conceptLists
            return art:getEnhancedValueSet($decor, $conceptList, $conceptAssociations, $language)
        )
    
    return 
        <concept>
        {
            (: attributes from concept except @type from originalConcept and @navkey which is irrelevant here :)
            $concept/(@* except (@navkey|@type|@minimumMultiplicity|@maximumMultiplicity|@conformance|@isMandatory))[not(.='')],
            $originalConcept/@type,
            if (empty($transactionconceptmap)) then () else (
                attribute minimumMultiplicity {if ($transactionconcept) then art:getMinimumMultiplicity($transactionconcept) else ('0')},
                attribute maximumMultiplicity {if ($transactionconcept) then art:getMaximumMultiplicity($transactionconcept) else ('*')},
                if ($transactionconcept/@isMandatory='true') then
                    attribute conformance {'M'}
                else if ($transactionconcept/@conformance[not(.='')]) then
                    $transactionconcept/@conformance
                else (),
                attribute isMandatory {$transactionconcept/@isMandatory='true'}
            ),
            if ($concept[@iddisplay | @refdisplay]) then () else 
            if ($concept[@id]) then (
                let $iddisplay  := map:get($oidnamemap, $concept/@id)
                let $iddisplay  := if (empty($iddisplay)) then art:getNameForOID($concept/@id, $language, $decor) else ($iddisplay)
                return
                    attribute iddisplay {$iddisplay}
            ) else
            if ($concept[@ref]) then (
                let $iddisplay  := map:get($oidnamemap, $concept/@ref)
                let $iddisplay  := if (empty($iddisplay)) then art:getNameForOID($concept/@ref, $language, $decor) else ($iddisplay)
                return
                    attribute refdisplay {$iddisplay}
            ) else (),
            (: Move shortName to concept, but keep in implementation (at least for a while) for backward compatibility :)
            if ($shortName) then attribute shortName {$shortName} else (),
            $transactionconcept/@enableBehavior[not(.='')]
        }
        {
            <implementation>
            {
                if ($shortName) then attribute shortName {$shortName} else (), 
                if ($xpathRow/@datatype) then attribute hl7Type {$xpathRow/@datatype} else (),
                $xpathRow/@xpath,
                $xpathRow/@message,
                if ($xpathRow/ancestor::template) then element templateLocation {$xpathRow/ancestor::template[1]/@*} else (),
                $xpathRow/xpath
            }
            </implementation>
        }
        {
            if ($transactionconcept) then (
                if ($language = '*') then (
                    $transactionconcept/context
                )
                else (
                    $transactionconcept/context[@language=$language][1]
                )
                ,
                (: element children, not concept &amp; history children, filtered for language :)
                for $condition in $transactionconcept/condition
                let $isMandatory    := $condition/@isMandatory='true'
                let $conformance    := if ($isMandatory) then 'M' else ($condition/@conformance)
                return
                <condition>
                {
                    attribute minimumMultiplicity {art:getMinimumMultiplicity($condition)},
                    attribute maximumMultiplicity {art:getMaximumMultiplicity($condition)},
                    if (empty($conformance)) then () else attribute conformance {$conformance},
                    attribute isMandatory {$isMandatory}
                    ,
                    if ($language = '*') then
                        $condition/desc
                    else (
                        $condition/desc[@language = $language][1]
                    )
                    ,
                    if ($condition[desc]) then () else 
                    if ($condition[string-length(.)=0]) then () else (
                        if ($language = '*') then
                            <desc language="{$originalConcept/name[1]/@language}">{$condition/text()}</desc>
                        else (
                            <desc language="{$language}">{$condition/text()}</desc>
                        )
                    )
                }
                </condition>
                ,
                $transactionconcept/enableWhen,
                (: new in AD 3.10 - no real support, just make sure we don't loose it :)
                $transactionconcept/containedTransaction
            )
            else ()
        }
        {
            if ($concept[inherit]) then 
                if ($concept[@refdisplay]) then (
                    $concept
                ) else (
                    <inherit>
                    {
                        $concept/inherit/@ref, $concept/inherit/@effectiveDate,
                        $inheritConcept/ancestor::decor/project/@prefix,
                        if ($inheritConcept) then (
                            attribute datasetId {$inheritConcept/ancestor::dataset/@id},
                            attribute datasetEffectiveDate {$inheritConcept/ancestor::dataset/@effectiveDate},
                            attribute datasetStatusCode {$inheritConcept/ancestor::dataset/@statusCode},
                            if ($inheritConcept/ancestor::dataset[@expirationDate]) then attribute datasetExpirationDate {$inheritConcept/ancestor::dataset/@expirationDate} else (),
                            if ($inheritConcept/ancestor::dataset[@versionLabel]) then attribute datasetVersionLabel {$inheritConcept/ancestor::dataset/@versionLabel} else (),
                            attribute iType {$originalConcept/@type}, 
                            attribute iStatusCode {$inheritConcept/@statusCode}, 
                            attribute iEffectiveDate {$inheritConcept/@effectiveDate},
                            if ($inheritConcept[@expirationDate]) then attribute iExpirationDate {$inheritConcept/@expirationDate} else (),
                            if ($inheritConcept[@versionLabel]) then attribute iVersionLabel {$inheritConcept/@versionLabel} else ()
                        ) else (
                            attribute linkedartefactmissing {true()}
                        )
                        ,
                        let $compare    := $inheritConcept[@id = $originalConcept/@id]
                        return
                        if ($compare[@effectiveDate = $originalConcept/@effectiveDate]) then () else (
                            attribute originalId {$originalConcept/@id}, 
                            attribute originalEffectiveDate {$originalConcept/@effectiveDate}, 
                            attribute originalStatusCode {$originalConcept/@statusCode}, 
                            if ($originalConcept[@expirationDate]) then attribute originalExpirationDate {$originalConcept/@expirationDate} else (),
                            if ($originalConcept[@versionLabel]) then attribute originalVersionLabel {$originalConcept/@versionLabel} else (),
                            attribute originalPrefix {$originalConcept/ancestor::decor/project/@prefix}
                        )
                        ,
                        let $iddisplay  := map:get($oidnamemap, $concept/inherit/@ref)
                        let $iddisplay  := if (empty($iddisplay)) then art:getNameForOID($concept/inherit/@ref, $concept/ancestor::decor/project/@defaultLanguage, $inheritConcept/ancestor::decor) else ($iddisplay)
                        return
                            attribute refdisplay {$iddisplay}
                        ,
                        (: check if we have any additional immediate child concepts that the original does not ... 
                           these might also be flagged linkedartefactmissing if they are really gone but more likely they are moved to somewhere else :)
                        let $currentExtra       :=
                            for $currentChild in $concept/concept
                            let $inheritChild := $inheritConcept/concept[@id = $currentChild/inherit/@ref][@effectiveDate = $currentChild/inherit/@effectiveDate]
                            let $inheritChild := if ($inheritChild) then $inheritChild else $originalConcept/concept[@id = $currentChild/inherit/@ref][@effectiveDate = $currentChild/inherit/@effectiveDate]
                            return
                                if ($inheritChild) then () else $currentChild
                        
                        (: check if our original has any additional immediate child concepts that the current does not ... :)
                        let $inheritExtra       :=
                            for $inheritChild in $inheritConcept/concept | $originalConcept/concept
                            let $currentChild := $concept/concept/inherit[@ref = $inheritChild/@id][@effectiveDate = $inheritChild/@effectiveDate]
                            return
                                if ($currentChild) then () else $inheritChild
                    
                        return
                            if ($currentExtra | $inheritExtra) then (
                                attribute inheritanceIssues {true()},
                                attribute inheritExtraConcepts {count($inheritExtra)},
                                attribute currentExtraConcepts {count($currentExtra)}
                            )
                            else ()
                    }
                    </inherit>
                )
            else ()
        }
        {
            let $containedConcept           :=
                if ($concept[contains]) then $concept else
                if ($inheritConcept[contains]) then $inheritConcept else
                if ($finalInherit[contains]) then $finalInherit else ()
            let $originalContainedConcept   :=
                if ($concept[contains]) then $originalConcept else
                if ($containedConcept[contains]) then art:getOriginalForConcept($containedConcept) else ()
            
            return
            if ($containedConcept) then
                <contains>
                {
                    $containedConcept/contains/@ref, $containedConcept/contains/@flexibility,
                    $originalContainedConcept/ancestor::decor/project/@prefix,
                    if ($originalContainedConcept) then (
                        attribute datasetId {$originalContainedConcept/ancestor::dataset/@id},
                        attribute datasetEffectiveDate {$originalContainedConcept/ancestor::dataset/@effectiveDate},
                        attribute datasetStatusCode {$originalContainedConcept/ancestor::dataset/@statusCode},
                        if ($originalConcept/ancestor::dataset[@expirationDate]) then attribute datasetExpirationDate {$originalContainedConcept/ancestor::dataset/@expirationDate} else (),
                        if ($originalConcept/ancestor::dataset[@versionLabel]) then attribute datasetVersionLabel {$originalContainedConcept/ancestor::dataset/@versionLabel} else (),
                        attribute iType {$originalContainedConcept/@type}, 
                        attribute iStatusCode {$originalContainedConcept/@statusCode}, 
                        attribute iEffectiveDate {$originalContainedConcept/@effectiveDate},
                        if ($originalContainedConcept[@expirationDate]) then attribute iExpirationDate {$originalContainedConcept/@expirationDate} else (),
                        if ($originalContainedConcept[@versionLabel]) then attribute iVersionLabel {$originalContainedConcept/@versionLabel} else ()
                    ) else ()
                    ,
                    let $iddisplay  := map:get($oidnamemap, $containedConcept/contains/@ref)
                    let $iddisplay  := if (empty($iddisplay)) then art:getNameForOID($containedConcept/contains/@ref, $containedConcept/ancestor::decor/project/@defaultLanguage, $originalContainedConcept/ancestor::decor) else ($iddisplay)
                    return
                        attribute refdisplay {$iddisplay}
                }
                </contains>
            else ()
        }
        {
            if ($language = '*') then (
                $originalConcept/name[.//text()],
                $originalConcept/synonym[.//text()],
                $originalConcept/desc[.//text()],
                $originalConcept/source[.//text()],
                $originalConcept/rationale[.//text()],
                if ($concept[inherit | contains]) then $concept/comment[.//text()] else (),
                $originalConcept/comment[.//text()]
            )
            else (
                let $name   := $originalConcept/name[@language=$language][.//text()][1]
                return 
                if ($name) then $name else (
                    <name language="{$language}">{$originalConcept/name[.//text()][1]/node()}</name>
                )
                ,
                $originalConcept/synonym[@language=$language][.//text()]
                ,
                let $desc   := $originalConcept/desc[@language=$language][.//text()][1]
                return 
                if ($desc) then $desc else (
                    <desc language="{$language}">{$originalConcept/desc[.//text()][1]/node()}</desc>
                )
                ,
                $originalConcept/source[@language=$language][.//text()][1],
                $originalConcept/rationale[@language=$language][.//text()][1],
                if ($concept[inherit | contains]) then $concept/comment[@language=$language][.//text()] else (),
                $originalConcept/comment[@language=$language][.//text()]
            ), 
            $originalConcept/property (:new since 2015-04-21:)
            ,
            (:new since 2015-04-21:)
            for $node in $originalConcept/relationship
            let $relatedConcept           := art:getConcept($node/@ref, $node/@flexibility)
            let $relatedOriginalConcept   := art:getOriginalForConcept($relatedConcept)
            return
                <relationship>
                {
                    $node/@type, $node/@ref, $node/@flexibility, $node/@refdisplay,
                    $relatedConcept/ancestor::decor/project/@prefix,
                    if ($relatedConcept) then (
                        attribute datasetId {$relatedConcept/ancestor::dataset/@id},
                        attribute datasetEffectiveDate {$relatedConcept/ancestor::dataset/@effectiveDate},
                        attribute datasetStatusCode {$relatedConcept/ancestor::dataset/@statusCode},
                        if ($relatedConcept/ancestor::dataset[@expirationDate]) then attribute datasetExpirationDate {$relatedConcept/ancestor::dataset/@expirationDate} else (),
                        if ($relatedConcept/ancestor::dataset[@versionLabel]) then attribute datasetVersionLabel {$relatedConcept/ancestor::dataset/@versionLabel} else (),
                        attribute iType {$relatedOriginalConcept/@type}, 
                        attribute iStatusCode {$relatedConcept/@statusCode}, 
                        attribute iEffectiveDate {$relatedConcept/@effectiveDate},
                        if ($relatedConcept[@expirationDate]) then attribute iExpirationDate {$relatedConcept/@expirationDate} else (),
                        if ($relatedConcept[@versionLabel]) then attribute iVersionLabel {$relatedConcept/@versionLabel} else ()
                    ) else (),
                    if ($node[@refdisplay]) then () else (
                        let $iddisplay  := map:get($oidnamemap, $node/@ref)
                        let $iddisplay  := if (empty($iddisplay)) then art:getNameForOID($node/@ref, $relatedConcept/ancestor::decor/project/@defaultLanguage, $relatedConcept/ancestor::decor) else ($iddisplay)
                        return
                            attribute refdisplay {$iddisplay}
                    ),
                    $relatedOriginalConcept/name
                }
                </relationship>
            ,
            if ($language = '*') then
                $originalConcept/operationalization[.//text()][1]
            else (
                $originalConcept/operationalization[@language=$language][.//text()][1]
            )
        }
        {
            (: this part is not necessary as we already solve the right stuff using art:getConceptAssociations() 
                with transaction concept as perspective when relevant :)
                
                (:(\:skip empty @strength attributes:\)
                for $node in $transactionconcept/terminologyAssociation[empty(@code)]
                return
                    <terminologyAssociation>{$node/@*[not(. = '')], $node/node()}</terminologyAssociation>
                ,
                for $item in $transactionconcept/terminologyAssociation[@code]
                (\: Quite often the displayName is not present on terminologyAssociation, but is there in $concept/valueSet/concept
                If it is, we pick it up there to avoid hitting the expensive getDisplayNameFromCodesystem :\)
                let $displayName    := 
                    if ($item/@displayName[not(. = '')]) then $item/@displayName else (
                        $valueDomainSets//concept[@code = $item/@code][@codeSystem = $item/@codeSystem]/@displayName |
                        $valueDomainSets//exception[@code = $item/@code][@codeSystem = $item/@codeSystem]/@displayName
                    )[1]
                (\:let $displayName    := if (empty($displayName)) then (art:getDisplayNameFromCodesystem($item/@code, $item/@codeSystem, $language)) else $displayName:\)
                return
                    <terminologyAssociation>
                    {
                        $item/@*[not(. = '')],
                        if ($item/@displayName[not(. = '')]) then () else if (empty($displayName)) then () else (
                            attribute displayName {$displayName}
                        )
                    }
                    </terminologyAssociation>
                ,
                for $node in $transactionconcept/identifierAssociation
                return
                    <identifierAssociation>{$node/@*[not(. = '')], $node/node()}</identifierAssociation>:)
            
            
            let $valueDomainAssociations    := (
                (:skip empty @strength attributes:)
                for $node in $conceptAssociations/terminologyAssociation[empty(@code)]
                return
                    <terminologyAssociation>{$node/@*[not(. = '')], $node/node()}</terminologyAssociation>
                ,
                for $item in $conceptAssociations/terminologyAssociation[@code]
                (: Quite often the displayName is not present on terminologyAssociation, but is there in $concept/valueSet/concept
                If it is, we pick it up there to avoid hitting the expensive getDisplayNameFromCodesystem :)
                let $displayName    := 
                    if ($item/@displayName[not(. = '')]) then $item/@displayName else (
                        $valueDomainSets//concept[@code = $item/@code][@codeSystem = $item/@codeSystem]/@displayName |
                        $valueDomainSets//exception[@code = $item/@code][@codeSystem = $item/@codeSystem]/@displayName
                    )[1]
                (:let $displayName    := if (empty($displayName)) then (art:getDisplayNameFromCodesystem($item/@code, $item/@codeSystem, $language)) else $displayName:)
                return
                    <terminologyAssociation>
                    {
                        $item/@*[not(. = '')],
                        if ($item/@displayName[not(. = '')]) then () else if (empty($displayName)) then () else (
                            attribute displayName {$displayName}
                        )
                    }
                    </terminologyAssociation>
                ,
                for $node in $conceptAssociations/identifierAssociation
                return
                    <identifierAssociation>{$node/@*[not(. = '')], $node/node()}</identifierAssociation>
            )
            return
                $valueDomainSets | $valueDomainAssociations
        }
        {
            if (empty($transactionconceptmap)) then (
                let $conceptset         := $concept/concept
                let $conceptnamemap2    := art:getConceptNameMap($conceptset, $language)
                    
                for $conceptChild in $conceptset
                return art:getFullConcept($decor, $conceptChild, $language, $xpaths, $xpathRow/@xpath, $oidnamemap, $transactionconceptmap, $conceptnamemap2)
            )
            else (
                let $conceptset         := $concept/concept[.[map:contains($transactionconceptmap, @id)] | descendant::concept[map:contains($transactionconceptmap, @id)]]
                let $conceptnamemap2    := art:getConceptNameMap($conceptset, $language)
                    
                for $conceptChild in $conceptset
                return art:getFullConcept($decor, $conceptChild, $language, $xpaths, $xpathRow/@xpath, $oidnamemap, $transactionconceptmap, $conceptnamemap2)
            )
        }
        </concept>
};

(:~ Returns a name which (in most cases) should be acceptable as XML element or SQL column name
    Most common diacritics are replaced
    
    Note: an implementation of this function exists in XSL too in the ADA package shortName.xsl. Changes here should be reflected there too and vice versa.
    
    Input:  xs:string, example: "Underscored Lowercase ë"
    Output: xs:string, example: "underscored_lowercase_e"
    
    @author Marc de Graauw, Alexander Henket
    @since 2013
:)
declare function art:shortName($name as xs:string?) as xs:string? {
    let $shortname := 
        if ($name) then (
            (: add some readability to CamelCased names. E.g. MedicationStatement to Medication_Statement. :)
            let $r0 := replace($name, '([a-z])([A-Z])', '$1_$2')
            
            (: find matching alternatives for <=? smaller(equal) and >=? greater(equal) :)
            let $r1 := replace($r0, '<\s*=', 'le')
            let $r2 := replace($r1, '<', 'lt')
            let $r3 := replace($r2, '>\s*=', 'ge')
            let $r4 := replace($r3, '>', 'gt')
            
            (: find matching alternatives for more or less common diacriticals, replace single spaces with _ , replace ? with q (same name occurs quite often twice, with and without '?') :)
            let $r5 := translate(normalize-space(lower-case($r4)),' àáãäåèéêëìíîïòóôõöùúûüýÿç€ßñ?','_aaaaaeeeeiiiiooooouuuuyycEsnq')
            (: ditch anything that's not alpha numerical or underscore :)
            let $r6 := replace($r5,'[^a-zA-Z\d_]','')
            (: make sure we do not start with a digit :)
            let $r7 := replace($r6, '^(\d)' , '_$1')
            return $r7
        ) else ()
    
    return if (matches($shortname, '^[a-zA-Z_][a-zA-Z\d_]+$')) then $shortname else ()
};

(:~ Returns an enhanced valueSet for a conceptList
    Input:  collection (take care to send more than project if conceptList/@ref points outside project)
            conceptList (from a dataset/concept/valueDomain)
            language, used to filter names, may be '' for no filtering
    Output: 
        valueSet with attributes from valueSet:
            <valueSet id="2.16.840.1.113883.2.4.3.46.99.3.11.5" effectiveDate="2012-07-25T15:22:56" name="tfw-meting-door" displayName="tfw-meting-door" statusCode="draft">
        conceptList with id from concept:
                <conceptList id="2.16.840.1.113883.2.4.3.46.99.3.2.55.0">
        all concepts from valueSet, 
        if a terminologyAssociation for concept exists, name from dataset, filtered by language if $language is not ''
                    <concept localId="1" code="P" codeSystem="2.16.840.1.113883.2.4.3.46.99.50" displayName="displayName-from-valueset" level="0" type="L">
                        <name language="nl-NL">Name from dataset conceptList's concept</name>
                    </concept>
        if no terminologyAssociation for concept exists, <name> is taken from @displayName in valueSet
                    <exception localId="6" code="OTH" codeSystem="2.16.840.1.113883.5.1008" displayName="Anders" level="0" type="L">
                        <name>Anders</name>
                    </exception>
                </conceptList>
            </valueSet>
        if no terminologyAssociation for valueSet exists, returns empty valueSet/conceptList:
            <valueSet><conceptList id="2.16.840.1.113883.2.4.3.46.99.3.2.55.0"/></valueSet>
        localId is a unique number within this particular conceptList (since code without codeSystem may not be unique) for use in code generators
        localId is only guaranteed to be unique within a single call, may change after dataset changes
        
    @author Marc de Graauw, Alexander Henket 
    @since 2013
:)
declare function art:getEnhancedValueSet($decor as element(decor), $conceptList as element(), $language as xs:string) as element()* {
    let $concept    := $conceptList/ancestor::concept[1]
    let $assocs     := art:getConceptAssociations($concept, 'normal', true())
    return
    art:getEnhancedValueSet($decor, $conceptList, $assocs, $language)
};
declare function art:getEnhancedValueSet($decor as element(decor), $conceptList as element(), $conceptAssociations as element()*, $language as xs:string) as element()* {
    let $prefix                     := $decor/project/@prefix
    let $conceptList                := art:getOriginalConceptList($conceptList)
    let $conceptListAssociations    := $conceptAssociations/terminologyAssociation[@conceptId=$conceptList/@id][empty(@expirationDate)]
    let $conceptAssociations        := $conceptAssociations/terminologyAssociation[@conceptId=$conceptList/concept/@id][empty(@expirationDate)]
    
    return
        if ($conceptListAssociations) then (
            for $association in $conceptListAssociations
            let $flexibility        := if ($association/@flexibility) then $association/@flexibility else 'dynamic'
            let $valueSet           :=
                if ($association[string-length(@valueSet)=0]) then
                    error(xs:QName('error:NotEnoughArguments'), concat('Terminology association for concept ', $conceptList/ancestor::concept[1]/@id, ' ''', $conceptList/ancestor::concept[1]/name[1], ''' with conceptList ', $association/@conceptId,' is missing the valueSet attribute.'))
                else
                    vs:getExpandedValueSetByRef($association/@valueSet, $flexibility, $prefix, $decor/@versionDate, ($decor/@language[not(. = ('*', ''))], $decor/project/@defaultLanguage)[1], false())/*/valueSet[@id]
            return (
                <valueSet>
                {
                    $valueSet[1]/(@* except (@strength)),
                    $association/@strength[not(.='')],
                    (:skip empty @strength attributes:)
                    for $node in $association
                    return
                        element {name($node)} {$node/@*[not(. = '')], $node/node()}
                    ,
                    for $node in $valueSet[1]/(* except conceptList)
                    return
                        if (name($node) = 'desc') then art:parseNode($node) else
                        if (name($node) = 'revisionHistory') then
                            <revisionHistory>
                            {
                                $node/@*
                                ,
                                for $subnode in $node/desc
                                return
                                    art:parseNode($subnode)
                            }
                            </revisionHistory>
                        else (
                            $node
                        )
                    ,
                    <conceptList id="{$conceptList/@id}">
                    {
                        for $concept at $pos in $valueSet[1]/conceptList/*
                        (: rewrite concept with NullFlavor to exception. NullFlavor never is a concept :)
                        let $conceptName            := 
                            if ($concept[self::concept | self::exception][@codeSystem = '2.16.840.1.113883.5.1008']) then 
                                'exception'
                            else (name($concept))
                        let $conceptAssociation     := $conceptAssociations[@code = $concept/@code][@codeSystem = $concept/@codeSystem]
                        let $conceptListConcept     := if ($conceptAssociation) then $conceptList/concept[@id = $conceptAssociation/@conceptId] else ()
                        let $conceptDesignations    := 
                            if ($language = '*') then $concept/designation else (
                                (: SNOMED CT does not do full lang-COUNTRY, just lang :)
                                $concept/designation[@language = $language] | $concept/designation[@language = substring-before($language, '-')]
                            )
                        return 
                            element {$conceptName}
                            {
                                attribute localId {$pos},
                                $concept/(@* except (@exception|@equivalence)),
                                (: rewrite/add @exception of includes for the HL7 ValueSet for 
                                NullFlavor to exception="true". NullFlavor never is a concept :)
                                if ($concept[self::include][@ref = '2.16.840.1.113883.1.11.10609'][not(@exception = 'true')]) then
                                    attribute exception {'true'}
                                else ($concept/@exception)
                                ,
                                $conceptAssociation[1]/@equivalence[not(.='')]
                                ,
                                if ($language = '*') then (
                                    $conceptListConcept/name,
                                    for $lang in $conceptList/parent::concept/@language
                                    return
                                        if ($conceptListConcept/name[@language = $lang]) then () else
                                        if ($conceptDesignations[@type = 'fsn']) then
                                            <name language="{$lang}">{data($conceptDesignations[@language = ($lang, substring-before($lang, '-'))][@type = 'fsn'][1]/@displayName)}</name>
                                        else
                                        if ($conceptDesignations[@type = 'preferred']) then
                                            <name language="{$lang}">{data($conceptDesignations[@language = ($lang, substring-before($lang, '-'))][@type = 'preferred'][1]/@displayName)}</name>
                                        else (
                                            <name language="{$lang}">{data($concept/(@displayName, @codeSystemName)[1])}</name>
                                        )
                                    ,
                                    for $synonym in $conceptListConcept/synonym
                                    return
                                        <designation language="{$synonym/@language}" type="synonym" displayName="{data($synonym)}"/>
                                    ,
                                    $conceptDesignations
                                )
                                else if ($conceptListConcept) then (
                                    if ($conceptListConcept/name[@language = $language]) then 
                                        $conceptListConcept/name[@language = $language]
                                    else
                                    if ($conceptDesignations[@type = 'fsn']) then
                                        <name language="{$language}">{data($conceptDesignations[@type = 'fsn'][1]/@displayName)}</name>
                                    else
                                    if ($conceptDesignations[@type = 'preferred']) then
                                        <name language="{$language}">{data($conceptDesignations[@type = 'preferred'][1]/@displayName)}</name>
                                    else (
                                        <name language="{$language}">{data($concept/(@displayName, @codeSystemName)[1])}</name>
                                    )
                                    ,
                                    for $synonym in $conceptListConcept/synonym[@language = $language]
                                    return
                                        <designation language="{$language}" type="synonym" displayName="{data($synonym)}"/>
                                    ,
                                    $conceptDesignations
                                )
                                else (
                                    if ($conceptDesignations[@type = 'fsn']) then
                                        <name language="{$language}">{data($conceptDesignations[@type = 'fsn'][1]/@displayName)}</name>
                                    else
                                    if ($conceptDesignations[@type = 'preferred']) then
                                        <name language="{$language}">{data($conceptDesignations[@type = 'preferred'][1]/@displayName)}</name>
                                    else (
                                        <name language="{$language}">{data($concept/(@displayName, @codeSystemName)[1])}</name>
                                    )
                                    ,
                                    $conceptDesignations
                                )
                                ,
                                if ($language = '*') then (
                                    for $node in $concept/desc
                                    return
                                        if (name($node)) then art:parseNode($node) else *
                                ) else (
                                    for $node in $concept/desc[@language = $language]
                                    return
                                        if (name($node)) then art:parseNode($node) else *
                                )
                            }
                    }
                    </conceptList>
                }
                </valueSet>
            )
        )
        else (
            (: concept list is not related to a valueSet :)
            (:<valueSet><conceptList id="{$conceptList/@id}"/></valueSet>:)
        )
};

(:~ Traverses the fullDatasetTree to add community[@name] elements as child of a matching concept. code originated from RetrieveTransaction 

    @param $fullDatasetTree a dataset to merge community info into
    @param $communities is potentially empty list of communities to check for data associated to $fullDatasetTree concepts
    @param $communityIdMap is a map object that holds the mapping for 'foreign' projectId to projectPrefix. key is expected to be the projectId, the value is expected to be an array where array[1] = prefix and array[position() gt 1] = (currently undecided)
:)
declare function art:mergeDatasetTreeWithCommunity ($fullDatasetTree as element(), $communities as element()*, $communityIdMap as item()) as element(dataset) {
    element {name($fullDatasetTree)}
    {
        $fullDatasetTree/@*
        ,
        for $node in $fullDatasetTree/(* | comment())
        return 
            if ($node instance of element(concept)) then
                art:mergeConceptTreeWithCommunity($node, $communities, $communityIdMap)
            else ($node)
    }
};

(:~ Traverses the fullDatasetTree concept to add community[@name] elements as child of a matching concept. code originated from RetrieveTransaction
    
    @param $concept a dataset concept to merge community info into
    @param $communities is potentially empty list of communities to check for data associated to $concept
    @param $communityIdMap is a map object that holds the mapping for 'foreign' projectId to projectPrefix. key is expected to be the projectId, the value is expected to be an array where array[1] = prefix and array[position() gt 1] = (currently undecided)
:)
declare function art:mergeConceptTreeWithCommunity ($concept as element(concept), $communities as element()*, $communityIdMap as item()) as element(concept) {
    if (empty($communities)) then ($concept) else (
        let $communityInfo      := 
            $communities//object[@ref = $concept/@id][@flexibility = $concept/@effectiveDate] |
            $communities//object[@ref = $concept/@id][empty(@flexibility)]
        let $communityInfo      := $communityInfo/ancestor::association[data]
        
        return
        <concept>
        {
            $concept/@*,
            $concept/(node() except concept)
        }
        {
            for $commInfo in $communityInfo
            let $prototypes    :=
                if ($commInfo/ancestor::community/prototype/@ref) then 
                    doc(xs:anyURI($commInfo/ancestor::community/prototype/@ref))/prototype
                else
                    $commInfo/ancestor::community/prototype
            return
                <community name="{$commInfo/ancestor::community/@name}">
                {
                    (: 2019-07-22 New ... :)
                    (: allows distinction between relevant info from communities by the same name from different projects. 
                        Add only if info in $communityIdMap which should mean it is not from our transaction project :)
                    let $projectId      := $commInfo/ancestor::community/@projectId
                    let $projectIdProps := if ($projectId) then map:get($communityIdMap, $projectId) else ()
                    return (
                        if (empty($projectIdProps)) then () else (
                            (: don't write @projectId as that clashes with main queries on community like decor:getDecorCommunity() :)
                            attribute projectRef {$projectId}
                            ,
                            attribute ident {$projectIdProps[1]}
                        )
                    )
                }
                {
                    for $association in $commInfo/data
                    let $assocType  := $association/@type
                    let $typeDef    := $prototypes/data[@type = $assocType]
                    return
                        <data>
                        {
                            $assocType,
                            $typeDef/(@* except @type),
                            $association/node()
                        }
                        </data>
                }
                </community>
        }
        {
            for $node in $concept/concept
            return 
                art:mergeConceptTreeWithCommunity($node, $communities, $communityIdMap)
        }
        </concept>
    )
};

(: Get the minimumMultiplicity from conditions, or zero if none :)
declare function art:getMinimumMultiplicity($concept as element()) as xs:string {
    let $minimumMultiplicity := 
        if ($concept[enableWhen] | $concept[@conformance='NP'] | $concept[@prohibited='true'] | $concept[@isOptional='true'] | $concept[@minimumMultiplicity = '']) then
            '0'
        else
        if ($concept/@minimumMultiplicity) then 
            $concept/@minimumMultiplicity
        else 
        if ($concept/condition/@minimumMultiplicity) then 
            if ($concept/condition[@minimumMultiplicity = ''] | $concept/condition[@conformance = 'NP']) then '0' else string(min($concept/condition/@minimumMultiplicity))
        else
        if ($concept[self::attribute]) then 
            '1'
        else ('0')
    return $minimumMultiplicity
};

(:~ Get the maximumMultiplicity from conditions, or * if none :)
declare function art:getMaximumMultiplicity($concept as element()) as xs:string {
    let $maximumMultiplicity := 
        if ($concept[@conformance = 'NP'] | $concept[@prohibited = 'true']) then
            '0'
        else
        if ($concept[@maximumMultiplicity = '']) then
            '*'
        else
        if ($concept/@maximumMultiplicity) then 
            $concept/@maximumMultiplicity
        else 
        if ($concept/condition/@maximumMultiplicity) then 
            if ($concept/condition[@maximumMultiplicity = ''] | $concept/condition[@maximumMultiplicity = '*']) then '*' else string(max($concept/condition/@maximumMultiplicity))
        else
        if ($concept[self::attribute]) then 
            '1'
        else ('*')
    return $maximumMultiplicity
};

(:~ serialize textWithMarkup for display in xforms:)
declare function art:serializeNode($textWithMarkup as element()) as element() {
    let $nodeName := name($textWithMarkup)
    return
    element {$nodeName} {
        $textWithMarkup/@*,
        if ($textWithMarkup[* | processing-instruction() | comment()]) then 
            fn:serialize($textWithMarkup/node(),
                <output:serialization-parameters xmlns:output="http://www.w3.org/2010/xslt-xquery-serialization">
                    <output:method>xml</output:method>
                    <output:encoding>UTF-8</output:encoding>
                </output:serialization-parameters>
            )
        else $textWithMarkup/node()
    }
};

(:~ parse serialized content to html for storage :)
declare function art:parseNode($textWithMarkup as element()?) as element()? {
    art:parseNodeByType($textWithMarkup, 'html')
};

(:~ When you parse XML as HTML sometimes funky things happen. Example: when you have a <code/> element in an
    observation, the util:parse-html will assume that the following-siblings of code should have been inside
    code and actually move stuff into it, rendering the example invalid. Hence, when we parse, we only parse
    into html when explicitly instructed to do so, otherwise we parse into XML
    
    Known issue as of eXist-db 5 since util:parse-html is no longer available:
        <desc language="nl-NL">SNOMED CT - SNOMED CT: &lt; 442083009 |Anatomical or acquired body structure|</desc>
        <desc language="nl-NL">&lt;div&gt;SNOMED CT - SNOMED CT: &lt; 442083009 |Anatomical or acquired body structure|&lt;/div&gt;</desc>

    Leads to:    
        err:FODC0006 String passed to fn:parse-xml is not a well-formed XML document.: Document is not valid.

    Input needs to look like: 
        <desc language="nl-NL">&lt;div&gt;SNOMED CT - SNOMED CT: &amp;lt; 442083009 |Anatomical or acquired body structure|&lt;/div&gt;</desc>
:)
declare function art:parseNodeByType($textWithMarkup as element()?, $type as xs:string) as element()? {
    try {
        if (empty($textWithMarkup)) then () else if ($textWithMarkup[*]) then
            (:return as-is. It is XML already:)
            $textWithMarkup
        else 
        if ($type=('html','xhtml')) then
            element {name($textWithMarkup)} {
                $textWithMarkup/@*
                ,
                let $node := util:parse-html($textWithMarkup)
                
                return
                    $node/*/(BODY | body | xhtml:body)/node()
            }
        else (
            element {name($textWithMarkup)} {
                $textWithMarkup/@*,
                fn:parse-xml-fragment($textWithMarkup)/node()
            }
        )
    }
    catch * { 
        (: two possibilities: this is either a simple string with some special character like < > & or ", or this is actually a string with illegal xml :)
        $textWithMarkup
    }
};

(:~ serialize the desc elements submitted, if any language is missing, add a hint that it is available in other languages.
    support for multi language description, adds hint that desc is available in other languages upon missing desc in the respective language
   
   lanuage support so far for 
   de-DE, de-AT
   en-US
   nl-NL
    
   Needs still some more improvements (i.e. list of supported languages etc)
    
:)
declare function art:serializeDescriptionNodes($textWithMarkup as element()*) as element()* {
    art:serializeDescriptionNodes($textWithMarkup, ())
};
declare function art:serializeDescriptionNodes($textWithMarkup as element()*, $project-languages as item()*) as element()* {
    (: process whatever is in the set :)
    for $node in $textWithMarkup
    return art:serializeNode($node)
    ,
    (: add any missing languages, compared to project-languages and art-languages on this server :)
    if ($textWithMarkup) then (
        let $language-set   := 
            for $lang in distinct-values(($project-languages, art:getArtLanguages()))
            return if ($textWithMarkup[@language = $lang]) then () else ($lang)
        
        for $lang in $language-set
        let $matchNode      := $textWithMarkup[starts-with(@language, substring($lang,1,2))]
        let $matchNode      := if ($matchNode) then $matchNode else $textWithMarkup[@language='en-US']
        let $matchNode      := if ($matchNode) then $matchNode else $textWithMarkup[1]
        return
            art:serializeNode(
                element {name($textWithMarkup[1])}
                {
                    attribute language {$lang}
                    ,
                    <span style="color: grey;">
                    {
                        art:getFormResourcesKey('art', $lang, 'no-desc-available')/node(), <br/>, 
                        $matchNode[1]/data(@language), ': ', $matchNode[1]/node()
                    }
                    </span>
                }
            )
    )
    else ()
};
declare function art:serializeNodes($textWithMarkup as element()*) as element()* {
    art:serializeNodes($textWithMarkup, ())
};
declare function art:serializeNodes($textWithMarkup as element()*, $project-languages as item()*) as element()* {
    (: process whatever is in the set :)
    for $node in $textWithMarkup
    return art:serializeNode($node)
    ,
    (: add any missing languages, compared to project-languages and art-languages on this server :)
    if ($textWithMarkup) then (
        let $language-set   := 
            for $lang in distinct-values(($project-languages, art:getArtLanguages()))
            return if ($textWithMarkup[@language = $lang]) then () else ($lang)
        
        for $lang in $language-set
        let $matchNode      := $textWithMarkup[starts-with(@language, substring($lang,1,2))]
        let $matchNode      := if ($matchNode) then $matchNode else $textWithMarkup[@language='en-US']
        let $matchNode      := if ($matchNode) then $matchNode else $textWithMarkup[1]
        return
            art:serializeNode(
                element {name($textWithMarkup[1])}
                {
                    attribute language {$lang}
                    ,
                    $matchNode[1]/node()
                }
            )
    )
    else ()
};
declare function art:getLanguageNodes($textNodes as element()*) as element()* {
    art:getLanguageNodes($textWithMarkup, ())
};
declare function art:getLanguageNodes($textNodes as element()*, $project-languages as item()*) as element()* {
    (: process whatever is in the set :)
    $textNodes
    ,
    (: add any missing languages, compared to project-languages and art-languages on this server :)
    if ($textNodes) then (
        let $language-set   := 
            for $lang in distinct-values(($project-languages, art:getArtLanguages()))
            return if ($textNodes[@language = $lang]) then () else ($lang)
        
        for $lang in $language-set
        let $matchNode      := $textNodes[starts-with(@language, substring($lang,1,2))]
        let $matchNode      := if ($matchNode) then $matchNode else $textNodes[@language='en-US']
        let $matchNode      := if ($matchNode) then $matchNode else $textNodes[1]
        return
            element {name($textNodes[1])}
            {
                attribute language {$lang}
                ,
                $matchNode[1]/node()
            }
    )
    else ()
};

(: **** ID functions **** :)

(:~ Check if input string is an ISO OID
    @param $oid - optional. If empty result is false
    @return true if complies with OID pattern or false otherwise
:)
declare function art:isOid($oid as xs:string?) as xs:boolean {
    matches($oid, "^[0-2](\.(0|[1-9][0-9]*))*$")
};

(:~ get a name for an OID, e.g. "SNOMED-CT" for 2.16.840.1.113883.6.96
    - First check the OID Registry Lookup file
    - Then check any DECOR defined ids in repositories, or specific to a project
    - Then check any DECOR defined codeSystems in repositories, or specific to the project
    - If no name could be found, return empty string
    
    @param $oid - optional. The OID to get the display name for
    @param $language - required. The language to get the name in
    @param $projectPrefix - optional. The prefix of the project or element(decor) itself. Falls back to 'all' projects marked as repository 
:)
declare function art:getNameForOID($oid as xs:string?, $language as xs:string?, $decorOrPrefix as item()?) as xs:string {
let $decorOrPrefix  := if ($decorOrPrefix instance of xs:string) then $decorOrPrefix[not(. = '')] else ($decorOrPrefix/ancestor-or-self::decor)[1]
let $language       := if (string-length($language) gt 0) then $language else ('en-US')
let $return         :=
    if (string-length($oid)=0) then ('') else (
        try {
            let $registryOids   := $get:colOidsData//oid[@oid = $oid]
            (:First get from primary registration if possible 
              <oid oid="2.16.840.1.113883.2.4.3.1" creationDate="20100101" status="completed" symbolicName="" category="root" registrationAuthorityCode="PRI">
            :)
            let $nameInRegistry := 
                if ($registryOids) then (
                    let $n      := $registryOids[@registrationAuthorityCode = 'PRI']/name[@language = $language]
                    let $n      := if ($n) then $n else $registryOids/name[@language = $language]
                    let $n      := if ($n) then $n else $registryOids[@registrationAuthorityCode = 'PRI']/name
                    let $n      := if ($n) then $n else $registryOids/name
                    return
                        $n[1]
                )
                else ()
            
            return
            if ($nameInRegistry) then 
                $nameInRegistry
            else (
                let $decor          := 
                    if (empty($decorOrPrefix)) then $get:colDecorData else (
                        let $baseDecor  := art:getDecor($decorOrPrefix, (), ())
                        (:let $bbrs       := art:getBuildingBlockRepositories($baseDecor, (), ()):)
                        return
                            $baseDecor, $get:colDecorData
                    )
                let $decor1         := $decor//terminology/codeSystem[@id = $oid] 
                let $decor2         := $decor//terminology/codeSystem[@ref = $oid]
                let $decor3         := $decor//ids/id[@root = $oid]/designation
                let $projectSystems := ($decor1 | $decor2 | $decor3)
                
                let $n              := $projectSystems[@language = $language]/@displayName
                let $n              := if ($n) then $n else $projectSystems/@displayName
                let $n              := if ($n) then $n else $projectSystems/@name
                
                return
                if ($n) then $n else (
                    let $baseId     := art:getBaseId($oid)
                    let $id         := if ($baseId) then replace($oid, concat($baseId/@id,'\.?'), $baseId/@prefix) else ($oid)
                    return
                        if ($oid = $id) then () else ($id)
                )
            )
        }
        catch * {()}
    )
return if ($return) then $return[1] else ('')
};

(: OID coming in: 2.16.840.1.113883.3.1937.99.62.3.1.1
    Look for the longest matching OID we can get a name for
    2.16.840.1.113883.3.1937.99.62.3.1
    2.16.840.1.113883.3.1937.99.62.3
    2.16.840.1.113883.3.1937.99.62
    2.16.840.1.113883.3.1937.99
    2.16.840.1.113883.3.1937
    2.16.840.1.113883.3
    2.16.840.1.113883
    2.16.840.1
    2.16.840
    2.16
    2
:)
declare %private function art:getBaseId($oid as xs:string?) as element(baseId)? {
    if (string-length($oid) = 0) then () else (
        (:let $baseId := map:get($basemap, $oid):)
        let $baseId := $get:colDecorData//baseId[@id = $oid][@prefix]
        
        return
        if ($baseId) then $baseId[1] else (
            art:getBaseId(string-join(tokenize($oid,'\.')[position() lt last()], '.'))
        )
    )
};
(:~ get a HL7 FHIR URI for an OID, e.g. "http://snomed.info/sct" for 2.16.840.1.113883.6.96
    - First check the OID Registry Lookup file
    - Then check any DECOR defined ids in repositories, or specific to a project
    - If no URI could be found, but OID was provided return urn:oid:"input"
    - If OID was empty, return empty
    
    params:
    @param $resourceType - required - the FHIR resource type to construct for
    @param $oid - optional. The OID to get the FHIR URI for
    @param $flexibility - optional. the flexibility of the object to get it from, if applicable. Latest if missing
    @param $projectPrefix - optional. The prefix of the project. Falls back to 'all' projects marked as repository 
    @param $fhirVersion - optional. The version of FHIR to retrieve the canonical for. Use any value from $get:arrKeyFHIRVersions, 
            or leave empty for the most current value. FHIR has changed the URIs a lot in various versions, hence the hassle
:)
declare function art:getCanonicalUriForOID($resourceType as xs:string, $object as element(), $decorOrPrefix as item()?, $fhirVersion as xs:string?) as xs:string* {
    if ($object[@canonicalUri]) then $object/@canonicalUri else
        switch (local-name($object))
        case 'include' return art:getCanonicalUriForOID($resourceType, $object/@ref, $object/@flexibility, $decorOrPrefix, $fhirVersion)
        case 'element' return art:getCanonicalUriForOID($resourceType, $object/@contains, $object/@flexibility, $decorOrPrefix, $fhirVersion)
        case 'relationship' return art:getCanonicalUriForOID($resourceType, $object/(@ref | @template), $object/@flexibility, $decorOrPrefix, $fhirVersion)
        case 'vocabulary' return art:getCanonicalUriForOID($resourceType, $object/@valueSet, $object/@flexibility, $decorOrPrefix, $fhirVersion)
        default return art:getCanonicalUriForOID($resourceType, $object/(@id, @ref)[1], $object/(@effectiveDate, @flexibility)[1], $decorOrPrefix, $fhirVersion)
};
declare function art:getCanonicalUriForOID($resourceType as xs:string, $oid as xs:string?, $flexibility as xs:string?, $decorOrPrefix as item()?, $fhirVersion as xs:string?) as xs:string* {
let $return         :=
    if (string-length($oid)=0) then () else (
        try {
            let $fhirKey     := get:strKeyCanonicalUriFhirPrefd($fhirVersion)
            
            (: oid registry :)
            let $oids        := $get:colOidsData//oid[@oid = $oid]/property[@type = $fhirKey]
            let $oids        := if ($oids) then $oids else $get:colOidsData//oid[@oid = $oid]/property[@type = $get:strKeyCanonicalUriPrefd]
            
            (: project :)
            let $oids        := if ($oids) then $oids else $get:colDecorData//codeSystem[@id = $oid]
            let $oids        := if ($oids) then $oids else $get:colDecorData//valueSet[@id = $oid]
            let $oids        := if ($oids) then $oids else $get:colDecorData//dataset[@id = $oid]
            let $oids        := if ($oids) then $oids else $get:colDecorData//transaction[@id = $oid]
            let $oids        := if ($oids) then $oids else $get:colDecorData//template[@id = $oid]
            let $oids        := if ($oids) then $oids else $get:colDecorData//concept[@id = $oid][not(ancestor::history)]
            let $oids        := if ($oids) then $oids else $get:colDecorData//questionnaire[@id = $oid]
            let $oids        := if ($oids) then $oids else $get:colDecorData//conceptMap[@id = $oid]
            let $oids        := 
                if ($oids) then $oids else (
                    let $ids    := $get:colDecorData//id[@root = $oid]/property[@name = $fhirKey]
                    let $ids   := if ($ids) then $oids else $get:colDecorData//id[@root = $oid]/property[@name = $get:strKeyCanonicalUriPrefd]
                    return  
                        if (empty($decorOrPrefix)) then
                            $ids[ancestor::decor/@repository = 'true']
                        else
                        if ($decorOrPrefix instance of element(decor)) then 
                            $ids[ancestor::decor/@repository = 'true'] | $ids[ancestor::decor/project/@prefix = $decorOrPrefix/project/@prefix]
                        else (
                            $ids[ancestor::decor/@repository = 'true'] | $ids[ancestor::decor/project/@prefix = $decorOrPrefix]
                        )
                )
            let $resourceType :=
                switch (local-name($oids[1]))
                case 'codeSystem' return 'CodeSystem'
                case 'questionnaire' return 'Questionnaire'
                case 'template' return 'StructureDefinition'
                case 'valueSet' return 'ValueSet'
                default return $resourceType
            return
                distinct-values(
                    if ($oids[@id]) then 
                        (: project related artifacts :)
                        if ($flexibility castable as xs:dateTime) then
                            art:getCanonicalUri($resourceType, $oids[@effectiveDate = $flexibility])
                        else
                            art:getCanonicalUri($resourceType, $oids[@effectiveDate = max($oids/xs:dateTime(@effectiveDate))])
                    else 
                    if ($oids[@name]) then
                        (: project related ids :)
                        $oids
                    else (
                    (: oid registry :)
                        $oids/@value
                    )
                )
        }
        catch * {()}
    )
return if (empty($return)) then (if (string-length($oid) gt 0) then (concat('urn:oid:',$oid)) else ()) else $return[1]
};
(:~ Get canonicalUri from object if present or construct from id and effectiveDate :) 
declare function art:getCanonicalUri($resourceType as xs:string, $object as element()) as xs:string {
    if ($object[@canonicalUri]) then $object/@canonicalUri else (
        art:getCanonicalUri($resourceType, $object/@id, $object/@effectiveDate)
    )
};
(:~ Construct canonicalUri from server registered FHIR Canonical Base, the type and [id]--[effectiveDate as yyyymmddhhmmss] :)
declare function art:getCanonicalUri($resourceType as xs:string, $objectId as xs:string, $objectEffectiveDate as xs:string?) as xs:string {
    (: AD30-1399 Real world trouble from (re)generated canonicals :)
    if ($resourceType = 'CodeSystem') then
        'urn:oid:' || $objectId
    else (
        adserver:getServerURLFhirCanonicalBase() || $resourceType || '/' || string-join(($objectId, replace($objectEffectiveDate, '\D', '')), '--')
    )
};
(:~ get an OID for a HL7 FHIR URI, e.g. 2.16.840.1.113883.6.96 for "http://snomed.info/sct"
    - First check the OID Registry Lookup file
    - Then check any DECOR defined ids in repositories, or specific to a project
    - If no URI could be found, but OID was provided return urn:oid:"input"
    - If OID was empty, return empty
    
    params:
    @param $uri - optional. The FHIR URI to get the OID for
    @param $flexibility - optional. the flexibility of the object to get it from, if applicable. Latest if missing
    @param $projectPrefix - optional. The prefix of the project. Falls back to 'all' projects marked as repository
    @param $fhirVersion - optional. The version of FHIR to retrieve the oid for. Use any value from $get:arrKeyFHIRVersions, 
            or leave empty for the most current value. FHIR has changed the URIs a lot in various versions, hence the hassle
:)
declare function art:getOIDForCanonicalUri($uri as xs:string?, $flexibility as xs:string?, $projectPrefix as xs:string?, $fhirVersion as xs:string?) as xs:string* {
let $return         :=
    if (string-length($uri)=0) then () else (
        try {
            let $fhirKey     := get:strKeyCanonicalUriFhirPrefd($fhirVersion)
            
            (: oid registry :)
            let $oids        := $get:colOidsData//property[@type = $fhirKey][@value = $uri]
            let $oids        := if ($oids) then $oids else $get:colOidsData//property[@type = $get:strKeyCanonicalUriPrefd][@value = $uri]
            
            (: project :)
            let $oids        := if ($oids) then $oids else $get:colDecorData//codeSystem[@id][@canonicalUri= $uri]
            let $oids        := if ($oids) then $oids else $get:colDecorData//valueSet[@id][@canonicalUri= $uri]
            let $oids        := if ($oids) then $oids else $get:colDecorData//dataset[@id][@canonicalUri= $uri]
            let $oids        := if ($oids) then $oids else $get:colDecorData//transaction[@id][@canonicalUri= $uri]
            let $oids        := if ($oids) then $oids else $get:colDecorData//template[@id][@canonicalUri= $uri]
            let $oids        := if ($oids) then $oids else $get:colDecorData//concept[@id][@canonicalUri= $uri][not(ancestor::history)]
            let $oids        := if ($oids) then $oids else $get:colDecorData//questionnaire[@id][@canonicalUri= $uri]
            let $oids        := if ($oids) then $oids else $get:colDecorData//conceptMap[@id][@canonicalUri= $uri]
            let $oids        := 
                if ($oids) then $oids else (
                    let $ids    := $get:colDecorData//property[@name = $fhirKey][. = $uri]
                    let $ids    := if ($ids) then $ids else $get:colDecorData//property[@name = $get:strKeyCanonicalUriPrefd][. = $uri]
                    return  
                        if (empty($projectPrefix)) then
                            $ids[ancestor::decor/@repository = 'true']
                        else (
                            $ids[ancestor::decor/@repository = 'true'] | $ids[ancestor::decor/project/@prefix = $projectPrefix]
                        )
                )
            
            return
                distinct-values(
                    if ($oids[@id]) then 
                    (: project related artifacts :)
                        if ($flexibility castable as xs:dateTime) then
                            $oids[@effectiveDate = $flexibility]/@id
                        else
                            $oids[@effectiveDate = max($oids/xs:dateTime(@effectiveDate))]/@id
                    else 
                    if ($oids[@name]) then
                    (: project related ids :)
                        $oids/ancestor::id[1]/@root
                    else (
                    (: oid registry :)
                        $oids/ancestor::oid[1]/@oid
                    )
                )
        }
        catch * {()}
    )
return $return[1]
};
(:~ set/update/delete a HL7 FHIR URI for an OID, e.g. "http://snomed.info/sct" for 2.16.840.1.113883.6.96
    - First check if the OID has been registered in the project, 
        - if it hasn't, get the name for the OID as preferred displayName and use project/@defaultLanguage
        - if it has, add or overwrite the preferred FHIR URI
    
    params:
    @param $oid - required. The OID to set the FHIR URI for
    @param $uri - optional. The FHIR URI to connect the OID to. Connection property be removed if uri is empty
    @param $projectPrefix - optional. The prefix of the project. Falls back to 'all' projects marked as repository
    @param $fhirVersion - optional. The version of FHIR to connect the canonical for. Use any value from $get:arrKeyFHIRVersions, 
            or leave empty for the most current value. FHIR has changed the URIs a lot in various versions, hence the hassle
    @return xs:boolean true() if the URI was set/updated/deleted or false() if not
:)
declare function art:setCanonicalUriForOID($oid as xs:string, $uri as xs:anyURI?, $projectPrefix as xs:string, $fhirVersion as xs:string?) as xs:boolean {
let $decor          := art:getDecorByPrefix($projectPrefix)
let $fhirKey        := get:strKeyCanonicalUriFhirPrefd($fhirVersion)
let $return         :=
    if (string-length($oid) = 0 or not($decor)) then ( false() ) else (
        let $language       := $decor/project/@defaultLanguage
        let $displayName    := art:getNameForOID($oid, $language, $decor)
        
        let $id             := $decor//id[@root = $oid]
        let $newproperty    := <property name="{$fhirKey}">{$uri}</property>
        let $newid          :=
            <id root="{$oid}">
                <designation language="{$language}" type="preferred" displayName="{$displayName}">{$displayName}</designation>
                {$newproperty}
            </id>
            
        let $updateDelete   :=
            if (string-length($uri) = 0) then
                update delete $id/property[@name = $fhirKey]
            else
            if ($id[property[@name = $get:strKeyCanonicalUriPrefd]]) then
                update replace $id/property[@name = $fhirKey] with $newproperty
            else
            if ($id) then
                update insert $newproperty into $id
            else (
                update insert $newid following ($decor/ids/baseId | $decor/ids/defaultBaseId)[last()]
            )
        return
            true()
    )
return $return
};

(:~ get an HL7 V2 Mnemonic for an OID, e.g. "SCT" for 2.16.840.1.113883.6.96
    - First check the OID Registry Lookup file
    - Then check any DECOR defined ids in repositories, or specific to a project
    - If no URI could be found, but OID was provided return urn:oid:"input"
    - If OID was empty, return empty
    
    params:
    @param $oid - optional. The OID to get the FHIR URI for
    @param $flexibility - optional. the flexibility of the object to get it from, if applicable. Latest if missing
    @param $projectPrefix - optional. The prefix of the project. Falls back to 'all' projects marked as repository 
:)
declare function art:getHL7v2Table0396MnemonicForOID($oid as xs:string?, $flexibility as xs:string?, $projectPrefix as xs:string?) as xs:string* {
let $return         :=
    if (string-length($oid)=0) then () else (
        try {
            let $v2Key     := $get:strKeyHL7v2Table0396CodePrefd
            
            (: oid registry :)
            let $oids        := $get:colOidsData//oid[@oid = $oid]/property[@type = $v2Key]
            
            (: project :)
            let $oids        := if ($oids) then $oids else $get:colDecorData//codeSystem[@hl7v2table0396][@id = $oid]
            let $oids        := 
                if ($oids) then $oids else (
                    let $ids    := $get:colDecorData//id[@root = $oid]/property[@name = $v2Key]
                    return  
                        if (empty($projectPrefix)) then
                            $ids[ancestor::decor/@repository = 'true']
                        else (
                            $ids[ancestor::decor/@repository = 'true'] | $ids[ancestor::decor/project/@prefix = $projectPrefix]
                        )
                )
            
            return
                distinct-values(
                    if ($oids[@hl7v2table0396]) then 
                        (: project related artifacts :)
                        if ($flexibility castable as xs:dateTime) then
                            $oids[@effectiveDate = $flexibility]/@hl7v2table0396
                        else
                            $oids[@effectiveDate = max($oids/xs:dateTime(@effectiveDate))]/@hl7v2table0396
                    else 
                    if ($oids[@name]) then
                    (: project related ids :)
                        $oids
                    else (
                    (: oid registry :)
                        $oids/@value
                    )
                )
        }
        catch * {()}
    )
return $return[1]
};
(:~ get an OID for an HL7 V2 Table 0396 mnemonic, e.g. 2.16.840.1.113883.6.96 for "SCT"
    - First check the OID Registry Lookup file
    - Then check any DECOR defined ids in repositories, or specific to a project
    - If no mnemonic could be found, but OID was provided return urn:oid:"input"
    - If OID was empty, return empty
    
    params:
    @param $uri - optional. The FHIR URI to get the OID for
    @param $flexibility - optional. the flexibility of the object to get it from, if applicable. Latest if missing
    @param $projectPrefix - optional. The prefix of the project. Falls back to 'all' projects marked as repository
:)
declare function art:getOIDForHL7v2Table0396Mnemonic($mnemonic as xs:string?, $flexibility as xs:string?, $projectPrefix as xs:string?) as xs:string* {
let $return         :=
    if (string-length($mnemonic)=0) then () else (
        try {
            let $v2key       := $get:strKeyHL7v2Table0396CodePrefd
            
            (: oid registry :)
            let $oids        := $get:colOidsData//property[@type = $v2key][@value = $mnemonic]
            
            (: project :)
            let $oids        := if ($oids) then $oids else $get:colDecorData//codeSystem[@id][@hl7v2table0396 = $mnemonic]
            let $oids        := 
                if ($oids) then $oids else (
                    let $ids    := $get:colDecorData//property[@name = $v2key][. = $mnemonic]
                    return  
                        if (empty($projectPrefix)) then
                            $ids[ancestor::decor/@repository = 'true']
                        else (
                            $ids[ancestor::decor/@repository = 'true'] | $ids[ancestor::decor/project/@prefix = $projectPrefix]
                        )
                )
            
            return
                distinct-values(
                    if ($oids[@id]) then 
                    (: project related artifacts :)
                        if ($flexibility castable as xs:dateTime) then
                            $oids[@effectiveDate = $flexibility]/@id
                        else
                            $oids[@effectiveDate = max($oids/xs:dateTime(@effectiveDate))]/@id
                    else 
                    if ($oids[@name]) then
                    (: project related ids :)
                        $oids/ancestor::id[1]/@root
                    else (
                    (: oid registry :)
                        $oids/ancestor::oid[1]/@oid
                    )
                )
        }
        catch * {()}
    )
return $return[1]
};
(:~ set/update/delete an HL7 V2 Table 0396 mnemonic for an OID, e.g. "SCT" for 2.16.840.1.113883.6.96
    - First check if the OID has been registered in the project, 
        - if it hasn't, get the name for the OID as preferred displayName and use project/@defaultLanguage
        - if it has, add or overwrite the preferred FHIR URI
    
    params:
    @param $oid - required. The OID to set the FHIR URI for
    @param $mnemonic - optional. The FHIR URI to connect the OID to. Connection property be removed if uri is empty
    @param $projectPrefix - optional. The prefix of the project. Falls back to 'all' projects marked as repository
    @return xs:boolean true() if the URI was set/updated/deleted or false() if not
:)
declare function art:setHL7v2Table0396MnemonicForOID($oid as xs:string, $mnemonic as xs:string?, $projectPrefix as xs:string) as xs:boolean {
let $decor          := art:getDecorByPrefix($projectPrefix)
let $v2key          := $get:strKeyHL7v2Table0396CodePrefd
let $return         :=
    if (string-length($oid) = 0 or empty($decor)) then ( false() ) else (
        let $language       := $decor/project/@defaultLanguage
        let $displayName    := art:getNameForOID($oid, $language, $decor)
        
        let $id             := $decor//id[@root = $oid]
        let $newproperty    := <property name="{$v2key}">{$mnemonic}</property>
        let $newid          :=
            <id root="{$oid}">
                <designation language="{$language}" type="preferred" displayName="{$displayName}">{$displayName}</designation>
                {$newproperty}
            </id>
            
        let $updateDelete   :=
            if (string-length($mnemonic) = 0) then
                update delete $id/property[@name = $v2key]
            else
            if ($id) then
                update insert $newproperty into $id
            else (
                update insert $newid following ($decor/ids/baseId | $decor/ids/defaultBaseId)[last()]
            )
        return
            true()
    )
return $return
};

(: **** Dataset functions **** :)

(:~ retrieve contains/inherit hierarchy for decor concept
:   input: inherit or contains element of decor concept
:   returns: hierarchy of contains/inherit elements containing decor comment elements and the original concept
:)
declare function art:getOriginalConcept($inherit as element()) as element()? {
let $concept    := art:getOriginalForConcept(<concept>{$inherit}</concept>)

return
    element {if (name($inherit) = 'contains') then 'contains' else 'inherit'}
    {
        $inherit/@ref, 
        $inherit/@effectiveDate,
        $inherit/@flexibility,
        attribute prefix {$concept/ancestor::decor/project/@prefix},
        attribute datasetId {$concept/ancestor::dataset[1]/@id},
        attribute datasetEffectiveDate {$concept/ancestor::dataset[1]/@effectiveDate}
        ,
        if ($concept/inherit) then (
            $concept/comment,
            art:getOriginalConcept($concept/inherit)
        ) else 
        if ($concept/contains) then (
            $concept/comment,
            art:getOriginalConcept($concept/contains)
        )
        else (
            $concept
        )
    }
};

(:~  retrieve original concept, no added elements, no preservation of comments that may have been added in inheritance trail. 
:   Just comments of original. Caller needs to keep track of current added comments
:   input: decor concept
:   returns: the original concept
:)
declare function art:getOriginalForConcept($concept as element(concept)?) as element(concept)? {
    if ($concept[inherit]) then (
        let $ref        := $concept/inherit/@ref
        let $eff        := $concept/inherit/@effectiveDate
        
        let $concepts   := art:getConcept($ref, $eff, (), ())
        let $error      := 
            if (count($concepts) le 1) then () else (
                error(xs:QName('error:ClassParentError'),concat('Concept id="',$ref,'" effectiveDate="',$eff,'" (inherited from concept id="',$concept/@id,'" effectiveDate="',$concept/@effectiveDate,'") gave ',count($concepts),' hits. Expected exactly 1'))
            )
        return
            art:getOriginalForConcept($concepts)
    )
    else 
    if ($concept[contains]) then (
        let $ref        := $concept/contains/@ref
        let $eff        := $concept/contains/@flexibility
        
        let $concepts   := art:getConcept($ref, $eff, (), ())
        let $error      := 
            if (count($concepts) le 1) then () else (
                error(xs:QName('error:ClassParentError'),concat('Concept id="',$ref,'" effectiveDate="',$eff,'" (contained from concept id="',$concept/@id,'" effectiveDate="',$concept/@effectiveDate,'") gave ',count($concepts),' hits. Expected exactly 1'))
            )
        return
            art:getOriginalForConcept($concepts)
    )
    else (
        $concept
    )
};

(:~ equivalent to getOriginalConcept, but returns name and desc of original concept only and without inherit hierarchy
    <inherit ref="1.2.3" effectiveDate="yyyy-mm-ddThh:mm:ss"/>
    <contains ref="1.2.3" flexibility="yyyy-mm-ddThh:mm:ss"/>
:)
declare function art:getOriginalConceptName($inherit as element()) as element() {
let $concept    := art:getOriginalForConcept(<concept><inherit>{$inherit/@ref | $inherit/@effectiveDate}</inherit></concept>)

return
    <concept id="{$inherit/@ref}" effectiveDate="{$inherit/@effectiveDate | $inherit/@flexibility}">
    {
        $concept/name,
        $concept/desc
    }
    </concept>
};

(:~ Return the original conceptList. If input has @id, return input. If input has @ref, find original with @id :)
declare function art:getOriginalConceptList($conceptList as element(conceptList)?) as element(conceptList)? {
    if ($conceptList[@ref]) then (
        let $projectPrefix := 
            if ($conceptList/ancestor::decor) then 
                $conceptList/ancestor::decor/project/@prefix
            else (
                (:when called through getOriginalConcept the prefix is on a parent inherit or contains element:)
                ($conceptList/ancestor::inherit/@prefix | $conceptList/ancestor::contains/@prefix)[1]
            )
        (:get original from same project:)
        let $originalConceptList := $get:colDecorData//conceptList[@id = $conceptList/@ref][not(ancestor::history)][ancestor::datasets][ancestor::decor[project[@prefix=$projectPrefix]]]
        return
            if ($originalConceptList) then (
                $originalConceptList[1] (:got original, return it:)
            ) else (
                $conceptList            (:could not find original, just return reference:)
            )
    )
    else (
        (: original or empty :)
        $conceptList
    )
};

(:~ Return id from input if it leads to an original concept. Recurse to return id of the original concept based on inherit :)
declare function art:getOriginalConceptId($conceptId as xs:string, $conceptEffectiveDate as xs:string?) as xs:string? {
let $concept    := art:getOriginalForConcept(<concept><inherit ref="{$conceptId}" effectiveDate="{$effectiveDate}"/></concept>)

return
    $concept/@id
};

(:~ Get .//terminologyAssociation and identifierAssociation element directly from the $concept, or if none exist connected to the dataset concept. 
:   Note that an incoming $concept might be a compiled $concept, a regular dataset concept or a transaction concept.
:)
declare function art:getConceptAssociations ($concept as element(concept)?) as element()* {
    let $datasetConcept     :=
        if ($concept[ancestor::dataset]) then $concept else
        if ($concept[@id]) then (art:getConcept($concept/@id, $concept/@effectiveDate)) else
        if ($concept[@ref]) then (art:getConcept($concept/@ref, $concept/@flexibility)) else ()
    return
    art:getConceptAssociations($concept, art:getOriginalForConcept($datasetConcept))
};
declare function art:getConceptAssociations ($concept as element(concept)?, $originalConcept as element(concept)?) as element()* {
if (empty($concept)) then () else
if ($concept/valueSet/terminologyAssociation | $concept/terminologyAssociation | $concept/identifierAssociation) then (
    (: if for some reason sent us a concept with contained terminologyAssociation then we should assume this is the result of art:getFullConcept and just return what was 'calculated' there.
        caveat: art:getFullConcept does not keep the conceptList/concept associations, only concept and valueSet.
    :)
    $concept/valueSet/terminologyAssociation | $concept/terminologyAssociation | $concept/identifierAssociation
)
else 
if ($concept[ancestor::transaction]) then ()
else (
    let $decor              := $concept/ancestor::decor
    (: if for some reason someone sent us a concept from memory and not from the db, then we will not have a decor ancestor... :)
    let $datasetConcept     :=
        if ($concept[ancestor::dataset]) then $concept else
        if ($concept[@id]) then (art:getConcept($concept/@id, $concept/@effectiveDate)) else
        if ($concept[@ref]) then (art:getConcept($concept/@ref, $concept/@flexibility)) else ()
    
    let $decor              :=  if ($decor) then $decor else $datasetConcept/ancestor::decor
        
    let $conceptLists       :=
        for $conceptList in $originalConcept/valueDomain/conceptList
        return art:getOriginalConceptList($conceptList)
    
    (: map for all concept(List) ids, without any entry value :)
    let $oidset             := distinct-values($concept/@id | $datasetConcept/@id | $originalConcept/@id | $conceptLists//@id)
    
    (: Circumvention of error based on combined index and null input https://github.com/eXist-db/exist/issues/4016 :)
    let $terminologyAssocs  := 
        if (empty($originalConcept)) then
            $decor/terminology/terminologyAssociation[@conceptId = $oidset][not(@conceptFlexibility castable as xs:dateTime)] | 
            $decor/terminology/terminologyAssociation[@conceptId = $datasetConcept/@id][@conceptFlexibility = $datasetConcept/@effectiveDate]
        else (
            $decor/terminology/terminologyAssociation[@conceptId = $oidset][not(@conceptFlexibility castable as xs:dateTime)] | 
            $decor/terminology/terminologyAssociation[@conceptId = $datasetConcept/@id][@conceptFlexibility = $datasetConcept/@effectiveDate] | 
            $decor/terminology/terminologyAssociation[@conceptId = $originalConcept/@id][@conceptFlexibility = $originalConcept/@effectiveDate]
        )
    
    return (
        for $association in $terminologyAssocs
        let $associationType := $association/concat(@conceptId,@valueSet,@flexibility,@code,@codeSystem)
        group by $associationType
        return $association[1]
        ,
        if ($originalConcept[valueDomain[@type= 'identifier']]) then (
            for $association in $decor/ids/identifierAssociation[@conceptId = $oidset][not(@conceptFlexibility castable as xs:dateTime)] | 
                                $decor/ids/identifierAssociation[@conceptId = $datasetConcept/@id][@conceptFlexibility = $datasetConcept/@effectiveDate] | 
                                $decor/ids/identifierAssociation[@conceptId = $originalConcept/@id][@conceptFlexibility = $originalConcept/@effectiveDate]
            let $associationType := $association/concat(@conceptId,@ref)
            group by $associationType
            return $association[1]
        ) else ()
    )
)
};

(:~ Function for retrieving list of terminology associations for (transaction) concept and contained conceptList/concept Modes: all, diff, normal (default)
    - all: retrieve any dataset and transaction terminologyAssociations and identifierAssociations for this concept
    - diff: as 'all', but trimmed only to those 
        - in any transactions if this a dataset concept ($trid is empty)
        - at dataset level if this a transaction concept ($trid is not empty)
    - normal (default): retrieve any terminologyAssociations and identifierAssociations for this concept at the appropriate level
        - in transaction ($trid is not empty)
        - at dataset level ($trid is empty)
:)
declare function art:getConceptAssociations ($concept as element(concept)?, $mode as xs:string?, $doNameForOID as xs:boolean) as element(associations) {
    let $datasetConcept     :=
        if ($concept[ancestor::dataset]) then $concept else
        if ($concept[@id]) then (art:getConcept($concept/@id, $concept/@effectiveDate)) else
        if ($concept[@ref]) then (art:getConcept($concept/@ref, $concept/@flexibility)) else ()
    return
    art:getConceptAssociations($concept, art:getOriginalForConcept($datasetConcept), $mode, $doNameForOID)
};
declare function art:getConceptAssociations ($concept as element(concept)?, $originalConcept as element(concept)?, $mode as xs:string?, $doNameForOID as xs:boolean) as element(associations) {

let $transactionConcept             := $concept[ancestor::transaction]
let $datasetConcept                 := 
    if (empty($concept)) then () else 
    if ($transactionConcept) then 
        art:getConcept($transactionConcept/@ref, $transactionConcept/@flexibility[. castable as xs:dateTime], (), ()) 
    else $concept

let $conceptId                      := $datasetConcept/@id
let $conceptEffectiveDate           := $datasetConcept/@effectiveDate
let $transactionId                  := $transactionConcept/ancestor::transaction[1]/@id
let $transactionEffectiveDate       := $transactionConcept/ancestor::transaction[1]/@effectiveDate

let $decor                          := $concept/ancestor::decor
let $projectLanguage                := $decor/project/@defaultLanguage
let $language                       := if (request:exists()) then request:get-parameter('language', $projectLanguage) else ($projectLanguage)

let $datasetConceptAssociations     := art:getConceptAssociations($datasetConcept, $originalConcept)

let $transactionConceptAssocations  := 
    if (empty($concept))        then () else
    if ($transactionConcept)    then (
        (: this is a transaction concept :)
        switch ($mode)
        case 'all' return (
            for $tc in art:getTransactionConcept($conceptId, $conceptEffectiveDate, (), (), (), ())
            return
                art:getConceptAssociations($tc, $originalConcept)
        )
        default return (
            art:getConceptAssociations($concept, $originalConcept)
        )
    ) else (
        (: this is a dataset concept :)
        switch ($mode)
        case 'diff' return (
            for $tc in art:getTransactionConcept($conceptId, $conceptEffectiveDate, (), (), (), ())
            return
                art:getConceptAssociations($tc, $originalConcept)
        )
        default return ()
    )


(: logic for transaction concepts: 
    - get associations from transaction concept if they exist else get from dataset concept
    - note that a transaction concept association might be 'empty', i.e. have no code/valueSet/ref. If that's the case then this counts as 'do not use dataset association(s), if any'
        This is relevant e.g. when you have a dataset conceptList with concepts, but in the transaction you do not support some of the concepts. In this case there's no real overriding association possible
:)
let $associations       :=
    switch ($mode)
    case 'all' return (
        $datasetConceptAssociations
        ,
        (: Mark the thing with the transaction id/effectiveDate to smooth downstream processing :)
        for $tca in $transactionConceptAssocations
        return
            element {name($tca)} {
                $tca/(@* except (@transactionId | @transactionEffectiveDate | @transactionName | @transactionVersionLabel | @transactionStatusCode | @transactionExpirationDate)), 
                attribute transactionId {$tca/ancestor::transaction[1]/@id}, 
                attribute transactionEffectiveDate {$tca/ancestor::transaction[1]/@effectiveDate},
                attribute transactionName {if ($tca/ancestor::transaction[1]/name[@language = $language]) then $tca/ancestor::transaction[1]/name[@language = $language] else $tca/ancestor::transaction[1]/name[1]},
                if ($tca/ancestor::transaction[1]/@versionLabel) then attribute transactionVersionLabel {$tca/ancestor::transaction[1]/@versionLabel} else (),
                attribute transactionStatusCode {($tca/ancestor::transaction/@statusCode)[1]},
                if ($tca/ancestor::transaction[1]/@expirationDate) then attribute transactionExpirationDate {$tca/ancestor::transaction[1]/@expirationDate} else ()
            }
    )
    case 'diff' return (
        if ($concept[ancestor::transaction]) then (
            for $dsa in $datasetConceptAssociations[name() = 'terminologyAssociation']
            return $dsa[@conceptId = $transactionConceptAssocations[name() = 'terminologyAssociation']/@conceptId]
            ,
            for $dsa in $datasetConceptAssociations[name() = 'identifierAssociation']
            return $dsa[@conceptId = $transactionConceptAssocations[name() = 'identifierAssociation']/@conceptId]
        )
        else (
            (: Mark the thing with the transaction id/effectiveDate to smooth downstream processing :)
            for $tca in $transactionConceptAssocations
            return
                element {name($tca)} {
                    $tca/(@* except (@transactionId | @transactionEffectiveDate | @transactionName | @transactionVersionLabel | @transactionStatusCode | @transactionExpirationDate)),
                    attribute transactionId {$tca/ancestor::transaction[1]/@id}, 
                    attribute transactionEffectiveDate {$tca/ancestor::transaction[1]/@effectiveDate},
                    attribute transactionName {if ($tca/ancestor::transaction[1]/name[@language = $language]) then $tca/ancestor::transaction[1]/name[@language = $language] else $tca/ancestor::transaction[1]/name[1]},
                    if ($tca/ancestor::transaction[1]/@versionLabel) then attribute transactionVersionLabel {$tca/ancestor::transaction[1]/@versionLabel} else (),
                    attribute transactionStatusCode {($tca/ancestor::transaction/@statusCode)[1]},
                    if ($tca/ancestor::transaction[1]/@expirationDate) then attribute transactionExpirationDate {$tca/ancestor::transaction[1]/@expirationDate} else ()
                }
        )
    )
    default return (
        if ($concept[ancestor::transaction]) then
            for $conceptId in $transactionConceptAssocations/@conceptId | $datasetConceptAssociations/@conceptId
            let $id := $conceptId
            group by $id
            return (
                (: Mark the thing with the transaction id/effectiveDate to smooth downstream processing :)
                if ($transactionConceptAssocations[name() = 'terminologyAssociation'][@conceptId = $conceptId[1]]) then (
                    for $tca in $transactionConceptAssocations[name() = 'terminologyAssociation'][@conceptId = $conceptId[1]]
                    return
                        element {name($tca)} {
                            $tca/(@* except (@transactionId | @transactionEffectiveDate | @transactionName | @transactionVersionLabel | @transactionStatusCode | @transactionExpirationDate)),
                            attribute transactionId {$tca/ancestor::transaction[1]/@id}, 
                            attribute transactionEffectiveDate {$tca/ancestor::transaction[1]/@effectiveDate},
                            attribute transactionName {if ($tca/ancestor::transaction[1]/name[@language = $language]) then $tca/ancestor::transaction[1]/name[@language = $language] else $tca/ancestor::transaction[1]/name[1]},
                            if ($tca/ancestor::transaction[1]/@versionLabel) then attribute transactionVersionLabel {$tca/ancestor::transaction[1]/@versionLabel} else (),
                            attribute transactionStatusCode {($tca/ancestor::transaction/@statusCode)[1]},
                            if ($tca/ancestor::transaction[1]/@expirationDate) then attribute transactionExpirationDate {$tca/ancestor::transaction[1]/@expirationDate} else ()
                        }
                )
                else ($datasetConceptAssociations[name() = 'terminologyAssociation'][@conceptId = $conceptId[1]])
                ,
                if ($transactionConceptAssocations[name() = 'identifierAssociation'][@conceptId = $conceptId[1]]) then (
                    for $tca in $transactionConceptAssocations[name() = 'identifierAssociation'][@conceptId = $conceptId[1]]
                    return
                        element {name($tca)} {
                            $tca/(@* except (@transactionId | @transactionEffectiveDate | @transactionName | @transactionVersionLabel | @transactionStatusCode | @transactionExpirationDate)),
                            attribute transactionId {$tca/ancestor::transaction[1]/@id}, 
                            attribute transactionEffectiveDate {$tca/ancestor::transaction[1]/@effectiveDate},
                            attribute transactionName {if ($tca/ancestor::transaction[1]/name[@language = $language]) then $tca/ancestor::transaction[1]/name[@language = $language] else $tca/ancestor::transaction[1]/name[1]},
                            if ($tca/ancestor::transaction[1]/@versionLabel) then attribute transactionVersionLabel {$tca/ancestor::transaction[1]/@versionLabel} else (),
                            attribute transactionStatusCode {($tca/ancestor::transaction/@statusCode)[1]},
                            if ($tca/ancestor::transaction[1]/@expirationDate) then attribute transactionExpirationDate {$tca/ancestor::transaction[1]/@expirationDate} else ()
                        }
                )
                else ($datasetConceptAssociations[name() = 'identifierAssociation'][@conceptId = $conceptId[1]])
            )
        else (
            $datasetConceptAssociations
        )
    )
(: build map of oid names. is more costly when you do that deep down :)
let $oidnamemap             :=
    if ($doNameForOID) then 
        map:merge(
            for $oid in distinct-values($associations/@codeSystem | $associations/@ref)
            let $oidName    := art:getNameForOID($oid, $language, $decor)
            return
                if (string-length($oidName) = 0) then () else map:entry($oid, $oidName)
        )
    else ()
return
<associations mode="{$mode}">
{
    for $association in $associations
    let $vsref          := $association/@valueSet
    let $vsflex         := $association/@flexibility
    let $codeSystemName := 
        if (empty($oidnamemap)) then () else 
        if ($association/@codeSystem[not(. ='')]) then map:get($oidnamemap, $association/@codeSystem) else ()
      
    (:let $codeSystemName := $association/@codeSystemName:)
    let $valueSetName   :=
        if ($doNameForOID and string-length($vsref) gt 0) then (
            if ($vsflex[. castable as xs:dateTime]) then (
                let $vs     :=  $decor/terminology/valueSet[@ref=$vsref] | 
                                $decor/terminology/valueSet[@ref][@name=$vsref] | 
                                $decor/terminology/valueSet[@id=$vsref] | 
                                $decor/terminology/valueSet[@id][@name=$vsref]
                let $vs     := ($vs[@effectiveDate=$vsflex], $vs[empty(@effectiveDate)])
                return
                    ($vs/@displayName, $vs/@name)[1]
            )
            else (
                let $vs     := ($decor/terminology/valueSet[@ref=$vsref] | 
                                $decor/terminology/valueSet[@ref][@name=$vsref] | 
                                $decor/terminology/valueSet[@id=$vsref] |
                                $decor/terminology/valueSet[@id][@name=$vsref])
                let $max    := max($vs/xs:dateTime(@effectiveDate))
                let $vs     := ($vs[@effectiveDate=$max], $vs[empty(@effectiveDate)])
                return
                    ($vs/@displayName, $vs/@name)[1]
            )
        )
        else ()
    let $identifierName :=
        if (empty($oidnamemap)) then () else 
        if ($association/@ref[not(. ='')]) then map:get($oidnamemap, $association/@ref) else ()
        
    order by $association/@codeSystem, $association/@expirationDate descending
    return
    element {name($association)}
    {
        $association/(@*[not(.='')] except (@codeSystemName|@valueSetName|@refdisplay)),
        if (empty($codeSystemName))    then () else attribute codeSystemName {$codeSystemName},
        if (empty($valueSetName))      then () else attribute valueSetName {$valueSetName},
        if ($association/@strength)    then () else if ($association/@valueSet) then attribute strength {''} else (),
        if ($association/@equivalence) then () else if ($association/@valueSet) then () else attribute equivalence {''},
        if (empty($identifierName))    then () else attribute refdisplay {$identifierName}
    }
}
</associations>
};

(:~ FIXME: does not work for valueSet/@ref. Need rewrite to (taken from RetrieveValueSet)
    let $valueSetList := vs:getValueSetList((),(),(),$projectPrefix)
    
    for $valueSet in $valueSetList/repository[empty(@url)][@ident=$projectPrefix]/valueSet
    return
        vs:getExpandedValueSetById($valueSet/(@id|@ref),$valueSet/@effectiveDate,$projectPrefix)
:)
declare function art:currentValuesets($decor as node()) as element()*{
    (: returns a sequence with the current valuesets for a particular decor node :)
    for $name in distinct-values($decor//valueSet/@name)
    return $decor//valueSet[@name=$name][@effectiveDate=max($decor//valueSet[@name=$name]/xs:dateTime(@effectiveDate))]
};

(:~ Remove superfluous elements and attributes and parse textWithMarkup nodes :)
declare function art:cleanConceptItem($concept as element()) as element() {
    let $id := $concept/@id
    return
    <concept id="{$id}" statusCode="{$concept/@statusCode}" effectiveDate="{$concept/@effectiveDate}">
    {
        $concept/(@type|@versionLabel|@expirationDate|@officialReleaseDate|@canonicalUri|@lastModifiedDate)[string-length()>0]
        ,
        $concept/inherit
        ,
        $concept/contains
        ,
        for $name in $concept/name
        return
        art:parseNode($name)
        ,
        for $synonym in $concept/synonym
        return
        art:parseNode($synonym)
        ,
        for $desc in $concept/desc
        return
        art:parseNode($desc)
        ,
        for $source in $concept/source
        return
        art:parseNode($source)
        ,
        for $rationale in $concept/rationale
        return
        art:parseNode($rationale)
        ,
        for $comment in $concept/comment
        return
        art:parseNode($comment)
        ,
        (:new since 2015-04-21:)
        for $property in $concept/property
        return
        art:parseNode($property)
        ,
        (:new since 2015-04-21:)
        $concept/relationship
        ,
        for $operationalization in $concept/operationalization
        return
        art:parseNode($operationalization)
        ,
        for $valueDomain in $concept/valueDomain
        return
        <valueDomain type="{$valueDomain/@type}">
        {
            if ($valueDomain/@type=('code','ordinal')) then
                for $conceptList in $valueDomain/conceptList
                return
                <conceptList>
                {
                    $conceptList/(@id|@ref),
                    for $conceptListConcept in $conceptList/concept
                    return
                    <concept>
                    {
                        $conceptListConcept/@*,
                        $conceptListConcept/name,
                        $conceptListConcept/synonym,
                        for $desc in $conceptListConcept/desc
                        return art:parseNode($desc)
                    }
                    </concept>
                }
                </conceptList>
            else (),
            for $property in $valueDomain/property[@*[string-length(.) gt 0]]
            return
            <property>
            {
                if ($valueDomain/@type=('count')) then (
                    $property/(@minInclude|@maxInclude|@default|@fixed)[string-length() gt 0]
                )
                else if ($valueDomain/@type=('decimal')) then (
                    $property/(@minInclude|@maxInclude|@fractionDigits|@default|@fixed)[string-length() gt 0]
                )
                else if ($valueDomain/@type=('duration','quantity')) then (
                    $property/(@unit|@minInclude|@maxInclude|@fractionDigits|@default|@fixed)[string-length() gt 0]
                )
                else if ($valueDomain/@type=('date','datetime')) then (
                    $property/(@timeStampPrecision|@default|@fixed)[string-length() gt 0]
                )
                else if ($valueDomain/@type=('code','score','ordinal','boolean')) then (
                    $property/(@default|@fixed)[string-length() gt 0]
                )
                else if ($valueDomain/@type=('string','text','identifier')) then (
                    $property/(@minLength|@maxLength|@default|@fixed)[string-length() gt 0]
                ) 
                else()
            }
            </property>
            ,
            for $example in $valueDomain/example[string-length(.) gt 0]
            return
               <example>
               {
                   $example/@*[string-length()>0],
                   $example/node()
               }
               </example>
        }
        </valueDomain>
    }
    </concept>
};

(: Updated so it doesn't list every attribute and element individually. The purpose is to remove history, 
   not to do a cleanup. Also it saves a maintenance hassle when new concept elements/attributes come up
   
   @author Alexander Henket
   @since 2015-04-21
:)
declare function art:removeConceptItemHistory($concept as element()) as element() {
    <concept>
    {
        $concept/@*[not(.='')],
        for $node in $concept/(* except (concept|history|edit|move|lock))
        return
            if ($node[self::inherit]) then (
                <inherit>{$node/@ref, $node/@effectiveDate}</inherit>
            ) else
            if ($node[self::contains]) then (
                <contains>{$node/@ref, $node/@flexibility}</contains>
            ) else
            if ($node[self::relationship]) then (
                <relationship>{$node/@type, $node/@ref, $node/@flexibility}</relationship>
            ) else (
                $node
            )
        ,
        (:since this is an item, there should be no child concept, but better safe then sorry:)
        for $child in $concept/concept[not(@statusCode = 'new')]
        return <concept>{$child/@*[not(.='')]}</concept>
    }
    </concept>
};

(:~ used by save-decor-dataset.xquery :)
declare function art:prepareItemForUpdate($concept as element(),$storedItem as element()?) as element() {
let $status     := if ($concept[@statusCode=('new','')] | $concept[empty(@statusCode)]) then 'draft' else ($concept/@statusCode)

return
    <concept id="{$concept/@id}" effectiveDate="{$concept/@effectiveDate}" statusCode="{$status}">
    {
        if ($concept[inherit | contains]) then () else (
            attribute type {'item'}
            (:$concept/@type:)
        )
        ,
        $concept/@versionLabel[string-length() gt 0],
        $concept/@expirationDate[string-length() gt 0],
        $concept/@officialReleaseDate[string-length() gt 0],
        $concept/@canonicalUri[string-length() gt 0],
        attribute lastModifiedDate {substring(string(current-dateTime()), 1, 19)}
        ,
        if ($concept[inherit]) then (
            <inherit>{$concept/inherit/@ref, $concept/inherit/@effectiveDate}</inherit>
            ,
            for $comment in $concept/comment[string-length(.) gt 0]
            return
            art:parseNode($comment)
        )
        else
        if ($concept[contains]) then (
            <contains>{$concept/contains/@ref, $concept/contains/@flexibility}</contains>
            ,
            for $comment in $concept/comment[string-length(.) gt 0]
            return
            art:parseNode($comment)
        )
        else (
            for $name in $concept/name[position() = 1 or string-length(.) gt 0]
            let $l  := $name/@language
            return
            art:parseNode($name[1])
            ,
            for $synonym in $concept/synonym[string-length(.) gt 0]
            return
            art:parseNode($synonym)
            ,
            for $desc in $concept/desc[position() = 1 or string-length(.) gt 0]
            let $l  := $desc/@language
            return
            art:parseNode($desc[1])
            ,
            for $source in $concept/source[string-length(.) gt 0]
            let $l  := $source/@language
            return
            art:parseNode($source[1])
            ,
            for $rationale in $concept/rationale[string-length(.) gt 0]
            let $l  := $rationale/@language
            return
            art:parseNode($rationale[1])
            ,
            for $comment in $concept/comment[string-length(.) gt 0]
            return
            art:parseNode($comment)
            ,
            (:new since 2015-04-21:)
            for $property in $concept/property[string-length(@name) gt 0]
            return
            art:parseNode($property)
            ,
            (:new since 2015-04-21:)
            for $relationship in $concept/relationship[string-length(@ref) gt 0]
            return
            <relationship>{$relationship/@type[not(. = '')], $relationship/@ref[not(.='')], $relationship/@flexibility[not(.='')]}</relationship>
            ,
            for $operationalization in $concept/operationalization[string-length(.) gt 0]
            let $l  := $operationalization/@language
            return
            art:parseNode($operationalization[1])
            ,
            for $valueDomain in $concept/valueDomain
            return
            <valueDomain type="{$valueDomain/@type}">
            {
                if ($valueDomain[@type=('code','ordinal')]) then
                    for $conceptList in $valueDomain/conceptList
                    return
                    <conceptList>
                    {
                        $conceptList/@id | $conceptList/@ref,
                        for $conceptListConcept in $conceptList[@id]/concept
                        return
                        <concept>
                        {
                            $conceptListConcept/@id,
                            $conceptListConcept/@level[string-length() gt 0],
                            $conceptListConcept/@type[string-length() gt 0],
                            $conceptListConcept/@exception[.='true'],
                            if ($valueDomain[@type = 'ordinal']) then
                                $conceptListConcept/@ordinal[. castable as xs:decimal]
                            else (),
                            for $node in $conceptListConcept/name[position() = 1 or string-length(.) gt 0]
                            let $l  := $node/@language
                            return
                            art:parseNode($node[1])
                            ,
                            for $node in $conceptListConcept/synonym[string-length(.) gt 0]
                            return art:parseNode($node),
                            for $node in $conceptListConcept/desc[string-length(.) gt 0]
                            let $l  := $node/@language
                            return
                            art:parseNode($node[1])
                        }
                        </concept>
                    }
                    </conceptList>
                else ()
                ,
                for $property in $valueDomain/property
                let $prop   :=
                    <property>
                    {
                        if ($valueDomain/@type=('count')) then (
                            $property/(@minInclude|@maxInclude|@default|@fixed)[string-length() gt 0]
                        )
                        else if ($valueDomain/@type=('decimal')) then (
                            $property/(@minInclude|@maxInclude|@fractionDigits|@default|@fixed)[string-length() gt 0]
                        )
                        else if ($valueDomain/@type=('duration','quantity')) then (
                            $property/(@unit|@minInclude|@maxInclude|@fractionDigits|@default|@fixed)[string-length() gt 0]
                        )
                        else if ($valueDomain/@type=('date','datetime')) then (
                            $property/(@timeStampPrecision|@default|@fixed)[string-length() gt 0]
                        )
                        else if ($valueDomain/@type=('code','score','ordinal','boolean')) then (
                            $property/(@default|@fixed)[string-length()>0]
                        )
                        else if ($valueDomain/@type=('string','text','identifier')) then (
                            $property/(@minLength|@maxLength|@default|@fixed)[string-length() gt 0]
                        ) 
                        else ()
                    }
                    </property>
                return
                    $prop[@*]
                ,
                for $example in $valueDomain/example[string-length(.) gt 0]
                return
                   <example>
                   {
                       $example/@*[string-length()>0],
                       $example/node()
                   }
                   </example>
            }
            </valueDomain>
        )
        (:,
        if ($concept[not(@statusCode='new')][edit/@mode='edit'] and $storedItem[not(@statusCode='new')]) then
            <history validTimeHigh="{format-dateTime(current-dateTime(), '[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01]')}">
            {
                art:removeConceptItemHistory($storedItem)
            }
            </history>
        else()
        ,
        $storedItem/history:)
   }
   </concept>
};

(:~ used by save-decor-dataset.xquery :)
declare function art:prepareGroupForUpdate($concept as element(),$storedGroup as element()?) as element() {
let $status     := if ($concept[@statusCode=('new','')] | $concept[empty(@statusCode)]) then 'draft' else ($concept/@statusCode)

return
    <concept id="{$concept/@id}" effectiveDate="{$concept/@effectiveDate}" statusCode="{$status}">
    {
        if ($concept[inherit | contains]) then () else (
            attribute type {'group'}
            (:$concept/@type:)
        )
        ,
        $concept/@versionLabel[string-length() gt 0],
        $concept/@expirationDate[string-length() gt 0],
        $concept/@officialReleaseDate[string-length() gt 0],
        $concept/@canonicalUri[string-length() gt 0],
        attribute lastModifiedDate {substring(string(current-dateTime()), 1, 19)}
        ,
        if ($concept[inherit]) then (
            <inherit>{$concept/inherit/@ref, $concept/inherit/@effectiveDate}</inherit>
            ,
            for $comment in $concept/comment[string-length(.) gt 0]
            return
            art:parseNode($comment)
        )
        else
        if ($concept[contains]) then (
            <contains>{$concept/contains/@ref, $concept/contains/@flexibility}</contains>
            ,
            for $comment in $concept/comment[string-length(.) gt 0]
            return
            art:parseNode($comment)
        )
        else (
            for $name in $concept/name[position() = 1 or string-length(.) gt 0]
            let $l  := $name/@language
            return
            art:parseNode($name[1])
            ,
            for $synonym in $concept/synonym[string-length(.) gt 0]
            return
            art:parseNode($synonym)
            ,
            for $desc in $concept/desc[position() = 1 or string-length(.) gt 0]
            let $l  := $desc/@language
            return
            art:parseNode($desc[1])
            ,
            for $source in $concept/source[string-length(.) gt 0]
            let $l  := $source/@language
            return
            art:parseNode($source[1])
            ,
            for $rationale in $concept/rationale[string-length(.) gt 0]
            let $l  := $rationale/@language
            return
            art:parseNode($rationale[1])
            ,
            for $comment in $concept/comment[string-length(.) gt 0]
            return
            art:parseNode($comment)
            ,
            (:new since 2015-04-21:)
            for $property in $concept/property[string-length(@name) gt 0]
            return
            art:parseNode($property)
            ,
            (:new since 2015-04-21:)
            for $relationship in $concept/relationship[string-length(@ref) gt 0]
            return
            <relationship>{$relationship/@type[not(. = '')], $relationship/@ref[not(.='')], $relationship/@flexibility[not(.='')]}</relationship>
            ,
            for $operationalization in $concept/operationalization[string-length(.) gt 0]
            let $l  := $operationalization/@language
            return
            art:parseNode($operationalization[1])
        )
    }
    {
        if ($concept[contains]) then () else
        if ($concept[inherit][concept]) then (
            (: when you select edit for a group, only the group level comes to the client. If you hit save 
            (e.g. after adding a comment somewhere) you would delete the stored children. So we check if the thing
            coming in has children of its own. When it does, it must have been for a new inherit in which case we 
            purposely want to overwrite the stored version of the group :)
            for $child in $concept/concept
            return 
                if ($child[concept]) 
                then art:prepareGroupForUpdate($child, art:getConcept($child/@id, $child/@effectiveDate)) 
                else art:prepareItemForUpdate($child, art:getConcept($child/@id, $child/@effectiveDate))
        ) 
        else (
            (: just get whatever was stored before under this group :)
            $storedGroup/concept
            (:for $child in $storedGroup/concept
            return 
                if ($child[concept]) 
                then art:prepareGroupForUpdate($child, art:getConcept($child/@id, $child/@effectiveDate)) 
                else art:prepareItemForUpdate($child, art:getConcept($child/@id, $child/@effectiveDate)):)
        )
        (:,
        if ($concept[not(@statusCode='new')][edit/@mode='edit'] and $storedGroup[not(@statusCode='new')]) then
            <history validTimeHigh="{format-dateTime(current-dateTime(), '[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01]')}">
            {
                art:removeGroupContent($storedGroup)
            }
            </history>
        else ()
        ,
        $storedGroup/history:)
    }
    </concept>
};

(:
    2015-04-21 AH. Updated so it doesn't list every attribute and element individually. The purpose is to remove history, 
                   not to do a cleanup. Also it saves a maintenance hassle when new concept elements/attributes come up
    2015-09-29 AH. Updated it does some basic cleanup that would otherwise lead to schema problems. Skip edit|move|lock 
                   and skip extra attributes in inherit and relationship 
:)
declare function art:removeGroupContent($concept as element()) as element() {
    <concept>
    {
        $concept/@*[not(.='')],
        for $node in $concept/(* except (concept|history|edit|move|lock))
        return
            if ($node[self::inherit]) then (
                <inherit>{$node/@ref, $node/@effectiveDate}</inherit>
            ) else if ($node[self::contains]) then (
                <contains>{$node/@ref, $node/@flexibility}</contains>
            ) else if ($node[self::relationship]) then (
                <relationship>{$node/@type, $node/@ref, $node/@flexibility}</relationship>
            ) else (
                $node
            )
        ,
        for $child in $concept/concept[not(@statusCode = 'new')]
        return <concept>{$child/@*[not(.='')]}</concept>
    }
    </concept>
};

(:~ Xquery function for retrieving the concept tree of a dataset for navigation purposes.
   Requires either:
   - id of a dataset.
   - transactionId of a transaction containing a representingTemplate
   Returns the concept hierarchy for the dataset, 
   inherits are resolved to retrieve the name of the concept.
   Providing a transactionId will return the cardinalities and conformance of the concept
   and an attribute absent='true' on concepts not in the representingTemplate
:)
declare function art:getDatasetTree($datasetId as xs:string?, $transactionId as xs:string?) as element(dataset)? { 
    art:getDatasetTree($datasetId, (), $transactionId, (), true())
};
declare function art:getDatasetTree($datasetId as xs:string?, $datasetEffectiveDate as xs:string?, $transactionId as xs:string?, $transactionEffectiveDate as xs:string?) as element(dataset)? {
    art:getDatasetTree($datasetId, $datasetEffectiveDate, $transactionId, $transactionEffectiveDate, true())
};
(:~  In a number of cases like RetrieveTransaction and template mapping we do not care about absent concepts to just omit those when $fullTree=false()
    Note: only active concepts are traversed. A concept with an absent parent is not retrieved. This situation should never occur.
:)
declare function art:getDatasetTree($datasetId as xs:string?, $datasetEffectiveDate as xs:string?, $transactionId as xs:string?, $transactionEffectiveDate as xs:string?, $fullTree as xs:boolean) as element(dataset)? {
    let $representingTemplate   :=
        if (string-length($transactionId)=0) then () else (
            art:getTransaction($transactionId, $transactionEffectiveDate)/representingTemplate
        )
    
    let $dataset                := 
        if ($representingTemplate) then
            art:getDataset($representingTemplate/@sourceDataset, $representingTemplate/@sourceDatasetFlexibility)
        else
        if ($datasetId) then
            art:getDataset($datasetId, $datasetEffectiveDate)
        else ()
    
    let $projectPrefix          := $dataset/ancestor::decor/project/@prefix
    let $defaultLanguage        := $dataset/ancestor::decor/project/@defaultLanguage
    let $projectLanguages       := $dataset/ancestor::decor/project/name/@language
    
    return
    if ($dataset) then 
        <dataset>
        {
            $dataset/@id | $dataset/@effectiveDate | $dataset/@statusCode | $dataset/@versionLabel | 
            $dataset/@expirationDate | $dataset/@officialReleaseDate | $dataset/@canonicalUri | $dataset/@lastModifiedDate,
            attribute iddisplay {art:getNameForOID($dataset/@id, (), $dataset/ancestor::decor)}
            ,
            if ($representingTemplate) then (
                attribute transactionId {$representingTemplate/parent::transaction/@id},
                attribute transactionEffectiveDate {$representingTemplate/parent::transaction/@effectiveDate},
                attribute transactionStatusCode {$representingTemplate/parent::transaction/@statusCode},
                if ($representingTemplate/parent::transaction/@expirationDate) then
                    attribute transactionExpirationDate {$representingTemplate/parent::transaction/@expirationDate}
                else (),
                if ($representingTemplate/parent::transaction/@versionLabel) then
                    attribute transactionVersionLabel {$representingTemplate/parent::transaction/@versionLabel}
                else (),
                if ($representingTemplate[@ref]) then (
                    attribute templateId {$representingTemplate/@ref},
                    if ($representingTemplate[@flexibility]) then
                        attribute templateEffectiveDate {$representingTemplate/@flexibility}
                    else ()
                ) else ()
                ,
                if ($representingTemplate[@representingQuestionnaire]) then (
                    attribute questionnaireId {$representingTemplate/@representingQuestionnaire},
                    if ($representingTemplate[@representingQuestionnaireFlexibility]) then
                        attribute questionnaireEffectiveDate {$representingTemplate/@representingQuestionnaireFlexibility}
                    else ()
                ) else ()
            ) else ()
        }
        {
            $dataset/name
            ,
            for $lang in $projectLanguages
            return
                if ($dataset/name[@language = $lang]) then () else (<name language="{$lang}"/>)
            ,
            for $desc in $dataset/desc
            return
                art:serializeNode($desc)
            ,
            for $lang in $projectLanguages
            return
                if ($dataset/desc[@language = $lang]) then () else (<desc language="{$lang}"/>)
            ,
            (: new ... 2021-05-21 :)
            for $node in $dataset/publishingAuthority
            return
                <publishingAuthority>
                {
                    $node/@id[not(. = '')],
                    $node/@name[not(. = '')],
                    for $addr in $node/addrLine
                    return
                        art:serializeNode($addr)
                }
                </publishingAuthority>
            ,
            (:new since 2015-04-21, updated 2021-05-21 :)
            if ($dataset/property) then
                for $node in $dataset/property
                return
                    art:serializeNode($node)
            else (
                <property name="" datatype="text"/>
            )
            ,
            (: new ... 2021-05-21 :)
            for $node in $dataset/copyright
            return
                art:serializeNode($node)
            ,
            (:new since 2015-04-21:)
            if ($dataset/relationship) then
                for $relationship in $dataset/relationship
                let $referredDataset    :=  
                    if (string-length($relationship/@ref)=0) then () else (
                        art:getDataset($relationship/@ref,$relationship/@flexibility)
                    )
                return
                    <relationship type="{$relationship/@type}" ref="{$relationship/@ref}" flexibility="{$relationship/@flexibility}">
                    {
                        if ($referredDataset) then (
                            $referredDataset/ancestor::decor/project/@prefix,
                            attribute iStatusCode {$referredDataset/@statusCode}, 
                            attribute iExpirationDate {$referredDataset/@expirationDate},
                            attribute iVersionLabel {$referredDataset/@versionLabel},
                            attribute iddisplay {art:getNameForOID($relationship/@ref, $defaultLanguage, $referredDataset/ancestor::decor)},
                            attribute localInherit {$referredDataset/ancestor::decor/project/@prefix=$projectPrefix},
                            $referredDataset/name,
                            if ($referredDataset/name[@language=$defaultLanguage]) then () else (
                                <name language="{$defaultLanguage}">{data($referredDataset/name[1])}</name>
                            )
                        ) else ()
                    }
                    </relationship>
            else ()
        }
        {
            for $concept in $dataset/concept
            return
                if ($representingTemplate) then
                    art:transactionConceptBasics($concept, $representingTemplate, $fullTree, $defaultLanguage)
                else (art:conceptBasics($concept, false()))
        }
        </dataset>
    else ()
};
declare function art:getConceptTree($conceptId as xs:string?, $conceptEffectiveDate as xs:string?, $transactionId as xs:string?, $transactionEffectiveDate as xs:string?, $fullTree as xs:boolean) as element(dataset)? {
    let $concept                := (art:getConcept($conceptId, $conceptEffectiveDate))[1]
    
    let $representingTemplate   :=
        if (string-length($transactionId)=0) then () else (
            art:getTransaction($transactionId, $transactionEffectiveDate)/representingTemplate
        )
    
    (: if our concept is child of a parent concept that inherits: is our concept represented in the original (missing-in-source = false) or has it been added (missing-in-source = true)? :)
    let $missing-in-source  := 
        if ($concept/parent::concept[inherit]) then (
            let $originalConcept    := art:getOriginalForConcept($concept/parent::concept)
            return
                if ($originalConcept[
                    concept[@id = $concept/inherit/@ref][@effectiveDate = $concept/inherit/@effectiveDate] |
                    concept[concat(@id, @effectiveDate) = $concept/relationship/concat(@ref, @flexibility)]
                ]) then false() else true()
        )
        else false()
    
    let $projectPrefix          := $concept/ancestor::decor/project/@prefix
    let $defaultLanguage        := $concept/ancestor::decor/project/@defaultLanguage
    let $projectLanguages       := $concept/ancestor::decor/project/name/@language
    
    return
    if ($concept) then 
        if ($representingTemplate) then
            art:transactionConceptBasics($concept, $representingTemplate, $fullTree, $defaultLanguage)
        else (art:conceptBasics($concept, false()))
    else ()
};

(:~ Function for retrieving the basic scenario info for a scenario hierarchy, optionally filtered to include only scenarios/transations connected to a particular dataset
   Used for creating scenario navigation.
:)
declare function art:scenarioTree($scenario as element(scenario), $language as xs:string?) as element(scenario) {
    art:scenarioTree($scenario, $language, (), ())
};
declare function art:scenarioTree($scenario as element(scenario), $language as xs:string?, $datasetId as xs:string?, $datasetEffectiveDate as xs:string?) as element(scenario) {
    <scenario>
    {
        $scenario/@id | 
        $scenario/@effectiveDate | 
        $scenario/@statusCode | 
        $scenario/@versionLabel | 
        $scenario/@expirationDate | 
        $scenario/@officialReleaseDate |
        $scenario/@canonicalUri |
        $scenario/@lastModifiedDate
    }
    {
        attribute iddisplay {art:getNameForOID($scenario/@id, $language, $scenario/ancestor::decor)},
        if (empty($language)) then () else (
            if ($scenario/name[@language=$language]) then () else (
                let $n := if ($scenario/name[@language='en-US']) then $scenario/name[@language='en-US'] else ($scenario/name[1])
                return
                art:serializeNode(<name language="{$language}">{$n/node()}</name>)
            )
        )
        ,
        for $name in $scenario/name
        return
            art:serializeNode($name)
        ,
        let $filteredTransactions :=
            if (empty($datasetId))
            then $scenario/transaction
            else art:getTransactionsByScenarioAndDataset($scenario/@id, $scenario/@effectiveDate, $datasetId, $datasetEffectiveDate)/ancestor-or-self::transaction[parent::scenario]
        for $transaction in $filteredTransactions
        return
            art:transactionBasics($transaction, $language)
    }
    </scenario>
};

(:~ Recursive function for retrieving the basic transaction (group) info for a transaction hierarchy.
   Used for creating transaction navigation.
:)
declare function art:transactionBasics($transaction as element(transaction), $language as xs:string?) as element(transaction) {
    let $projectPrefix  := $transaction/ancestor::decor/project/@prefix
    return
    <transaction>
    {
        $transaction/@id | 
        $transaction/@effectiveDate | 
        $transaction/@statusCode | 
        $transaction/@versionLabel | 
        $transaction/@expirationDate | 
        $transaction/@officialReleaseDate | 
        $transaction/@type | 
        $transaction/@label | 
        $transaction/@model |
        $transaction/@canonicalUri |
        $transaction/@lastModifiedDate
    }
    {
        attribute iddisplay {art:getNameForOID($transaction/@id, $language, $transaction/ancestor::decor)},
        (:these have not always been there. copy from ancestor scenario if missing:)
        if ($transaction[@statusCode]) then () else ($transaction/ancestor::scenario/@statusCode),
        if ($transaction[@effectiveDate]) then () else ($transaction/ancestor::scenario/@effectiveDate)
        ,
        if (empty($language)) then () else (
            if ($transaction/name[@language=$language]) then () else (
                let $n := if ($transaction/name[@language='en-US']) then $transaction/name[@language='en-US'] else ($transaction/name[1])
                return
                art:serializeNode(<name language="{$language}">{$n/node()}</name>)
            )
        )
        ,
        for $name in $transaction/name
        return
            art:serializeNode($name)
        ,
        if ($transaction[@type='group']) then (
            for $t in $transaction/transaction
            return
            art:transactionBasics($t, $language)
        ) else (
            $transaction/actors
            ,
            for $representingTemplate in $transaction/representingTemplate
            return
                <representingTemplate>
                {
                    $representingTemplate/@ref | 
                    $representingTemplate/@flexibility | 
                    $representingTemplate/@sourceDataset | 
                    $representingTemplate/@sourceDatasetFlexibility |
                    $representingTemplate/@representingQuestionnaire | 
                    $representingTemplate/@representingQuestionnaireFlexibility
                }
                </representingTemplate>
        )
    }
    </transaction>
};

(:~ Recursive function for retrieving the basic concept info for a concept hierarchy.
   Used for creating dataset navigation.
   
   TODO A concept can be in any of these states:
    1. concept[@added]              -- a concept that was willfully added under an inherited group, 
                                        concept actually lives in the dataset
    2. concept[@absent]             -- a concept that was willfully omitted under an inherited group, 
                                        concept actually lives in the dataset and could be activated by deleting @absent at any time
    3. concept[@missing-in-source]  -- a concept exists in the current dataset, but not in the original concept group that the parent of the current concept inherits from, 
                                        concept does not live in the current dataset, but is created runtime to flag this exceptional state. 
                                        There are two possible reasons for this situation, both can only occur post inheriting:
                                        - Someone hand-edited the origin in the DECOR project file (you cannot do this using ART)
                                        - Someone inherited from a concept that inherits and this concept was updated to inherit from a different concept group
    
    A concept could have both @absent and @missing-in-source.
    
    4. concept[@missing-in-target]  -- a concept exists in the original concept group that the parent of the current concept inherits from, but not in the current dataset, 
                                        concept does not live in the current dataset, but is created runtime to flag this exceptional state. 
                                        There are two possible reasons for this situation, both can only occur post inheriting:
                                        - Someone edited the origin in the DECOR project file and added the concept (group)
                                        - Someone inherited from a concept that inherits and this concept was updated to inherit from a different concept group
    
    5. If the concept does not have any of the markers above, it should be considered a normal concept (group)
:)
declare function art:conceptBasics($concept as element(), $missing-in-source as xs:boolean?) as element(concept) {
    (:let $inheritConcept     := if ($concept[inherit]) then art:getConcept($concept/inherit/@ref, $concept/inherit/@effectiveDate) else ():)
    (:let $originalConcept    := if ($concept[name]) then $concept else if ($inheritConcept[name]) then $inheritConcept else art:getOriginalForConcept($concept):)
    let $originalConcept    := if ($concept[name]) then $concept else art:getOriginalForConcept($concept)
    
    return
        <concept>
        {
            (:useful for targeted nagivation in the tree. alternative is keeping @id/@effectiveDate together all the time:)
            attribute navkey {util:uuid()},
            $originalConcept/@type,
            $concept/(@* except (@type | @navkey))
        }
        {
            $concept/inherit,
            $concept/contains,
            $originalConcept/name,
            if ($concept[contains]) then ( (: don't venture into potential circular references :) ) else (
                for $c in $concept/concept
                return art:conceptBasics($c, false())
            )
        }
        </concept>
};

(:~ Recursive function for retrieving the basic concept info for a concept hierarchy in the context of a representingTemplate.
   Adds @absent='true' to concepts not in representingTemplate.
   Used for creating dataset navigation and  by transaction editor
   In a number of cases like RetrieveTransaction and template mapping we do not care about absent concepts to just omit those when $fullTree=false()
   Note: only active concepts are traversed. A concept with an absent parent is not retrieved. This situation should never occur.
:)
declare function art:transactionConceptBasics($concept as element(concept), $representingTemplate as element(representingTemplate), $fullTree as xs:boolean, $defaultLanguage as xs:string) as element(concept)? {
    let $matchingConcept    := $representingTemplate/concept[@ref=$concept/@id] 
    let $matchingConcept    := if ($matchingConcept[@flexibility]) then $matchingConcept[@flexibility=$concept/@effectiveDate][1] else $matchingConcept[1]
    
    return
        if ($matchingConcept or $fullTree) then (
            let $originalConcept    := if ($concept[name]) then $concept else art:getOriginalForConcept($concept)
            let $isMandatory        := $matchingConcept/@isMandatory='true'
            let $conditions         := 
                for $condition in $matchingConcept/condition
                let $isMandatory    := $condition/@isMandatory='true'
                return
                <condition>
                {
                    attribute minimumMultiplicity {art:getMinimumMultiplicity($condition)},
                    attribute maximumMultiplicity {art:getMaximumMultiplicity($condition)},
                    attribute conformance {if ($isMandatory) then 'M' else ($condition/@conformance)},
                    attribute isMandatory {$isMandatory},
                    $condition/desc,
                    if ($condition[desc]) then () else if ($condition[string-length(.)=0]) then () else (
                        <desc language="{$defaultLanguage}">{$condition/text()}</desc>
                    )
                }
                </condition>
            
            return 
            <concept>
            {
                (:useful for targeted nagivation in the tree. alternative is keeping @id/@effectiveDate together all the time:)
                attribute navkey {util:uuid()}
            }
            {
                $originalConcept/@type,
                $concept/(@* except (@type | @ref | @flexibility | @navkey))
                ,
                if ($matchingConcept) then ((:purposefully do not add anything here!!:)) else (attribute absent {'true'}),
                attribute minimumMultiplicity {if ($matchingConcept) then art:getMinimumMultiplicity($matchingConcept) else ('0')},
                attribute maximumMultiplicity {if ($matchingConcept) then art:getMaximumMultiplicity($matchingConcept) else ('*')},
                attribute conformance {if ($isMandatory) then 'M' else ($matchingConcept/@conformance)},
                attribute isMandatory {$isMandatory},
                $matchingConcept/@enableBehavior,
                $concept/inherit,
                $concept/contains,
                $originalConcept/name,
                for $node in $matchingConcept/context
                return
                    art:serializeNode($node)
                ,
                $conditions,
                $matchingConcept/enableWhen,
                $matchingConcept/terminologyAssociation,
                $matchingConcept/identifierAssociation,
                (: new in AD 3.10 - no real support, just make sure we don't loose it :)
                $matchingConcept/containedTransaction,
                if ($concept[contains]) then ( (: don't venture into potential circular references :) ) else (
                    for $c in $concept/concept
                    return art:transactionConceptBasics($c, $representingTemplate, $fullTree, $defaultLanguage)
                )
            }
            </concept>
        ) else ()
};

(:~  Return live specific or repository/non-private DECOR project OR compiled, archived/released DECOR project based on project/@id and decor/@versionDate.
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $decorOrPrefixOrId - required element(decor) or decor/project/@prefix or decor/project/@id or '*' for any project. If empty returns all non private BBRs
:   @param $decorVersion      - optional decor/@versionDate, Empty uses live projects, '*' selects all versions, dateTime() selects that versionDate, otherwise selects latest version for language
:   @param $language          - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @return all repository/non-private project files, exactly 1 DECOR live project file, or 1 or more archived DECOR version instances or nothing if not found
:   @since 2023-09-25
:)
declare function art:getDecor($decorOrPrefixOrId as item()?, $decorVersion as xs:string?, $language as xs:string?) as element(decor)* {
    typeswitch ($decorOrPrefixOrId)
    case element(decor) return $decorOrPrefixOrId
    case xs:string return
        if (art:isOid($decorOrPrefixOrId)) then
            art:getDecorById($decorOrPrefixOrId, $decorVersion, $language)
        else (
            art:getDecorByPrefix($decorOrPrefixOrId, $decorVersion, $language)
        )
    default return $decorOrPrefixOrId/ancestor-or-self::decor[1]

};
(:~  Return one live specific or all repository/non-private DECOR projects
:
:   @param $projectPrefix   - required decor/project/@prefix
:   @return exactly 1 DECOR project file or nothing if $projectPrefix and nothing found. If $projectPrefix is empty returns all non-private repositories
:   @since 2015-04-29
:)
declare function art:getDecorByPrefix($projectPrefix as item()?) as element(decor)* {
    art:getDecorByPrefix($projectPrefix, (), ())
};
(:~  Return one live specific or repository/non-private DECOR project OR compiled, archived/released DECOR 
:   project based on project/@prefix and decor/@versionDate.
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $projectPrefix   - required decor/project/@prefix or '*' for any project. If empty returns all non private BBRs
:   @param $decorVersion    - optional decor/@versionDate, Empty uses live projects, '*' selects all versions, dateTime() selects that versionDate, otherwise selects latest version for language
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @return all repository/non-private project files, exactly 1 DECOR live project file, or 1 or more archived 
:           DECOR version instances or nothing if not found
:   @since 2015-04-29
:)
declare function art:getDecorByPrefix($projectPrefix as item()?, $decorVersion as xs:string?) as element(decor)* {
    art:getDecorByPrefix($projectPrefix, $decorVersion, ())
};
declare function art:getDecorByPrefix($projectPrefix as item()?, $decorVersion as xs:string?, $language as xs:string?) as element(decor)* {
    if (empty($projectPrefix)) then
        $get:colDecorData/decor[@repository = 'true'][not(@private = 'true')]
    else
    if (($projectPrefix instance of element() or $projectPrefix instance of attribute()) and $projectPrefix/ancestor-or-self::decor) then
        $projectPrefix/ancestor-or-self::decor[1]
    else
    if (empty($decorVersion)) then
        if ($projectPrefix = '*') then (
            $get:colDecorData/decor
        )
        else (
            $get:colDecorData/decor[project[@prefix = $projectPrefix]]
        )
    else 
    if ($decorVersion = '*') then (
        let $d      := 
            if ($projectPrefix = '*') then 
                $get:colDecorVersion/decor[@versionDate]
            else (
                $get:colDecorVersion/decor[@versionDate][project[@prefix = $projectPrefix]]
            )
        return
        if (empty($language)) then
            ($d[@language = $d/project/@defaultLanguage], $d)[1]
        else
            $d[@language = $language]
    )
    else 
    if (string-length($decorVersion) gt 0) then (
        let $d      := 
            if ($projectPrefix = '*') then 
                $get:colDecorVersion/decor[@versionDate = $decorVersion]
            else (
                $get:colDecorVersion/decor[@versionDate = $decorVersion][project[@prefix = $projectPrefix]]
            )
        return
        if (empty($language)) then
            ($d[@language = $d/project/@defaultLanguage], $d)[1]
        else
            $d[@language = $language]
    )
    else (
        let $d      := 
            if ($projectPrefix = '*') then 
                for $decor in $get:colDecorVersion/decor[@versionDate castable as xs:dateTime]
                let $pfx    := $decor/project/@prefix
                group by $pfx
                return
                    $decor[@versionDate = string(max($decor/xs:dateTime(@versionDate)))]
            else (
                for $decor in $get:colDecorVersion//project[@prefix = $projectPrefix]/ancestor::decor[@versionDate castable as xs:dateTime]
                let $pfx    := $decor/project/@prefix
                group by $pfx
                return
                    $decor[@versionDate = string(max($decor/xs:dateTime(@versionDate)))]
            )
        return
        if (empty($language)) then
            ($d[@language = $d/project/@defaultLanguage], $d)[1]
        else
            $d[@language = $language]
    )
};
(:~  Return one live specific or all repository/non-private DECOR projects
:
:   @param $projectId       - required decor/project/@id
:   @return exactly 1 DECOR project file or nothing if not found
:   @since 2015-04-29
:)
declare function art:getDecorById($projectId as xs:string?) as element(decor)? {
    art:getDecorById($projectId, (), ())
};
(:~  Return one live specific or repository/non-private DECOR project OR compiled, archived/released DECOR 
:   project based on project/@id and decor/@versionDate.
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $projectId       - required decor/project/@id or '*' for any project. If empty returns all non private BBRs
:   @param $decorVersion    - optional decor/@versionDate, Empty uses live projects, '*' selects all versions, dateTime() selects that versionDate, otherwise selects latest version for language
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @return all repository/non-private project files, exactly 1 DECOR live project file, or 1 or more archived 
:           DECOR version instances or nothing if not found
:   @since 2015-04-29
:)
declare function art:getDecorById($projectId as xs:string?, $decorVersion as xs:string?) as element(decor)* {
    art:getDecorById($projectId, $decorVersion, ())
};
declare function art:getDecorById($projectId as xs:string?, $decorVersion as xs:string?, $language as xs:string?) as element(decor)* {
    if (empty($projectId)) then
        $get:colDecorData/decor[@repository = 'true'][not(@private = 'true')]
    else
    if (empty($decorVersion)) then
        if ($projectId = '*') then (
            $get:colDecorData/decor
        )
        else (
            $get:colDecorData/decor[project[@id = $projectId]]
        )
    else 
    if ($decorVersion = '*') then (
        let $d      := 
            if ($projectId = '*') 
            then ($get:colDecorVersion/decor[@versionDate])
            else ($get:colDecorVersion/decor[@versionDate][project[@id = $projectId]])
        return
        if (empty($language)) then
            ($d[@language = $d/project/@defaultLanguage], $d)[1]
        else
            $d[@language = $language]
    )
    else 
    if (string-length($decorVersion) gt 0) then (
        let $d      := 
            if ($projectId = '*') 
            then ($get:colDecorVersion/decor[@versionDate = $decorVersion])
            else ($get:colDecorVersion/decor[@versionDate = $decorVersion][project[@id = $projectId]])
        return
        if (empty($language)) then
            ($d[@language = $d/project/@defaultLanguage], $d)[1]
        else 
            $d[@language = $language]
    )
    else (
        let $d              := 
            if ($projectId = '*') 
            then ($get:colDecorVersion/decor[@versionDate castable as xs:dateTime])
            else ($get:colDecorVersion/decor[@versionDate castable as xs:dateTime][project[@id = $projectId]])
        let $d              :=
            if (empty($language)) then
                ($d[@language = $d/project/@defaultLanguage], $d)[1]
            else
                $d[@language = $language]
        return
            $d[@versionDate = max($d/xs:dateTime(@versionDate))]
    )
};

(:~ Return list of live repository/non-private data sets and/or from a specific project
:
:   @param $projectPrefix   - optional project/@prefix
:   @param $includebbr      - optional boolean. If true includes datasets from in scope BBRs
:   @param $scenariosonly   - optional boolean. If true only includes datasets bound in a scenario transaction
:   @return all live repository/non-private data sets, all data sets for the given $projectPrefix or nothing if not found
:   @since 2017-09-12
:)
declare function art:getDatasetList($projectPrefix as xs:string?, $includebbr as xs:boolean, $scenariosonly as xs:boolean) as element(datasets) {
    art:getDatasetList($projectPrefix, (), (), $includebbr, $scenariosonly)
};
(:~ Return all repository/non-private data sets or from a specific project. Returns live data sets 
:   if param decorVersion is not a dateTime OR compiled, archived/released DECOR data sets based on project/@prefix and decor/@versionDate.
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $projectPrefix   - optional project/@prefix
:   @param $decorVersion    - optional decor/@versionDate
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @param $includebbr      - optional boolean. If true includes datasets from in scope BBRs
:   @param $scenariosonly   - optional boolean. If true only includes datasets bound in a scenario transaction
:   @return all live repository/non-private data sets, all data sets for the given $projectPrefix, or all archived 
:           DECOR version instances for the given $projectPrefix and $decorVersion or nothing if not found
:   @since 2017-09-12
:)
declare function art:getDatasetList($projectPrefix as xs:string?, $decorVersion as xs:string?, $includebbr as xs:boolean, $scenariosonly as xs:boolean) as element(datasets) {
    art:getDatasetList($projectPrefix, $decorVersion, (), $includebbr, $scenariosonly)
};
declare function art:getDatasetList($projectPrefix as xs:string?, $decorVersion as xs:string?, $language as xs:string?, $includebbr as xs:boolean, $scenariosonly as xs:boolean) as element(datasets) {
let $datasets       := art:getDatasets($projectPrefix, $decorVersion, $language)
let $datasets       := 
    if (not(empty($projectPrefix)) and empty($decorVersion) and $includebbr) then 
        $datasets | art:getDatasets((), (), ())
    else (
        $datasets
    )
    
let $datasets       :=
    if ($scenariosonly) then
        if (empty($projectPrefix)) then 
            $datasets[@id = ancestor::decor//representingTemplate/@sourceDataset]
        else (
            let $decor  := art:getDecorByPrefix($projectPrefix, $decorVersion, $language)
            return $datasets[@id = $decor//representingTemplate/@sourceDataset]
        )
    else (
        $datasets
    )

let $datasetnames           := 
    for $name in $datasets/name
    let $versionLabel   := $name/../@versionLabel
    return
        string-join(($name, $versionLabel, $name/@language), '')
let $datasetnameentries     :=
    for $datasetname in $datasetnames
    let $dsnm   := $datasetname
    group by $dsnm
    return map:entry($datasetname[1], count($datasetname))
let $datasetnamemap         := map:merge($datasetnameentries)

return
<datasets includebbr="{$includebbr}" scenariosonly="{$scenariosonly}">
{
    
    for $datasetById in $datasets
    let $ds-id          := $datasetById/@id
    group by $ds-id
    (:assumption is that newest comes after oldest. cheaper than date calculations:)
    order by if ($datasetById[last()]/name[@language = $language]) then $datasetById[last()]/name[@language = $language] else $datasetById[last()]/name[1]
    return (
        for $dataset in $datasetById
        let $statusCode     := 
            if ($dataset/@statusCode) then $dataset/@statusCode else (
                if ($dataset//concept[@statusCode = ('draft','new','review', 'pending')]) then
                    'draft'
                else (
                    'final'
                )
            )
        let $versionLabel   := $dataset/@versionLabel
        order by $dataset/@effectiveDate descending
        return
            <dataset projectId="{$dataset/ancestor::decor/project/@id}" prefix="{$dataset/ancestor::decor/project/@prefix}">
            {
                $dataset/@id,
                $dataset/@effectiveDate,
                attribute statusCode {$statusCode},
                $dataset/@versionLabel,
                $dataset/@expirationDate,
                $dataset/@officialReleaseDate,
                $dataset/@canonicalUri,
                $dataset/@lastModifiedDate
            }
            {
                for $name in $dataset/name
                let $label      := if ($versionLabel) then concat(' (',$versionLabel,')') else ()
                let $labeldate  := 
                    if ($dataset/@effectiveDate castable as xs:dateTime) then 
                        replace(format-dateTime(xs:dateTime($dataset/@effectiveDate),'[Y0001]-[M01]-[D01] [H01]:[m01]:[s01]', (), (), ()),' 00:00:00','')
                    else ()
                (: functional people find the date attached to the name in the selector/drop down list in the UI confusing
                    and it is not necessary when the name for a given language is unique enough. Often the effectiveDate
                    is not very helpful at all and rather random. So we generate a language dependent name specifically for 
                    the drop down selector that only concatenates the effectiveDate when the name for the given language is 
                    not unique in itself.
                :)
                let $selectorName   := string-join(($name, $versionLabel, $name/@language), '')
                let $selectorName   := 
                    if (map:get($datasetnamemap, $selectorName) gt 1) then 
                        concat($name, $label, ' :: ', $labeldate)
                    else (
                        concat($name, $label)
                    )
                return
                    (: use this for the drop down selector :)
                    <name>
                    {
                        $name/(@* except @selectorName)
                        ,
                        attribute selectorName {$selectorName}
                        ,
                        $name/node()
                    }
                    </name>
            }
            {
                 $dataset/(node() except (name|concept))
            }
            </dataset>
    )
}
</datasets>
};
(:~  Return all live repository/non-private data sets or from a specific project.
:
:   @param $projectPrefix   - optional project/@prefix
:   @return all live repository/non-private data sets, all data sets for the given $projectPrefix or nothing if not found
:   @since 2015-09-21
:)
declare function art:getDatasets($projectPrefix as xs:string?) as element(dataset)* {
    art:getDatasets($projectPrefix, (), ())
};
(:~  Return all repository/non-private data sets and/or from a specific project. Returns live data sets 
:   if param decorVersion is not a dateTime OR compiled, archived/released DECOR data sets based on 
:   project/@prefix and decor/@versionDate.
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $projectPrefix   - optional project/@prefix
:   @param $decorVersion    - optional decor/@versionDate
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @return all live repository/non-private data sets, all data sets for the given $projectPrefix, or all archived 
:           DECOR version instances for the given $projectPrefix and $decorVersion or nothing if not found
:   @since 2015-09-21
:)
declare function art:getDatasets($projectPrefix as xs:string?, $decorVersion as xs:string?) as element(dataset)* {
    art:getDatasets($projectPrefix, $decorVersion, ())
};
declare function art:getDatasets($projectPrefix as xs:string?, $decorVersion as xs:string?, $language as xs:string?) as element(dataset)* {
    art:getDecorByPrefix($projectPrefix, $decorVersion, $language)/datasets/dataset
};
(:~  Return the live data set based on id and optionally effectiveDate. If effectiveDate is empty, the latest version is returned
:
:   @param $ds-id   - required dataset/@id
:   @param $ds-ed   - optional dataset/@effectiveDate
:   @return exactly 1 dataset or nothing if not found
:   @since 2015-04-27
:)
declare function art:getDataset($ds-id as xs:string, $ds-ed as xs:string?) as element(dataset)? {
    art:getDataset($ds-id, $ds-ed, (), ())
};
(:~  Return the live data set if param decorVersion is not a dateTime OR 
:   compiled, archived/released DECOR data set based on dataset/@id|@effectiveDate and decor/@versionDate.
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $ds-id           - required dataset/@id
:   @param $ds-ed           - optional dataset/@effectiveDate
:   @param $decorVersion    - optional decor/@versionDate
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @return exactly 1 live data set, or 1 or more archived DECOR version instances or nothing if not found
:   @since 2015-04-29
:)
declare function art:getDataset($ds-id as xs:string, $ds-ed as xs:string?, $decorVersion as xs:string?) as element(dataset)* {
    art:getDataset($ds-id, $ds-ed, $decorVersion, ())
};
declare function art:getDataset($ds-id as xs:string, $ds-ed as xs:string?, $decorVersion as xs:string?, $language as xs:string?) as element(dataset)* {
    (: first try to use combined indexes to the max :)
    let $datasets       := 
        if ($decorVersion castable as xs:dateTime) then
            if ($ds-ed castable as xs:dateTime) then
                (: In eXist-db 5 this does not perform well :)
                (:$get:colDecorVersion//dataset[@id = $ds-id][@effectiveDate = $ds-ed]:)
                (: And for unknown reason this does :)
                for $ds in $get:colDecorVersion//dataset[@id = $ds-id]
                return
                    if ($ds[@effectiveDate = $ds-ed]) then $ds else ()
            else (
                let $c  := $get:colDecorVersion//dataset[@id = $ds-id]
                return $c[@effectiveDate = string(max($c/xs:dateTime(@effectiveDate)))]
                (:head(
                    for $ds in $get:colDecorVersion//dataset[@id = $ds-id]
                    order by $ds/@effectiveDate descending
                    return $ds
                ):)
            )
        else (
            if ($ds-ed castable as xs:dateTime) then
                (: In eXist-db 5 this does not perform well :)
                (:$get:colDecorData//dataset[@id = $ds-id][@effectiveDate = $ds-ed]:)
                (: And for unknown reason this does :)
                for $ds in $get:colDecorData//dataset[@id = $ds-id]
                return
                    if ($ds[@effectiveDate = $ds-ed]) then $ds else ()
            else (
                let $c  := $get:colDecorData//dataset[@id = $ds-id]
                return $c[@effectiveDate = string(max($c/xs:dateTime(@effectiveDate)))]
                (:head(
                    for $ds in $get:colDecorData//dataset[@id = $ds-id]
                    order by $ds/@effectiveDate descending
                    return $ds
                ):)
            )
        )
    (: then do subselection for version/language if requested :)
    let $datasets       :=
        if ($decorVersion castable as xs:dateTime) then
            switch ($language)
            case '*' return (
                $datasets[ancestor::decor[@versionDate = $decorVersion]]
            )
            default return 
                if (empty($language)) then
                    ($datasets[ancestor::decor[@versionDate = $decorVersion][@language = project/@defaultLanguage]], $datasets)[1]
                else (
                    ($datasets[ancestor::decor[@versionDate = $decorVersion][@language = $language]])[1]
                )
        else (
            $datasets
        )
    
    return
        $datasets[ancestor::datasets]
};

(:~  Return concept based on id and optionally effectiveDate. If effectiveDate is empty, the latest version is returned
:
:   @param $de-id   - required concept/@id
:   @param $de-ed   - optional concept/@effectiveDate
:   @return exactly 1 concept or nothing if not found
:   @since 2015-04-27
:)
declare function art:getConcept($de-id as xs:string, $de-ed as xs:string?) as element(concept)? {
    art:getConcept($de-id, $de-ed, (), ())
};
(:~  Return the live concept if param decorVersion is not a dateTime OR 
:   compiled, archived/released DECOR concept based on concept/@id|@effectiveDate and decor/@versionDate.
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $de-id           - required concept/@id
:   @param $de-ed           - optional concept/@effectiveDate
:   @param $decorVersion    - optional decor/@versionDate
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:   @return exactly 1 live concept, or 1 or more archived DECOR version instances or nothing if not found
:   @since 2015-04-29
:)
declare function art:getConcept($de-id as xs:string, $de-ed as xs:string?, $decorVersion as xs:string?) as element(concept)* {
    art:getConcept($de-id, $de-ed, $decorVersion, ())
};
declare function art:getConcept($de-id as xs:string, $de-ed as xs:string?, $decorVersion as xs:string?, $language as xs:string?) as element(concept)* {
    (: first try to use combined indexes to the max :)
    let $concepts       := 
        if ($decorVersion castable as xs:dateTime) then
            if ($de-ed castable as xs:dateTime) then
                (: In eXist-db 5 this does not perform well :)
                (:$get:colDecorVersion//concept[@id = $de-id][@effectiveDate = $de-ed]:)
                (: And for unknown reason this does :)
                for $c in $get:colDecorVersion//concept[@id = $de-id]
                return
                    if ($c[@effectiveDate = $de-ed]) then $c else ()
            else (
                let $c  := $get:colDecorVersion//concept[@id = $de-id]
                return $c[@effectiveDate = string(max($c/xs:dateTime(@effectiveDate)))]
                (:head(
                    for $de in $get:colDecorVersion//concept[@id = $de-id]
                    order by $de/@effectiveDate descending
                    return $de
                ):)
            )
        else (
            if ($de-ed castable as xs:dateTime) then
                (: In eXist-db 5 this does not perform well :)
                (:$get:colDecorData//concept[@id = $de-id][@effectiveDate = $de-ed]:)
                (: And for unknown reason this does :)
                for $c in $get:colDecorData//concept[@id = $de-id]
                return
                    if ($c[@effectiveDate = $de-ed]) then $c else ()
            else (
                let $c  := $get:colDecorData//concept[@id = $de-id]
                return $c[@effectiveDate = string(max($c/xs:dateTime(@effectiveDate)))]
                (:head(
                    for $de in $get:colDecorData//concept[@id = $de-id]
                    order by $de/@effectiveDate descending
                    return $de
                ):)
            )
        )
    (: then do subselection for version/language if requested :)
    let $concepts       :=
        if ($decorVersion castable as xs:dateTime) then
            switch ($language)
            case '*' return (
                $concepts[ancestor::decor[@versionDate = $decorVersion]]
            )
            default return 
                if (empty($language)) then
                    ($concepts[ancestor::decor[@versionDate = $decorVersion][@language = project/@defaultLanguage]], $concepts)[1]
                else (
                    ($concepts[ancestor::decor[@versionDate = $decorVersion][@language = $language]])[1]
                )
        else (
            $concepts
        )
    
    return
        $concepts[ancestor::datasets][not(ancestor::history)]
};

declare function art:getBuildingBlockRepositories($decor as element(decor)*, $sofar as map(*)?, $strDecorServicesURL as xs:string?) as element(decor)* {
    let $strDecorServicesURL        := if (empty($strDecorServicesURL)) then $get:strDecorServicesURL else $strDecorServicesURL
    let $sofar                      := 
        if (empty($sofar)) then
            map:merge(
                for $p in $decor 
                let $repourl                := ($p/ancestor::cacheme/@bbrurl, $strDecorServicesURL)[1]
                let $repoident              := $p/project/@prefix
                group by $repourl, $repoident
                return 
                    map:entry($repourl || '-' || $repoident, ())
            )
        else (
            $sofar
        )
    let $bbrp1                      := $decor/project/buildingBlockRepository[empty(@format)]
    let $bbrp2                      := $decor/project/buildingBlockRepository[@format = 'decor']
    let $bbrs                       :=
        for $p in $bbrp1 | $bbrp2
        let $repourl                := $p/@url
        let $repoident              := $p/@ident
        let $repoKey                := $repourl || '-' || $repoident
        group by $repourl, $repoident
        return (
            if (map:keys($sofar)[. = $repoKey]) then () else
            if ($repourl = $strDecorServicesURL) then 
                $get:colDecorData//decor[project/@prefix = $repoident] 
            else (
                $get:colDecorCache//cacheme[@bbrident = $repoident][@bbrurl = $repourl]/decor
            )
        )
    let $new-sofar                  :=
        map:merge(
            for $cc in (map:keys($sofar), $bbrs/concat((ancestor::cacheme/@bbrurl, $strDecorServicesURL)[1], '-', project/@prefix))
            return map:entry($cc, ())
        )
    let $other-bbrs                 := if ($bbrs) then art:getBuildingBlockRepositories($bbrs, $new-sofar, $strDecorServicesURL) else () 
    return
        $decor | $bbrs | $other-bbrs
};

(:~ Return list of live repository/non-private scenarios or from a specific project
:
:   @param $projectPrefix   - optional project/@prefix
:   @param $listtype        - optional code.
:                               all     = all transactions
:                               ds      = only transactions that have a representingTemplate/@sourceDataset
:                               tm      = only transactions that have a representingTemplate/@ref
:                               dstm    = only transactions that have a representingTemplate with @sourceDataset and @ref
:                               group   = only transactions of type group (transactions that group other transactions)
:                               item    = only transactions of type item (transactions that are stationary, initial or back)
:   @return all live repository/non-private scenarios, all scenarios for the given $projectPrefix or nothing if not found
:   @since 2017-09-12
:)
declare function art:getScenarioList($projectPrefix as xs:string?, $listtype as xs:string) as element(scenarios) {
    art:getScenarioList($projectPrefix, (), (), $listtype)
};
(:~ Return all repository/non-private scenarios or from a specific project. Returns live scenarios 
:   if param decorVersion is not a dateTime OR compiled, archived/released DECOR scenarios based on project/@prefix and decor/@versionDate.
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $projectPrefix   - optional project/@prefix
:   @param $decorVersion    - optional decor/@versionDate
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @param $listtype        - optional code.
:                               all     = all transactions
:                               ds      = only transactions that have a representingTemplate/@sourceDataset
:                               tm      = only transactions that have a representingTemplate/@ref
:                               dstm    = only transactions that have a representingTemplate with @sourceDataset and @ref
:                               group   = only transactions of type group (transactions that group other transactions)
:                               item    = only transactions of type item (transactions that are stationary, initial or back)
:   @return all live repository/non-private scenarios, all scenarios for the given $projectPrefix, or all archived 
:           DECOR version instances for the given $projectPrefix and $decorVersion or nothing if not found
:   @since 2017-09-12
:)
declare function art:getScenarioList($projectPrefix as xs:string?, $decorVersion as xs:string?, $listtype as xs:string?) as element(scenarios) {
    art:getScenarioList($projectPrefix, $decorVersion, (), $listtype)
};
declare function art:getScenarioList($projectPrefix as xs:string?, $decorVersion as xs:string?, $language as xs:string?, $listtype as xs:string?) as element(scenarios) {
let $decor          := art:getDecorByPrefix($projectPrefix, $decorVersion, $language)
let $scenarios      := $decor/scenarios/scenario

(: if transactions is empty, this equates to listtype = 'all' and we return every scenario/transaction based on the other params :)
let $transactions   := 
    if ($listtype='ds') then
        $scenarios//transaction[representingTemplate[@sourceDataset][concept]]
    else if ($listtype='tm') then
        $scenarios//transaction[representingTemplate[@ref][concept]]
    else if ($listtype='dstm') then
        $scenarios//transaction[representingTemplate[@sourceDataset][@ref][concept]]
    else if ($listtype='group') then
        $scenarios//transaction[@type='group']
    else if ($listtype='item') then
        $scenarios//transaction[not(@type='group')]
    else ()

(: build name maps because that is much faster than lookup :)
let $scenarionames  := 
    for $name in $scenarios
    let $versionLabel   := $name/../@versionLabel
    return
        string-join(($name, $versionLabel, $name/@language), '')
let $scnameentries  :=
    for $name in $scenarionames
    let $nm         := $name
    group by $nm
    return map:entry($name[1], count($name))
let $scnamemap      := map:merge($scnameentries)

(: get names for any transaction regardless of whether or not it is in scope :)
let $transactionnames   := 
    for $name in $scenarios//transaction/name
    let $versionLabel   := $name/../@versionLabel
    return
        string-join(($name, $versionLabel, $name/@language), '')
let $trnameentries  :=
    for $name in $scenarionames
    let $nm         := $name
    group by $nm
    return map:entry($name[1], count($name))
let $trnamemap      := map:merge($trnameentries)

let $sclist         := if ($transactions) then $scenarios/transaction[.//@id = $transactions/@id] else $scenarios

return
<scenarios listtype="{$listtype}">
{
    for $scenario in $scenarios
    let $ident          := $scenario/ancestor::decor/project/@prefix
    let $name           := if ($scenario/name[@language = $language]) then $scenario/name[@language = $language] else $scenario/name[1]
    let $versionLabel   := $scenario/@versionLabel
    order by lower-case($name)
    return
        <scenario>
        {
            $scenario/@id,
            $scenario/@effectiveDate,
            $scenario/@statusCode,
            $scenario/@versionLabel,
            $scenario/@expirationDate,
            $scenario/@officialReleaseDate,
            $scenario/@canonicalUri,
            $scenario/@lastModifiedDate,
            if ($ident = $projectPrefix) then () else (attribute ident {$ident}),
            attribute iddisplay {art:getNameForOID($scenario/@id, $language, $decor)}
        }
        {
            for $name in $scenario/name
            let $label      := if ($versionLabel) then concat(' (',$versionLabel,')') else ()
            let $labeldate  := 
                if ($scenario/@effectiveDate castable as xs:dateTime) then 
                    replace(format-dateTime(xs:dateTime($scenario/@effectiveDate),'[Y0001]-[M01]-[D01] [H01]:[m01]:[s01]', (), (), ()),' 00:00:00','')
                else ()
            (: functional people find the date attached to the name in the selector/drop down list in the UI confusing
                and it is not necessary when the name for a given language is unique enough. Often the effectiveDate
                is not very helpful at all and rather random. So we generate a language dependent name specifically for 
                the drop down selector that only concatenates the effectiveDate when the name for the given language is 
                not unique in itself.
            :)
            let $selectorName   := string-join(($name, $versionLabel, $name/@language), '')
            let $selectorName   := 
                if (map:get($scnamemap, $selectorName) gt 1) then 
                    concat($name, $label, ' :: ', $labeldate)
                else (
                    concat($name, $label)
                )
            return
                (: use this for a drop down selector :)
                <name>
                {
                    $name/(@* except @selectorName),
                    attribute selectorName {$selectorName},
                    $name/node()
                }
                </name>
        }
        {
            let $trlist     := if ($transactions) then $scenario/transaction[.//@id = $transactions/@id] else $scenario/transaction
            
            for $transaction in $trlist
            return art:getTransactionList($transaction, $projectPrefix, $transactions, $trnamemap)
        }
        </scenario>
}
</scenarios>
};
(: Recursive function for retrieving the basic transaction (group) info for a transaction hierarchy.
   Used for creating transaction navigation.
:)
declare function art:getTransactionList($transaction as element(transaction), $projectPrefix as xs:string?, $filter as element()*, $trnamemap as item()) as element(transaction) {
    let $ident          := $transaction/ancestor::decor/project/@prefix
    let $effectiveDate  := if ($transaction[@effectiveDate]) then ($transaction/@effectiveDate) else ($transaction/ancestor::scenario/@effectiveDate)
    let $versionLabel   := $transaction/@versionLabel
    return
    <transaction>
    {
        $transaction/@id,
        (:effectiveDate and statusCode have not always been there. copy from ancestor scenario if missing:)
        $effectiveDate,
        if ($transaction[@statusCode]) then ($transaction/@statusCode) else ($transaction/ancestor::scenario/@statusCode),
        $versionLabel,
        $transaction/@expirationDate,
        $transaction/@officialReleaseDate,
        $transaction/@type,
        $transaction/@label,
        $transaction/@model,
        $transaction/@canonicalUri,
        $transaction/@lastModifiedDate,
        if ($ident = $projectPrefix) then () else (attribute ident {$ident}),
        attribute iddisplay {art:getNameForOID($transaction/@id, (), $transaction/ancestor::decor)}
    }
    {
        for $name in $transaction/name
        let $label      := if ($versionLabel) then concat(' (',$versionLabel,')') else ()
        let $labeldate  := 
            if ($effectiveDate castable as xs:dateTime) then 
                replace(format-dateTime(xs:dateTime($effectiveDate),'[Y0001]-[M01]-[D01] [H01]:[m01]:[s01]', (), (), ()),' 00:00:00','')
            else ()
        (: functional people find the date attached to the name in the selector/drop down list in the UI confusing
            and it is not necessary when the name for a given language is unique enough. Often the effectiveDate
            is not very helpful at all and rather random. So we generate a language dependent name specifically for 
            the drop down selector that only concatenates the effectiveDate when the name for the given language is 
            not unique in itself.
        :)
        let $selectorName   := string-join(($name, $versionLabel, $name/@language), '')
        let $selectorName   := 
            if (map:get($trnamemap, $selectorName) gt 1) then 
                concat($name, $label, ' :: ', $labeldate)
            else (
                concat($name, $label)
            )
        return
            (: use this for a drop down selector :)
            <name>
            {
                $name/(@* except @selectorName),
                attribute selectorName {$selectorName},
                $name/node()
            }
            </name>
    }
    {
        if ($transaction[@type='group']) then (
            let $trlist     := if ($filter) then $transaction/transaction[.//@id = $filter/@id] else $transaction/transaction
            for $t in $trlist
            return
                art:getTransactionList($t, $projectPrefix, $filter, $trnamemap)
        ) else (
            $transaction/actors
            ,
            for $representingTemplate in $transaction/representingTemplate
            return
                <representingTemplate>
                {
                    $representingTemplate/@sourceDataset,
                    $representingTemplate/@sourceDatasetFlexibility,
                    $representingTemplate/@ref,
                    $representingTemplate/@flexibility,
                    $representingTemplate/@representingQuestionnaire,
                    $representingTemplate/@representingQuestionnaireFlexibility
                }
                </representingTemplate>
        )
    }
    </transaction>
};
(:~  Return scenario based on id and optionally effectiveDate. If effectiveDate is empty, the latest version is returned
:
:   @param $sc-id   - required dataset/@id
:   @param $sc-ed   - optional dataset/@effectiveDate
:   @return exactly 1 scenario or nothing if not found
:   @since 2015-04-27
:)
declare function art:getScenario($sc-id as xs:string, $sc-ed as xs:string?) as element(scenario)? {
    art:getScenario($sc-id, $sc-ed,(),())
};
(:~  Return the live scenario if param decorVersion is not a dateTime OR 
:   compiled, archived/released DECOR scenario based on scenario/@id|@effectiveDate and decor/@versionDate.
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $sc-id           - required scenario/@id
:   @param $sc-ed           - optional scenario/@effectiveDate
:   @param $decorVersion    - optional decor/@versionDate
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @return exactly 1 live scenario, or 1 or more archived DECOR version instances or nothing if not found
:   @since 2015-04-29
:)
declare function art:getScenario($sc-id as xs:string, $sc-ed as xs:string?, $decorVersion as xs:string?) as element(scenario)* {
    art:getScenario($sc-id, $sc-ed,$decorVersion,())
};
declare function art:getScenario($sc-id as xs:string, $sc-ed as xs:string?, $decorVersion as xs:string?, $language as xs:string?) as element(scenario)* {
    let $decor          :=
        if ($decorVersion[string-length()>0]) then
            let $d      := $get:colDecorVersion/decor[@versionDate=$decorVersion]
            return
            if (empty($language)) then
                $d[@language=$d/project/@defaultLanguage]
            else if ($language='*') then
                $d
            else
                $d[@language=$language]
        else (
            $get:colDecorData/decor
        )
    let $scenarios      := $decor//scenario[@id = $sc-id][ancestor::scenarios]
    
    return
        if ($sc-ed castable as xs:dateTime) then 
            $scenarios[@effectiveDate = $sc-ed] 
        else 
        if ($scenarios[2]) then 
            $scenarios[@effectiveDate = max($scenarios/xs:dateTime(@effectiveDate))]
        else (
            $scenarios
        )
};

(:~  Return transaction based on id and optionally effectiveDate. If effectiveDate is empty, the latest version is returned
:
:   @param $tr-id   - required dataset/@id
:   @param $tr-ed   - optional dataset/@effectiveDate
:   @return exactly 1 transaction or nothing if not found
:   @since 2015-04-27
:)
declare function art:getTransaction($tr-id as xs:string, $tr-ed as xs:string?) as element(transaction)? {
    art:getTransaction($tr-id, $tr-ed, (), ())
};
(:~  Return the live transaction if param decorVersion is not a dateTime OR 
:   compiled, archived/released DECOR transaction based on transaction/@id|@effectiveDate and decor/@versionDate.
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $tr-id           - required transaction/@id
:   @param $tr-ed           - optional transaction/@effectiveDate
:   @param $decorVersion    - optional decor/@versionDate
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @return exactly 1 live scenario, or 1 or more archived DECOR version instances or nothing if not found
:   @since 2015-04-29
:)
declare function art:getTransaction($tr-id as xs:string, $tr-ed as xs:string?, $decorVersion as xs:string?) as element(transaction)* {
    art:getTransaction($tr-id, $tr-ed, $decorVersion, ())
};
declare function art:getTransaction($tr-id as xs:string, $tr-ed as xs:string?, $decorVersion as xs:string?, $language as xs:string?) as element(transaction)* {
    let $decor          :=
        if ($decorVersion[string-length()>0]) then
            let $d      := $get:colDecorVersion/decor[@versionDate=$decorVersion]
            return
            if (empty($language)) then
                $d[@language=$d/project/@defaultLanguage]
            else if ($language='*') then
                $d
            else
                $d[@language=$language]
                
        else (
            $get:colDecorData/decor
        )
    let $transactions   := $decor//transaction[@id = $tr-id][ancestor::scenarios]
    
    return
        if ($tr-ed castable as xs:dateTime) then 
            $transactions[@effectiveDate = $tr-ed] 
        else 
        if ($transactions[2]) then 
            $transactions[@effectiveDate = max($transactions/xs:dateTime(@effectiveDate))]
        else (
            $transactions
        )
};

(:~  Return the live scenarios based on having any contained transaction that binds the requested dataset.
:
:   @param $ds-id           - required representingTemplate/@sourceDataset
:   @param $ds-ed           - optional representingTemplate/@sourceDatasetFlexibility
:   @return zero or more live scenarios
:   @since 2015-05-04
:)
declare function art:getScenariosByDataset($ds-id as xs:string, $ds-ed as xs:string?) as element(scenario)* {
    art:getScenariosByDataset($ds-id, $ds-ed, (), ())
};
(:~  Return the live scenarios if param decorVersion is not a dateTime OR 
:   compiled, archived/released DECOR scenarios based on having any contained transaction that binds the requested dataset.
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $ds-id           - required representingTemplate/@sourceDataset
:   @param $ds-ed           - optional representingTemplate/@sourceDatasetFlexibility
:   @param $decorVersion    - optional decor/@versionDate
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @return zero or more live scenarios, or 1 or more archived DECOR version instances or nothing if not found
:   @since 2015-05-04
:)
declare function art:getScenariosByDataset($ds-id as xs:string, $ds-ed as xs:string?, $decorVersion as xs:string?) as element(scenario)* {
    art:getScenariosByDataset($ds-id, $ds-ed, $decorVersion, ())
};
declare function art:getScenariosByDataset($ds-id as xs:string, $ds-ed as xs:string?, $decorVersion as xs:string?, $language as xs:string?) as element(scenario)* {
    let $decor          :=
        if ($decorVersion[string-length()>0]) then
            let $d      := $get:colDecorVersion/decor[@versionDate=$decorVersion]
            return
            if (empty($language)) then
                $d[@language=$d/project/@defaultLanguage]
            else if ($language='*') then
                $d
            else
                $d[@language=$language]
                
        else (
            $get:colDecorData/decor
        )
    let $scenarios      := $decor//scenario[ancestor::scenarios]
    let $dataset        := art:getDataset($ds-id, $ds-ed, $decorVersion)
    
    for $scenario in $scenarios
    let $datasets       := 
        for $representingTemplate in $scenario//representingTemplate[@sourceDataset = $ds-id]
        return art:getDataset($representingTemplate/@sourceDataset, $representingTemplate/@sourceDatasetFlexibility, $decorVersion)
    return
        if ($datasets[@effectiveDate = $dataset/@effectiveDate]) then $scenario else ()
};
(:~  Return the live transactions based on having a representingTemplate that binds the requested dataset.
:
:   @param $ds-id           - required representingTemplate/@sourceDataset
:   @param $ds-ed           - optional representingTemplate/@sourceDatasetFlexibility
:   @param $decorVersion    - optional decor/@versionDate
:   @return zero or more live transactions
:   @since 2015-05-04
:)
declare function art:getTransactionsByDataset($ds-id as xs:string, $ds-ed as xs:string?) as element(transaction)* {
    art:getTransactionsByScenarioAndDataset( (), (), $ds-id, $ds-ed, ())
};
(:~  Return the live transactions if param decorVersion is not a dateTime OR 
:   compiled, archived/released DECOR transactions based on having a representingTemplate that binds the requested dataset.
:   Could return multiple instances of the same transaction if the project was compiled in multiple languages.
:
:   @param $ds-id           - required representingTemplate/@sourceDataset
:   @param $ds-ed           - optional representingTemplate/@sourceDatasetFlexibility
:   @param $decorVersion    - optional decor/@versionDate
:   @return zero or more live scenarios, or 1 or more archived DECOR version instances or nothing if not found
:   @since 2015-05-04
:)
declare function art:getTransactionsByDataset($ds-id as xs:string, $ds-ed as xs:string?, $decorVersion as xs:string?) as element(transaction)* {
    art:getTransactionsByScenarioAndDataset((), (), $ds-id, $ds-ed, $decorVersion, ())
};
(:~  Return the live transactions based on having a representingTemplate that binds the requested dataset, 
:   and optionally filtered based on a specific scenario
:
:   @param $sc-id           - required scenario/@sourceDataset
:   @param $sc-ed           - optional scenario/@sourceDatasetFlexibility
:   @param $ds-id           - required representingTemplate/@sourceDataset
:   @param $ds-ed           - optional representingTemplate/@sourceDatasetFlexibility
:   @param $decorVersion    - optional decor/@versionDate
:   @return zero or more live transactions
:   @since 2015-05-04
:)
declare function art:getTransactionsByScenarioAndDataset($sc-id as xs:string, $sc-ed as xs:string?, $ds-id as xs:string, $ds-ed as xs:string?) as element(transaction)* {
    art:getTransactionsByScenarioAndDataset($sc-id, $sc-ed, $ds-id, $ds-ed, (), ())
};
(:~  Return the live transactions if param decorVersion is not a dateTime OR 
:   compiled, archived/released DECOR transactions based on having a representingTemplate that binds the requested dataset, 
:   and optionally filtered based on a specific scenario
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:   -- note: this function effectively performs the same as art:getTransactionsByDataset($ds-id, $ds-ed, $decorVersion) when attribute $sc-id is missing.
:
:   @param $sc-id           - optional scenario/@sourceDataset
:   @param $sc-ed           - optional scenario/@sourceDatasetFlexibility
:   @param $ds-id           - required representingTemplate/@sourceDataset
:   @param $ds-ed           - optional representingTemplate/@sourceDatasetFlexibility
:   @param $decorVersion    - optional decor/@versionDate
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @return zero or more live transactions, or 1 or more archived DECOR version instances
:   @since 2015-05-04
:)
declare function art:getTransactionsByScenarioAndDataset($sc-id as xs:string?, $sc-ed as xs:string?, $ds-id as xs:string, $ds-ed as xs:string?, $decorVersion as xs:string?) as element(transaction)* {
    art:getTransactionsByScenarioAndDataset($sc-id, $sc-ed, $ds-id, $ds-ed, $decorVersion, ())
};
declare function art:getTransactionsByScenarioAndDataset($sc-id as xs:string?, $sc-ed as xs:string?, $ds-id as xs:string, $ds-ed as xs:string?, $decorVersion as xs:string?, $language as xs:string?) as element(transaction)* {
    let $decor          :=
        if ($decorVersion[string-length()>0]) then
            let $d      := $get:colDecorVersion/decor[@versionDate=$decorVersion]
            return
            if (empty($language)) then
                $d[@language=$d/project/@defaultLanguage]
            else if ($language='*') then
                $d
            else
                $d[@language=$language]
                
        else (
            $get:colDecorData/decor
        )
    let $scenarios      := if (empty($sc-id)) then $decor/scenarios/scenario else (art:getScenario($sc-id, $sc-ed, $decorVersion))
    let $dataset        := art:getDataset($ds-id, $ds-ed, $decorVersion)
    
    for $representingTemplate in $scenarios//representingTemplate[@sourceDataset = $ds-id]
    return
        if ($representingTemplate/@sourceDatasetFlexibility[. castable as xs:dateTime] and 
            $dataset[@effectiveDate = $representingTemplate/@sourceDatasetFlexibility]) then
            $representingTemplate/parent::transaction
        else (
            let $ds     := art:getDataset($ds-id, $representingTemplate/@sourceDatasetFlexibility, $decorVersion, $language)
            return
                if ($dataset/@effectiveDate[.=$ds/@effectiveDate]) then $representingTemplate/parent::transaction else ()
        )
};

(:~  Return live transaction concept based on id and optionally effectiveDate + transaction id and optionally effectiveDate. If an effectiveDate is empty, the latest version is returned
:
:   @param $de-id   - required concept/@id
:   @param $de-ed   - optional concept/@effectiveDate
:   @param $tr-id   - required transaction/@id
:   @param $tr-ed   - optional transaction/@effectiveDate
:   @return exactly 1 concept or nothing if not found
:   @since 2018-06-21
:)
declare function art:getTransactionConcept($de-id as xs:string, $de-ed as xs:string?, $tr-id as xs:string?, $tr-ed as xs:string?) as element(concept)* {
    art:getTransactionConcept($de-id, $de-ed, $tr-id, $tr-ed, (), ())
};
(:~  Return live transaction concept if param decorVersion is not a dateTime OR 
:   compiled, archived/released DECOR transaction concept based on concept/@id|@effectiveDate and decor/@versionDate.
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $de-id           - required concept/@id
:   @param $de-ed           - optional concept/@effectiveDate
:   @param $tr-id           - required transaction/@id
:   @param $tr-ed           - optional transaction/@effectiveDate
:   @param $decorVersion    - optional decor/@versionDate
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:   @return exactly 1 live concept, or 1 or more archived DECOR version instances or nothing if not found
:   @since 2015-04-29
:)
declare function art:getTransactionConcept($de-id as xs:string, $de-ed as xs:string?, $tr-id as xs:string?, $tr-ed as xs:string?, $decorVersion as xs:string?) as element(concept)* {
    art:getTransactionConcept($de-id, $de-ed, $tr-id, $tr-ed, $decorVersion, ())
};
declare function art:getTransactionConcept($de-id as xs:string, $de-ed as xs:string?, $tr-id as xs:string?, $tr-ed as xs:string?, $decorVersion as xs:string?, $language as xs:string?) as element(concept)* {
    let $transaction        := 
        if (empty($tr-id)) then
            $get:colDecorData//transaction
        else 
            art:getTransaction($tr-id, $tr-ed, $decorVersion, $language)
    
    let $concepts           := $transaction//concept[@ref = $de-id]
    
    (: Note that we currently do not implement doing much with the concept effectivedate. In a normal dataset and hence in a normal transaction there will be only 1 version of a given concept so we should not really worry about getting 'the right version' :)
    return
        if ($de-ed castable as xs:dateTime) then 
            for $concept in $concepts
            let $referencedConcept  := art:getConcept($concept/@ref, $concept/@flexibility)
            return
                if ($referencedConcept[@effectiveDate = $de-ed]) then $concept else ()
        else
        if ($concepts[2]) then (
            let $referencedConcepts :=
                for $concept in $concepts
                return art:getConcept($concept/@ref, $concept/@flexibility)
            
            return
            $concepts[not(@flexibility castable as xs:dateTime)] | $concepts[@flexibility = max($referencedConcepts/xs:dateTime(@effectiveDate))]
        )
        else (
            $concepts
        )
};

(:~  Return live terminologyAssociations for the project with the given @prefix
:
:   @param $projectPrefix   - required project/@prefix
:   @return all live terminologyAssociations for the given $projectPrefix or nothing if not found
:   @since 2015-09-22
:)
declare function art:getTerminologyAssociations($projectPrefix as xs:string) as element(terminologyAssociation)* {
    art:getTerminologyAssociations($projectPrefix, (), ())
};
(:~  Return live terminologyAssociations for the project with the given @prefix or terminologyAssociations from an archived $decorVersion
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $projectPrefix   - required project/@prefix
:   @param $decorVersion    - optional decor/@versionDate
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @return all live terminologyAssociations for the given $projectPrefix, or all terminologyAssociations from an archived $decorVersion or nothing if not found
:   @since 2015-09-22
:)
declare function art:getTerminologyAssociations($projectPrefix as xs:string, $decorVersion as xs:string?) as element(terminologyAssociation)* {
    art:getTerminologyAssociations($projectPrefix, $decorVersion, ())
};
declare function art:getTerminologyAssociations($projectPrefix as xs:string, $decorVersion as xs:string?, $language as xs:string?) as element(terminologyAssociation)* {
    art:getTerminologyAssociationsP(art:getDecorByPrefix($projectPrefix, $decorVersion, $language))
};
(:~  Return terminologyAssociations for the project in parameter $decor
:
:   @param $decor           - optional decor project
:   @return all terminologyAssociations for the given project in $decor, or nothing if not found
:   @since 2015-09-22
:)
declare function art:getTerminologyAssociationsP($decor as element(decor)?) as element(terminologyAssociation)* {
    $decor/terminology/terminologyAssociation
};

(:~  Return live identifierAssociations for the project with the given @prefix
:
:   @param $projectPrefix   - required project/@prefix
:   @return all live identifierAssociations for the given $projectPrefix or nothing if not found
:   @since 2017-04-05
:)
declare function art:getIdentifierAssociations($projectPrefix as xs:string) as element(identifierAssociation)* {
    art:getIdentifierAssociations($projectPrefix, (), ())
};
(:~  Return live identifierAssociations for the project with the given @prefix or identifierAssociations from an archived $decorVersion
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $projectPrefix   - required project/@prefix
:   @param $decorVersion    - optional decor/@versionDate
:   @param $language        - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @return all live identifierAssociations for the given $projectPrefix, or all identifierAssociations from an archived $decorVersion or nothing if not found
:   @since 2017-04-05
:)
declare function art:getIdentifierAssociations($projectPrefix as xs:string, $decorVersion as xs:string?) as element(identifierAssociation)* {
    art:getIdentifierAssociations($projectPrefix, $decorVersion, ())
};
declare function art:getIdentifierAssociations($projectPrefix as xs:string, $decorVersion as xs:string?, $language as xs:string?) as element(identifierAssociation)* {
    art:getIdentifierAssociationsP(art:getDecorByPrefix($projectPrefix, $decorVersion, $language))
};
(:~  Return identifierAssociations for the project in parameter $decor
:
:   @param $decor           - optional decor project
:   @return all identifierAssociations for the given project in $decor, or nothing if not found
:   @since 2017-04-05
:)
declare function art:getIdentifierAssociationsP($decor as element(decor)?) as element(identifierAssociation)* {
    $decor/ids/identifierAssociation
};

(:~ Convenience function that adds a list of identifierAssociations and terminologyAssociations through respective functions and returns the combined output thereof :)
declare function art:addAssociations($associations as element()*, $decorOrtransactionConcept as element(), $replacemode as xs:boolean, $testmode as xs:boolean) as element(result) {
    <return>
    {
        art:addTerminologyAssociations($associations[self::terminologyAssociation], $decorOrtransactionConcept, $replacemode, $testmode)/node()
        ,
        art:addIdentifierAssociations($associations[self::identifierAssociation], $decorOrtransactionConcept, $replacemode, $testmode)/node()
    }
    </return>
};
(:~  Add a list of terminologyAssociations in the project in parameter $decor. Each association is checked for existence so we don't add twice. With $replacemode=true we delete first and add as new. 
:   With $testmode=true we do not change anything in the project but only return what would happen *if* the given list was applied. For a terminology association to a value set to work it is important
:   that there is a matching value set (ref) and the project contains the right building block repository reference in the project section if applicable. This is all checked and handled.
:
:   Known issue: does not currently handle adding multiple building block repository reference when a value set lives in multiple
:
:   @param $terminologyAssociations     - 0..* terminologyAssociation elements to add
:   @param $decor                       - 1..1 decor project to add the associations to
:   @param $replacemode                 - 1..1 boolean. true will delete each association first and add as new. false will only add if not present
:   @param $testmode                    - 1..1 boolean. true will give back what would happen if changes would be applied but doesn't change anything. false will apply whatever changes necessary
:   @return a report of changes there were done ($testmode=false) or would be done ($testmode=true)
:           <result>
:               <terminologyAssociation conceptId="x" @*>               (contains only attributes as they would be written, which might be less than what was supplied)
:                   <add>true|false</add>                               (contains true if association is/would be added with attributes as indicated)
:                   <add-ref>true|false</add-ref>                       (only present for value set associations. contains true if association is a value set that did not exist in the project yet)
:                   <add-bbr url="x" ident="y">true|false</add-bbr>     (only present for value set associations. contains true if value set lives in a repository that this project does not reference yet. @url|@ident contain the reference details)
:               </terminologyAssociation>
:           </result>
:   @since 2015-09-22
:)
declare function art:addTerminologyAssociations($terminologyAssociations as element(terminologyAssociation)*, $decorOrtransactionConcept as element(), $replacemode as xs:boolean, $testmode as xs:boolean) as element(result) {
    let $decor                          := $decorOrtransactionConcept/ancestor-or-self::decor
    let $concept                        := $decorOrtransactionConcept[self::concept][ancestor::representingTemplate]
    let $allStoredAssociations         := art:getTerminologyAssociationsP($decor)
    
    (: radical solution: delete first, insert later, might be too harsh :)
    return
    <result>
    {
        for $terminologyAssociation in $terminologyAssociations
        let $s_cid                  := $terminologyAssociation/@conceptId[string-length()>0]
        let $s_ceff                 := $terminologyAssociation/@conceptFlexibility[string-length()>0]
        let $s_valueSet             := $terminologyAssociation/@valueSet[string-length()>0]
        let $s_valueSetFlexibility  := $terminologyAssociation/@flexibility[string-length()>0]
        let $s_code                 := $terminologyAssociation/@code[string-length()>0]
        let $s_codeSystem           := $terminologyAssociation/@codeSystem[string-length()>0]
        let $rewrittedAssociation   := art:prepareTerminologyAssociationForUpdate($terminologyAssociation, exists($concept))
        let $currentStoredAssocs    := ($allStoredAssociations[@conceptId=$s_cid][@valueSet=$s_valueSet] | 
                                        $allStoredAssociations[@conceptId=$s_cid][@code=$s_code][@codeSystem=$s_codeSystem])
        let $deleteAssoc            :=
            if ($testmode) then () else if ($replacemode) then (update delete $currentStoredAssocs) else ()
        let $storeAssoc             :=
            if ($currentStoredAssocs or empty($rewrittedAssociation)) then (false()) else (
                let $x              :=
                    if ($testmode) then ()
                    else if ($concept) then (
                        if ($concept/identifierAssociation) then
                            update insert $rewrittedAssociation preceding $concept/identifierAssociation[1]
                        else (
                            update insert $rewrittedAssociation into $concept
                        )
                    )
                    else if (not($decor/terminology)) then (
                        update insert <terminology>{$rewrittedAssociation}</terminology> following $decor/ids
                    )
                    else if ($decor/terminology/terminologyAssociation) then (
                        update insert $rewrittedAssociation preceding $decor/terminology/terminologyAssociation[1]
                    )
                    else if ($decor/terminology/*) then (
                        update insert $rewrittedAssociation preceding $decor/terminology/*[1]
                    )
                    else (
                        update insert $rewrittedAssociation into $decor/terminology
                    )
                return true()
            )
        
        (: === ValuetSet and BuildingBlockRepository handling === :)
        let $currentStoredRefs      := if (empty($s_valueSet)) then () else ($decor/terminology/valueSet[@id=$s_valueSet] | $decor/terminology/valueSet[@ref=$s_valueSet] | $decor/terminology/valueSet[@name=$s_valueSet])
        let $valueSetExists         := if ($s_valueSet) then (exists($currentStoredRefs)) else (true())
        let $conceptList            := if ($valueSetExists) then () else $get:colDecorData//conceptList[@id=$s_cid][not(ancestor::history)]
        let $valueSet               := 
            if ($conceptList) then (
                vs:getValueSetByRef($s_valueSet, $s_valueSetFlexibility, $conceptList/ancestor::decor/project/@prefix, false())//valueSet[@id] 
            )[1] else ()
        
        let $valueSetRef            :=
            if ($valueSet) then (
                <valueSet ref="{$valueSet/@id}" name="{$valueSet/@name}" displayName="{if ($valueSet/@displayName[not(.='')]) then $valueSet/@displayName else $valueSet/@name}"/>
            ) else ()
        let $deleteValueSetRef      :=
            if ($testmode) then () else if ($replacemode) then (update delete $currentStoredRefs) else ()
        let $storeValueSetRef       :=
            if ($valueSetRef) then (
                let $x              :=
                    if ($testmode) then ()
                    else if (not($decor/terminology)) then (
                        update insert <terminology>{$valueSetRef}</terminology> following $decor/ids
                    )
                    else if ($decor/terminology/valueSet) then (
                        update insert $valueSetRef following $decor/terminology/valueSet[last()]
                    )
                    else if ($decor/terminology/*) then (
                        update insert $valueSetRef following $decor/terminology/*[last()]
                    )
                    else (
                        update insert $valueSetRef into $decor/terminology
                    )
                return true()
            ) else (false())
            
        let $bbrRef                 := 
            if ($valueSet) then (
                if ($valueSet/parent::repository) then (
                    <buildingBlockRepository url="{$valueSet/../@url}" ident="{$valueSet/../@ident}"/>
                ) else if ($valueSet/parent::project) then (
                    <buildingBlockRepository url="{adserver:getServerURLServices()}" ident="{$valueSet/../@ident}"/>
                ) else ()
            ) else ()
        let $storeBBR               :=
            if ($bbrRef and not($decor/project/buildingBlockRepository[@url=$bbrRef/@url][@ident=$bbrRef/@ident][empty(@format)] |
                                $decor/project/buildingBlockRepository[@url=$bbrRef/@url][@ident=$bbrRef/@ident][@format='decor'])) then (
                let $x              :=
                    if ($testmode) then ()
                    else (update insert $bbrRef following $decor/project/(author|reference|restURI|defaultElementNamespace|contact)[last()])
                return true()
            ) else (false())
        return (
            element {$rewrittedAssociation/name()} {
                $rewrittedAssociation/@*,
                <add>{$storeAssoc}</add>,
                if ($s_valueSet) then (
                    <add-ref>{$storeValueSetRef}</add-ref>,
                    <add-bbr>{if ($bbrRef) then ($bbrRef/(@url|@ident)) else (),$storeBBR}</add-bbr>
                ) else ()
            }
        )
    }
    </result>
};

(:~  Return only valid, non empty attributes on a terminology association as defined in the DECOR format. Note: does not check co-occurence problems such as @valueSet + @code
:
:   @param $terminologyAssociation     - 0..1 terminologyAssociation elements
:   @param $allowEmpty                - true|false only relevant for transaction concepts when you need to explicitly exclude a dataset association in a transaction
:   @return the pruned terminologyAssociation element for the given project in $decor, or nothing input was empty
:   @since 2015-09-22
:)
declare function art:prepareTerminologyAssociationForUpdate($terminologyAssociation as element()?) as element(terminologyAssociation)? {
    art:prepareTerminologyAssociationForUpdate($terminologyAssociation, false())
};
declare function art:prepareTerminologyAssociationForUpdate($terminologyAssociation as element()?, $allowEmpty as xs:boolean) as element(terminologyAssociation)? {
    if ($terminologyAssociation[@valueSet[not(.='')]] | $terminologyAssociation[@code[not(.='')]][@codeSystem[not(.='')]] | $terminologyAssociation[$allowEmpty][not(@valueSet | @code | @codeSystem)]) then (
        <terminologyAssociation>
        {
            $terminologyAssociation/@conceptId,
            $terminologyAssociation[string-length(@valueSet) = 0]/@conceptFlexibility[not(.='')]
            ,
            if ($terminologyAssociation[@valueSet[not(.='')]]) then (
                $terminologyAssociation/@valueSet,
                $terminologyAssociation/@flexibility[not(.='')],
                $terminologyAssociation/@strength[not(.='')]
            )
            else
            if ($terminologyAssociation[@code[not(.='')]][@codeSystem[not(.='')]]) then (
                $terminologyAssociation/@code,
                $terminologyAssociation/@codeSystem,
                $terminologyAssociation/@codeSystemName[not(.='')],
                $terminologyAssociation/@codeSystemVersion[not(.='')],
                $terminologyAssociation/@displayName[not(.='')]
            )
            else ()
            ,
            if ($terminologyAssociation/@effectiveDate[. castable as xs:dateTime]) then (
                $terminologyAssociation/@effectiveDate
            )
            else (
                attribute effectiveDate {format-dateTime(current-dateTime(),'[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01]', (), (), ())}
            )
            ,
            $terminologyAssociation/@expirationDate[. castable as xs:dateTime], 
            $terminologyAssociation/@officialReleaseDate[. castable as xs:dateTime], 
            $terminologyAssociation/@versionLabel[not(.='')],
            $terminologyAssociation/@equivalence[not(.='')]

        }
        </terminologyAssociation>
    ) else ()
};

(:~  Return only valid, non empty attributes on a valueSet as defined in the DECOR format.
:   Example baseValueset:
:       <valueSet id="..." name="..." displayName="..." effectiveDate="yyyy-mm-ddThh:mm:ss" statusCode="draft" versionLabel="..."/>
:
:   @param $editedValueSet     - 1..1 valueSet as edited
:   @param $baseValueset       - 1..1 valueSet element containing all attributes that should be set on the prepared valueSet, like new id/status etc.
:   @return the pruned valueSet element for the given project in $decor, or nothing input was empty
:   @since 2015-09-22
:)
declare function art:prepareValueSetForUpdate($editedValueset as element(valueSet), $baseValueset as element(valueSet)) as element(valueSet) {
    <valueSet>
    {
        $baseValueset/@*[not(. = '')]
    }
    {
        for $desc in $editedValueset/desc[.//text()[string-length()>0]]
        return
            art:parseNode($desc)
    }
    {
        for $relshp in $editedValueset/relationship
        return
        <relationship>
        {
            $relshp/@type
            ,
            $relshp/@ref
            ,
            $relshp/@flexibility[. castable as xs:dateTime or . = 'dynamic']
        }
        </relationship>
    }
    {
        (:want at least a @name on publishingAuthority for it to persist:)
        for $authority in $editedValueset/publishingAuthority[@name[string-length()>0]]
        return
        <publishingAuthority>
        {
            $authority/@*[string-length()>0]
            ,
            for $addrLine in $authority/addrLine[string-length()>0]
            return <addrLine>{$addrLine/@*[string-length()>0],$addrLine/node()}</addrLine>
        }
        </publishingAuthority>
    }
    {
        (:want at least a @name on endorsingAuthority for it to persist:)
        for $authority in $editedValueset/endorsingAuthority[@name[string-length()>0]]
        return
        <endorsingAuthority>
        {
            $authority/@*[string-length()>0]
            ,
            for $addrLine in $authority/addrLine[string-length()>0]
            return <addrLine>{$addrLine/@*[string-length()>0],$addrLine/node()}</addrLine>
        }
        </endorsingAuthority>
    }
    {
        for $desc in $editedValueset/copyright[empty(inherited)][.//text()[string-length()>0]]
        return
            <copyright>{$desc/@*, art:parseNode($desc)/(node() except *:div[@data-source = 'inherited'])}</copyright>
    }
    {
        (:want at least a @date on revisionHistory for it to persist:)
        for $revisionHistory in $editedValueset/revisionHistory[string-length(@date)>0]
        return
        <revisionHistory>
        {
            $revisionHistory/@*[string-length()>0],
            for $desc in $revisionHistory/desc
            return
                art:parseNode($desc)
        }
        </revisionHistory>
    }
    {
        if ($editedValueset/conceptList[concept|include|exclude|exception]) then
            <conceptList>
            {
                for $completeCodeSystem in $editedValueset/completeCodeSystem[string-length(@codeSystem) gt 0]
                return
                    <include>
                    {
                        $completeCodeSystem/@codeSystem[not(. = '')],
                        $completeCodeSystem/@codeSystemName[not(. = '')],
                        $completeCodeSystem/@codeSystemVersion[not(. = '')],
                        $completeCodeSystem/@flexibility[. castable as xs:dateTime] | $completeCodeSystem/@flexibility[. = 'dynamic'],
                        $completeCodeSystem/@type[. = 'D'],
                        for $f in $completeCodeSystem/filter[(@property|@op|@value)[not(. = '')]]
                        return
                            <filter>{$f/@property[not(. = '')], $f/@op[not(. = '')], $f/@value[not(. = '')]}</filter>
                    }
                    </include>
                ,
                for $concept in (   $editedValueset/conceptList/concept[string-length(@code)>0][string-length(@codeSystem)>0] |
                                    $editedValueset/conceptList/include[(@ref|@op|@codeSystem)[string-length()>0]][not(@exception='true')] |
                                    $editedValueset/conceptList/exclude[(@ref|@op|@codeSystem)[string-length()>0]][not(@exception='true')])
                return
                element {$concept/name()}
                {
                    $concept/(@*[string-length()>0] except @conceptId),
                    for $desc in $concept/designation[@displayName[string-length()>0]] return art:parseNode($desc),
                    for $desc in $concept/desc[.//text()[string-length()>0]] return art:parseNode($desc),
                    for $f in $concept/filter[(@property|@op|@value)[not(. = '')]]
                    return
                        <filter>{$f/@property[not(. = '')], $f/@op[not(. = '')], $f/@value[not(. = '')]}</filter>
                }
                ,
                for $concept in (   $editedValueset/conceptList/exception[string-length(@code)>0][string-length(@codeSystem)>0] |
                                    $editedValueset/conceptList/include[(@ref|@op|@codeSystem)[string-length()>0]][@exception='true'] |
                                    $editedValueset/conceptList/exclude[(@ref|@op|@codeSystem)[string-length()>0]][@exception='true'])
                return
                element {$concept/name()}
                {
                    $concept/(@*[string-length()>0] except @conceptId),
                    for $desc in $concept/designation[@displayName[string-length()>0]] return art:parseNode($desc),
                    for $desc in $concept/desc[.//text()[string-length()>0]] return art:parseNode($desc),
                    for $f in $concept/filter[(@property|@op|@value)[not(. = '')]]
                    return
                        <filter>{$f/@property[not(. = '')], $f/@op[not(. = '')], $f/@value[not(. = '')]}</filter>
                }
            }
            </conceptList>
        else ()
    }
    </valueSet>
};

(:~  Return only valid, non empty attributes on a codeSystem as defined in the DECOR format.
:   Example baseCodeSystem:
:       <codeSystem id="..." name="..." displayName="..." effectiveDate="yyyy-mm-ddThh:mm:ss" statusCode="draft" versionLabel="..."/>
:
:   @param $editedCodeSystem   - 1..1 codeSystem as edited
:   @param $baseCodeSystem       - 1..1 codeSystem element containing all attributes that should be set on the prepared codeSystem, like new id/status etc.
:   @return the pruned codeSystem element for the given project in $decor, or nothing input was empty
:   @since 2018-09-27
:)
declare function art:prepareCodeSystemForUpdate($editedCodeSystem as element(codeSystem), $baseCodeSystem as element(codeSystem)) as element(codeSystem) {
    <codeSystem>
    {
        $baseCodeSystem/@*[not(. = '')]
    }
    {
        for $desc in $editedCodeSystem/desc[.//text()[string-length()>0]]
        return
            art:parseNode($desc)
    }
    {
        (:want at least a @name on publishingAuthority for it to persist:)
        for $authority in $editedCodeSystem/publishingAuthority[@name[string-length()>0]]
        return
        <publishingAuthority>
        {
            $authority/@*[string-length()>0]
            ,
            for $addrLine in $authority/addrLine[string-length()>0]
            return <addrLine>{$addrLine/@*[string-length()>0],$addrLine/node()}</addrLine>
        }
        </publishingAuthority>
    }
    {
        (:want at least a @name on endorsingAuthority for it to persist:)
        for $authority in $editedCodeSystem/endorsingAuthority[@name[string-length()>0]]
        return
        <endorsingAuthority>
        {
            $authority/@*[string-length()>0]
            ,
            for $addrLine in $authority/addrLine[string-length()>0]
            return <addrLine>{$addrLine/@*[string-length()>0], $addrLine/node()}</addrLine>
        }
        </endorsingAuthority>
    }
    {
        $editedCodeSystem/copyright[.//text()[string-length() gt 0]]
    }
    {
        (:want at least a @date on revisionHistory for it to persist:)
        for $revisionHistory in $editedCodeSystem/revisionHistory[string-length(@date)>0]
        return
        <revisionHistory>
        {
            $revisionHistory/@*[string-length()>0],
            for $desc in $revisionHistory/desc
            return
                art:parseNode($desc)
        }
        </revisionHistory>
    }
    {
        if ($editedCodeSystem/conceptList[codedConcept]) then
            <conceptList>
            {
                for $concept in $editedCodeSystem/conceptList/codedConcept[string-length(@code) gt 0]
                return
                element {$concept/name()}
                {
                    $concept/@code[string-length() gt 0],
                    $concept/@ordinal[string-length() gt 0],
                    $concept/@level[string-length() gt 0],
                    $concept/@type[string-length() gt 0],
                    $concept/@statusCode[string-length() gt 0],
                    $concept/@effectiveDate[. castable as xs:dateTime],
                    $concept/@expirationDate[. castable as xs:dateTime],
                    $concept/@officialReleaseCode[. castable as xs:dateTime],
                    for $desc in $concept/designation[@displayName[string-length() gt 0]] return art:parseNode($desc),
                    for $desc in $concept/desc[.//text()[string-length() gt 0]] return art:parseNode($desc)
                }
            }
            </conceptList>
        else ()
    }
    </codeSystem>
};

(:~  Add a list of identifierAssociations in the project in parameter $decor. Each association is checked for existence so we don't add twice. With $replacemode=true we delete first and add as new. 
:   With $testmode=true we do not change anything in the project but only return what would happen *if* the given list was applied. 
:
:   @param $identifierAssociations      - 0..* identifierAssociation elements to add
:   @param $decor                       - 1..1 decor project to add the associations to
:   @param $replacemode                 - 1..1 boolean. true will delete each association first and add as new. false will only add if not present
:   @param $testmode                    - 1..1 boolean. true will give back what would happen if changes would be applied but doesn't change anything. false will apply whatever changes necessary
:   @return a report of changes there were done ($testmode=false) or would be done ($testmode=true)
:           <result>
:               <identifierAssociation conceptId="x" @*>                (contains only attributes as they would be written, which might be less than what was supplied)
:                   <add>true|false</add>                               (contains true if association is/would be added with attributes as indicated)
:               </identifierAssociation>
:           </result>
:   @since 2017-04-05
:)
declare function art:addIdentifierAssociations($identifierAssociations as element(identifierAssociation)*, $decorOrtransactionConcept as element(), $replacemode as xs:boolean, $testmode as xs:boolean) as element(result) {
    let $decor                          := $decorOrtransactionConcept/ancestor-or-self::decor
    let $concept                        := $decorOrtransactionConcept[self::concept][ancestor::representingTemplate]
    let $allStoredAssociations          := art:getIdentifierAssociationsP($decor)
    
    (: radical solution: delete first, insert later, might be too harsh :)
    return
    <result>
    {
        for $identifierAssociation in $identifierAssociations
        let $s_cid                  := $identifierAssociation/@conceptId[string-length()>0]
        let $s_ceff                 := $identifierAssociation/@conceptFlexibility[string-length()>0]
        let $s_ref                  := $identifierAssociation/@ref[string-length()>0]
        let $rewrittedAssociation   := art:prepareIdentifierAssociationForUpdate($identifierAssociation, exists($concept))
        let $currentStoredAssocs    := $allStoredAssociations[@conceptId=$s_cid][@ref=$s_ref]
        
        let $deleteAssoc            :=
            if ($testmode) then () else if ($replacemode) then (update delete $currentStoredAssocs) else ()
        let $storeAssoc             :=
            if ($currentStoredAssocs or empty($rewrittedAssociation)) then (false()) else (
                let $x              :=
                    if ($testmode) then ()
                    else if ($concept) then (
                        update insert $rewrittedAssociation into $concept
                    )
                    else if ($decor/ids/identifierAssociation) then (
                        update insert $rewrittedAssociation preceding $decor/ids/identifierAssociation[1]
                    )
                    else if ($decor/ids/*) then (
                        update insert $rewrittedAssociation following $decor/ids/*[last()]
                    )
                    else (
                        update insert $rewrittedAssociation into $decor/ids
                    )
                return true()
            )
        
        return (
            element {$rewrittedAssociation/name()} {
                $rewrittedAssociation/@*,
                if ($rewrittedAssociation[@refdisplay]) then () else 
                if ($s_ref) then (attribute refdisplay {art:getNameForOID($s_ref, (), ())}) else (),
                <add>{$storeAssoc}</add>
            }
        )
    }
    </result>
};

(:~  Return only valid, non empty attributes on an identifier association as defined in the DECOR format.
:
:   @param $identifierAssociation     - 0..1 identifierAssociation elements
:   @param $allowEmpty                - true|false only relevant for transaction concepts when you need to explicitly exclude a dataset association in a transaction
:   @return the pruned identifierAssociation element for the given project in $decor, or nothing input was empty
:   @since 2017-04-05
:)
declare function art:prepareIdentifierAssociationForUpdate($identifierAssociation as element()?) as element(identifierAssociation)? {
    art:prepareIdentifierAssociationForUpdate($identifierAssociation, false())
};
declare function art:prepareIdentifierAssociationForUpdate($identifierAssociation as element()?, $allowEmpty as xs:boolean) as element(identifierAssociation)? {
    if ($identifierAssociation[@ref[not(.='')]] | $identifierAssociation[$allowEmpty]) then (
        <identifierAssociation>
        {
            $identifierAssociation/@conceptId,
            $identifierAssociation/@conceptFlexibility[not(.='')],
            $identifierAssociation/@ref[not(.='')]
            ,
            if ($identifierAssociation/@effectiveDate[. castable as xs:dateTime]) then (
                $identifierAssociation/@effectiveDate
            )
            else (
                attribute effectiveDate {format-dateTime(current-dateTime(),'[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01]', (), (), ())}
            )
            ,
            $identifierAssociation/@expirationDate[. castable as xs:dateTime], 
            $identifierAssociation/@officialReleaseDate[. castable as xs:dateTime], 
            $identifierAssociation/@versionLabel[not(.='')]
        }
        </identifierAssociation>
    ) else ()
};

(:~  Return live templateAssociations for the project with the given @prefix
:
:   @param $projectPrefix   - required project/@prefix
:   @return all live templateAssociations for the given $projectPrefix or nothing if not found
:   @since 2017-06-30
:)
declare function art:getTemplateAssociations($projectPrefix as xs:string) as element(templateAssociation)* {
    art:getTemplateAssociations($projectPrefix, (), (), (), ())
};
(:~  Return live templateAssociations for the project with the given @prefix or templateAssociations from an archived $decorVersion
:   Released/compiled matches could be present in multiple languages. Use the parameters @language to select the right one.
:
:   @param $projectPrefix           - required project/@prefix
:   @param $decorVersion            - optional decor/@versionDate
:   @param $language                - optional decor/@language. Only relevant with parameter $decorVersion. Empty uses decor/project/@defaultLanguage, '*' selects all versions, e.g. 'en-US' selects the US English version (if available)
:   @param $templateId              - optional @templateId as found on templateAssociation element. Returns templateAssociations only for this template
:   @param $templateEffectiveDate   - optional @effectiveDate as found on templateAssociation element. Returns templateAssociations only for this template version. Requires $templateId
:   @return all live templateAssociations for the given $projectPrefix, or all templateAssociations from an archived $decorVersion or nothing if not found
:   @since 2017-06-30
:)
declare function art:getTemplateAssociations($projectPrefix as xs:string, $decorVersion as xs:string?) as element(templateAssociation)* {
    art:getTemplateAssociations($projectPrefix, $decorVersion, (), (), ())
};
declare function art:getTemplateAssociations($projectPrefix as xs:string, $decorVersion as xs:string?, $language as xs:string?) as element(templateAssociation)* {
    art:getTemplateAssociations($projectPrefix, $decorVersion, $language, (), ())
};
declare function art:getTemplateAssociations($projectPrefix as xs:string, $decorVersion as xs:string?, $language as xs:string?, $templateId as xs:string?, $templateEffectiveDate as xs:string?) as element(templateAssociation)* {
    art:getTemplateAssociationsP(art:getDecorByPrefix($projectPrefix, $decorVersion, $language), $templateId, $templateEffectiveDate)
};
(:~  Return templateAssociations for the project in parameter $decor
:
:   @param $decor           - optional decor project
:   @return all templateAssociations for the given project in $decor, or nothing if not found
:   @since 2017-06-30
:)
declare function art:getTemplateAssociationsP($decor as element(decor)?) as element(templateAssociation)* {
    art:getTemplateAssociationsP($decor, (), ())
};
(:~  Return templateAssociations for the project in parameter $decor
:
:   @param $decor                   - optional decor project
:   @param $templateId              - optional @templateId as found on templateAssociation element. Returns templateAssociations only for this template
:   @param $templateEffectiveDate   - optional @effectiveDate as found on templateAssociation element. Returns templateAssociations only for this template version. Requires $templateId
:   @return all templateAssociations for the given project in $decor, or nothing if not found
:   @since 2017-06-30
:)
declare function art:getTemplateAssociationsP($decor as element(decor)?, $templateId as xs:string?, $templateEffectiveDate as xs:string?) as element(templateAssociation)* {
    if (string-length($templateId) = 0) then 
        $decor/rules/templateAssociation
    else 
    if ($templateEffectiveDate castable as xs:dateTime) then
        $decor/rules/templateAssociation[@templateId = $templateId][@effectiveDate = $templateEffectiveDate]
    else (
        $decor/rules/templateAssociation[@templateId = $templateId]
    )
};

(:~ Returns all namespaces in scope of a DECOR project in Schematron convention. 
:   Either @param $projectId or @param $projectPrefix is required. $projectId takes precedence over $projectPrefix.
:   <ns uri="xx" prefix="yy"/>
:   
:   There are two default elements that will always be returned:
:   <ns uri="urn:hl7-org:v3" prefix="hl7"/>
:   <ns uri="urn:hl7-org:v3" prefix="cda"/>
:   
:   @param $projectId       - optional. Shall match decor/project/@id
:   @param $projectPrefix   - optional. Shall match decor/project/@prefix
:   @return zero or more ns elements
:   @since 2015-03-19
:)
declare function art:getDecorNamespaces($projectId as xs:string?, $projectPrefix as xs:string?) as element(ns)* {
    let $decor          :=
        if ($projectId) then 
            $get:colDecorData//project[@id=$projectId]/ancestor::decor
        else (
            $get:colDecorData//project[@prefix=$projectPrefix]/ancestor::decor
        )
    
    return
        art:getDecorNamespaces($decor[1])
};
declare function art:getDecorNamespaces($decor as element(decor)?) as element(ns)* {
    let $reservedPrefixes   := ('xml','xsi','hl7','cda')

    return
    if ($decor) then (
        
        let $ns-default     := 
            if ($decor/project/defaultElementNamespace[string-length(@ns)>0]) then
                replace($decor/project/defaultElementNamespace/@ns,':.*','')
            else ('hl7')
        
        let $ns-set         :=
            if ($decor) then (
                for $ns-prefix at $i in in-scope-prefixes($decor)[not(.=$reservedPrefixes)]
                let $ns-uri := namespace-uri-for-prefix($ns-prefix, $decor)
                return
                    <ns uri="{$ns-uri}" prefix="{$ns-prefix}" default="{$ns-prefix=$ns-default}"/>
            ) else ()
        let $ns-xml         := <ns uri="http://www.w3.org/XML/1998/namespace" prefix="xml" default="{$ns-default='xml'}" readonly="true"/>
        let $ns-hl7         := <ns uri="urn:hl7-org:v3" prefix="hl7" default="{$ns-default='hl7'}" readonly="true"/>
        let $ns-cda         := <ns uri="urn:hl7-org:v3" prefix="cda" default="{$ns-default='cda'}" readonly="true"/>
        let $ns-xsi         := 
            if (in-scope-prefixes($decor)[. = 'xsi']) then
                <ns uri="http://www.w3.org/2001/XMLSchema-instance" prefix="xsi" default="{$ns-default='xsi'}" readonly="true"/>
             else ()
        
        return
            ($ns-set | $ns-xml | $ns-hl7 | $ns-cda | $ns-xsi)
        
    ) else ()
    
};

(:~ Updates the current DECOR namespace declarations with the given set. The given set is assumed to be the 
:   full set you want to have on the project. The default namespace declarations for hl7 and cda are never updated or deleted
:   The set should have zero or more elements that look like:
:   <ns uri="xx" prefix="yy"/>
:   
:   @param $projectId       - required. Shall match decor/project/@id
:   @param $namespace-nodes - optional. Zero nodes will delete every declaration except xml, hl7 and cda
:   @return nothing
:   @since 2015-03-19
:)
declare function art:setDecorNamespaces($projectId as xs:string, $namespace-nodes as element(ns)*) {
    let $reservedPrefixes   := ('xml','xsi','hl7','cda')
    let $currentDecor       := $get:colDecorData//project[@id=$projectId]/ancestor::decor
    let $ns-set             := $namespace-nodes[string-length(@uri) gt 0][string-length(@prefix) gt 0][not(@prefix = $reservedPrefixes)]
    
    let $delete             := update delete $currentDecor/@*[matches(local-name(),'^dummy-\d*')]
    
    let $add                :=
        for $ns at $i in $ns-set
        let $ns-uri         := $ns/@uri
        let $ns-prefix      := $ns/@prefix
        order by lower-case($ns/@prefix)
        return
            update insert attribute {QName($ns-uri,concat($ns-prefix,':dummy-',$i))} {$ns-uri} into $currentDecor
    
    (: workaround for eXist-db not recognizing namespaces on the root unless you re-save the whole thing :)
    let $coll               := util:collection-name($currentDecor)
    let $res                := util:document-name($currentDecor)
    let $doc                := doc(concat($coll, '/', $res))
    let $store              := xmldb:store($coll, $res, $doc)
    
    return ()
};

(:~ Return the configured server-url http or https for ART-DECOR services or empty string. Example: http://art-decor.org/decor/services/

@return xs:anyURI('http://.../services/')
@since 2014-03-27
:)
declare function art:getServerURLServices() as xs:string? {
    doc($get:strServerInfo)/server-info/url-art-decor-services/string()
};


(: **** DECOR.xsd functions **** :)

(: get Decor Types from DECOR.xsd:)
declare function art:getLabelsAndHints($type as element()) as element()* {
let $art-languages  := art:getArtLanguages()
let $labels         := $type/xs:annotation/xs:appinfo/xforms:label
let $hints          := $type/xs:annotation/xs:appinfo/xforms:hint
let $hints          := if ($hints) then $hints else $type/xs:annotation/xs:documentation
return (
    for $label in $labels
    return
    <label language="{$label/@xml:lang}">{$label/text()}</label>
    ,
    (:add any missing language as copy from en-US:)
    if ($labels) then (
        for $lang in $art-languages[not(. = $labels/@xml:lang)]
        return
        <label language="{$lang}">{$labels[@xml:lang = 'en-US']/text()}</label>
    ) else ()
    ,
    for $hint in $hints
    return
    <hint language="{$hint/@xml:lang}">{$hint/text()}</hint>
    ,
    (:add any missing language as copy from en-US:)
    if ($hints) then (
        for $lang in $art-languages[not(. = $hints/@xml:lang)]
        return
        <hint language="{$lang}">{$hints[@xml:lang = 'en-US']/text()}</hint>
    ) else ()
)
};

(:~ Called from all DECOR oriented forms :)
declare function art:getDecorTypes() as element()* {
    art:getDecorTypes(false())
};

(:~ Called from DECOR-core post-install.xql with parameter true() to recreate the normalized xml file from a potentially updated DECOR.xsd file :)
declare function art:getDecorTypes($recreate as xs:boolean) as element()* {
    let $decorTypes     := 
        if ($recreate) then () else if (doc-available($get:strDecorTypes)) then doc($get:strDecorTypes)/list[@artifact = 'TYPES'] else ()
    
    return
    if ($recreate or empty($decorTypes)) then (
        let $types      :=
            (:<xs:simpleType name="DecorAndOtherObjectFormats">
                <xs:union memberTypes="DecorObjectFormat FhirObjectFormat NonEmptyString"/>
              </xs:simpleType>:)
            for $simpleType in $get:docDecorSchema/xs:schema/xs:simpleType[xs:union] | $get:docDecorBasicSchema/xs:schema/xs:simpleType[xs:union]
            let $memberTypes    := $get:docDecorSchema/xs:schema/xs:simpleType[@name = $simpleType/xs:union/tokenize(@memberTypes, '\s')] |
                                   $get:docDecorBasicSchema/xs:schema/xs:simpleType[@name = $simpleType/xs:union/tokenize(@memberTypes, '\s')]
            return
                <xs:simpleType>
                {
                    $simpleType/@name,
                    $simpleType/xs:annotation
                }
                {
                    <xs:restriction base="xs:string">
                    {
                        $memberTypes//xs:enumeration
                    }
                    </xs:restriction>
                }
                </xs:simpleType>
            

        let $types      := ($types|
                            $get:docDecorSchema/xs:schema/xs:element|
                            $get:docDecorSchema/xs:schema/xs:complexType|
                            $get:docDecorSchema/xs:schema/xs:simpleType[.//xs:enumeration]|
                            $get:docDecorBasicSchema/xs:schema/xs:element|
                            $get:docDecorBasicSchema/xs:schema/xs:complexType|
                            $get:docDecorBasicSchema/xs:schema/xs:simpleType[.//xs:enumeration])
        let $decorTypes := 
            <list artifact="TYPES" current="{count($types)}" total="{count($types)}" all="{count($types)}" xmlns:json="http://www.json.org">
            {
                for $type in $types
                return
                    switch ($type/@name)
                    case 'InstalledFhirEndpoints' return (
                        art:addJsonArrayToElements(
                            element {$type/@name} {
                                art:getLabelsAndHints($type)
                                ,
                                for $endpoint in adserver:getInstalledFhirServices()
                                let $label      :=
                                    switch ($endpoint)
                                    case '1.0'
                                    case 'dstu2' return 'DSTU2'
                                    case '3.0'
                                    case 'stu3' return 'STU3'
                                    default return (
                                        if (matches($endpoint, '^\d+\.\d+(\.\d+)?$')) then
                                            concat('R', string-join(tokenize($endpoint, '\.')[position() le 2], '.'))
                                        else (
                                            $endpoint
                                        )
                                    )
                                order by $endpoint
                                return 
                                    <enumeration name="{$endpoint}">
                                    {
                                        art:getLabelsAndHints(
                                            <xs:enumeration value="{$endpoint}">
                                                <xs:annotation>
                                                    <xs:appinfo>
                                                        <xforms:label xml:lang="en-US">{data($label)}</xforms:label>
                                                    </xs:appinfo>
                                                </xs:annotation>
                                            </xs:enumeration>
                                        )
                                    }
                                    </enumeration>
                            }
                        )
                    )
                    case 'InstalledMenuTemplates' return (
                        art:addJsonArrayToElements(
                            element {$type/@name} {
                                art:getLabelsAndHints($type)
                                ,
                                for $xls in adserver:getServerMenuTemplates()
                                order by $xls
                                return 
                                    <enumeration name="{$xls}">
                                    {
                                        art:getLabelsAndHints(
                                            <xs:enumeration value="{$xls}">
                                                <xs:annotation>
                                                    <xs:appinfo>
                                                        <xforms:label xml:lang="en-US">{$xls}</xforms:label>
                                                    </xs:appinfo>
                                                </xs:annotation>
                                            </xs:enumeration>
                                        )
                                    }
                                    </enumeration>
                            }
                        )
                    )
                    default return (
                        art:addJsonArrayToElements(
                            element {$type/@name} {
                                art:getLabelsAndHints($type),
                                for $element in $type//xs:element
                                return
                                <element>
                                {
                                    $element/@name | $element/@ref,
                                    art:getLabelsAndHints($element)
                                }
                                </element>
                                ,
                                for $attribute in $type//xs:attribute
                                return
                                <attribute>
                                {
                                    $attribute/@name | $attribute/@ref,
                                    art:getLabelsAndHints($attribute)
                                }
                                </attribute>
                                ,
                                for $enumeration in $type//xs:enumeration
                                return
                                <enumeration>
                                {
                                    $enumeration/@value,
                                    art:getLabelsAndHints($enumeration)
                                }
                                </enumeration>
                            }
                        )
                    )
            }
            </list>
        
        (:don't store if we cannot write:)
        let $f          := tokenize($get:strDecorTypes,'/')[last()]
        let $upd        := 
            if (doc-available($get:strDecorTypes)) then
                if (sm:has-access(xs:anyURI(concat($get:strArtData, '/', $f)),'w')) then (
                    sm:chgrp(xs:anyURI(xmldb:store($get:strArtData, $f, $decorTypes)), 'decor')
                )
                else ()
            else if (sm:has-access(xs:anyURI($get:strArtData),'w')) then
                sm:chgrp(xs:anyURI(xmldb:store($get:strArtData, $f, $decorTypes)), 'decor')
            else ()
        
        return $decorTypes
    ) else (
        $decorTypes
    )
};

(: **** ART functions **** :)

(: scans the database for artXformResources and returns a list of package roots with package title and abbreviation:)
declare function art:getPackageList() as element(packageRoots) {
let $resourcesList := collection($get:root)//artXformResources

return
    <packageRoots>
    {
        for $resources in $resourcesList
        let $packageFullRoot := substring-before(util:collection-name($resources),'/resources')
        let $packageRoot     := substring-after($packageFullRoot,$get:root)
        let $packageInfo     := doc(concat($packageFullRoot,'/expath-pkg.xml'))
        order by lower-case($packageInfo//expath:title)
        return
            if (empty($packageInfo) or empty($packageRoot)) then () else ( 
                <root abbrev="{$packageInfo/expath:package/@abbrev}" title="{$packageInfo//expath:title}">{$packageRoot}</root>
            )
    }
    </packageRoots>
};

(:  Return artXformResources from the requested package by calling "package"/resources/form-resources.xml
:   Contents are sorted alphabetically
:
:   <artXformResources packageRoot="$package">
:       <resources xml:lang="en-US" displayName="English (en-US)">
:           <key>value</key>
:           ...
:       </resources>
:       <resources xml:lang="nl-NL" displayName="Nederlands (nl-NL)">
:           <key>value</key>
:           ...
:       </resources>
:       <resources xml:lang="de-DE" displayName="Deutsch (de-DE)">
:           <key>value</key>
:           ...
:       </resources>
:   </artXformResources>
:)
declare function art:getFormResources($packageRoot as xs:string?) as element(artXformResources) {
    art:getLanguageResources($packageRoot, true())
};

(:  Return local or default artXformResources from the requested package by calling "package"/resources/form-resources.xml
:   Contents are sorted alphabetically
:
:   <artXformResources packageRoot="$package">
:       <resources xml:lang="en-US" displayName="English (en-US)">
:           <key>value</key>
:           ...
:       </resources>
:       <resources xml:lang="nl-NL" displayName="Nederlands (nl-NL)">
:           <key>value</key>
:           ...
:       </resources>
:       <resources xml:lang="de-DE" displayName="Deutsch (de-DE)">
:           <key>value</key>
:           ...
:       </resources>
:   </artXformResources>
:   
:   Needed a different name because xquery function signatures work based on param count, not param type
:)
declare function art:getLanguageResources($packageRoot as xs:string?, $local as xs:boolean) as element(artXformResources) {
    art:getFormResourcesLocal($packageRoot, $local)
};

(:  Return artXformResources from the requested package by calling "package"/resources/form-resources.xml
:   and in the requested language. Contents are sorted alphabetically
:   If the parameter language is empty, then try to get from the user settings and if all else fails go 
:   to server setting.
:   Other languages in the form-resources.xml are returned empty to signal that they are available.
:
:   <artXformResources packageRoot="$package">
:       <resources xml:lang="en-US" displayName="English (en-US)">
:           <key>value</key>
:           ...
:       </resources>
:       <resources xml:lang="nl-NL" displayName="Nederlands (nl-NL)"/>
:       <resources xml:lang="de-DE" displayName="Deutsch (de-DE)"/>
:   </artXformResources>
:)
declare function art:getFormResources($packageRoot as xs:string?, $language as xs:string?) as element(artXformResources) {
let $formResources      := art:getFormResourcesLocal($packageRoot)

(:If the user is not logged in, the user is guest:)
(:Not all packages may carry the same language. We always want to return A language. Checked in order:
:   1. The parameter $langugae, 
:   2. User preference,
:   3. Server preference
:   4. en-US
:   5. whatever is the first available language in the package
:)
let $userLang       := aduser:getUserLanguage()
let $servLang       := $get:strArtLanguage
(:let $language       := if (empty($language)) then (aduser:getUserLanguage()) else ($language):)

let $resources      := $formResources/resources[@xml:lang = $language]
let $resources      := if ($resources) then $resources else $formResources/resources[@xml:lang = $userLang]
let $resources      := if ($resources) then $resources else $formResources/resources[@xml:lang = $servLang]
let $resources      := if ($resources) then $resources else $formResources/resources[@xml:lang = 'en-US']
let $resources      := if ($resources) then $resources else $formResources/resources[1]

let $calcLang       := $resources[1]/@xml:lang

return
    (:<artXformResources packageRoot="{$packageRoot}" inputLang="{$language}" userLang="{$userLang}" servLang="{$servLang}" calcLang="{$calcLang}">:)
    <artXformResources packageRoot="{$packageRoot}">
    {
        (:make requested language the first:)
        $resources
    }
    {
        (:and add any other available languages as empty elements. ART can then make the user choose from available languages:)
        for $otherresources in $formResources/resources[not(@xml:lang = $calcLang)]
        return
            <resources xml:lang="{$otherresources/@xml:lang}" displayName="{$otherresources/@displayName}"/>
    }
    </artXformResources>
};

(:  Return form key/value pairs from the requested package by calling "package"/resources/form-resources.xml
:   and in the requested language(s).
:   If the parameter language is empty, then try to get from the user settings and if all else fails go 
:   to server setting.
:
:       <key xml:lang="en-US">value</key>
:       <key xml:lang="nl-NL">value</key>
:)
declare function art:getFormResourcesKey($packageRoot as xs:string?, $language as xs:string*, $key as xs:string) as element()* {
(:getUserLanguage gets user language if defined, or falls back onto server language. If the user is not logged in, the user is guest:)
let $language           := if (empty($language)) then (aduser:getUserLanguage()) else ($language)
let $formResources      := art:getFormResourcesLocal($packageRoot)/resources[@xml:lang=$language]

for $element in util:eval(concat('$formResources/',$key))
return
    element {$key} {$element/../@xml:lang, $element/node()}
};

(:  Add $newResources to an existing package (normally only called during package development). Keys are added 
:   alphabetically in both the local package AND the base package. The base package may be used to sync back to
:   code base which is much easier than the local package that has a different structure.
:
:   <undefinedResources packageRoot="art">
:       <resource key="key">
:           <text xml:lang="en-US" displayName="English (en-US)">value</text>
:           <text xml:lang="nl-NL" displayName="Nederlands (nl-NL)">value</text>
:           <text xml:lang="de-DE" displayName="Deutsch (de-DE)">value</text>
:           <text xml:lang="pl-PL" displayName="Polskie (pl-PL)">value</text>
:       </resource>
:   </undefinedResources>
:)
declare function art:addFormResources($packageRoot as xs:string, $newResources as element(undefinedResources)) {
let $baseFormResources      := art:getFormResourcesLocal($packageRoot, false())
let $localFormResources     := art:getFormResourcesLocal($packageRoot)

let $baseEditedResources   := 
    <artXformResources>
    {
        $baseFormResources/@*
        ,
        for $resources in $baseFormResources/resources
        let $lang := $resources/@xml:lang
        return
            <resources>
            {
                $resources/@*
                ,
                for $text in ($resources/*|$newResources/resource)
                let $sortkey := if ($text/@key) then $text/@key else $text/name()
                let $content := if ($text/@key) then $text/text[@xml:lang=$lang]/node() else $text/node()
                order by lower-case($sortkey)
                return
                    element {$sortkey} {$content}
            }
            </resources>
    }
    </artXformResources>
let $localEditedResources   := 
    <artXformResources>
    {
        $localFormResources/@*
        ,
        for $resources in $localFormResources/resources
        let $lang := $resources/@xml:lang
        return
            <resources>
            {
                $resources/@*
                ,
                for $text in ($resources/*|$newResources/resource)
                let $sortkey := if ($text/@key) then $text/@key else $text/name()
                let $content := if ($text/@key) then $text/text[@xml:lang=$lang]/node() else $text/node()
                order by lower-case($sortkey)
                return
                    element {$sortkey} {$content}
            }
            </resources>
    }
    </artXformResources>

let $update                 := art:saveFormResourcesLocal($packageRoot, $localEditedResources)
let $update                 := art:saveFormResourcesBase($packageRoot, $baseEditedResources)
return ()
};

(:  Save $newResources as-is and update the ART menu file with potentially updated translations. Only keys are sorted alphabetically.
:   Add updates from stuff to save into art-data/form-resources.xml so we know what was done on this server 
:   and so we can restore that in new installs of the package
:)
declare function art:saveFormResources($packageRoot as xs:string, $newResources as element(artXformResources)) {
    art:saveFormResources($packageRoot, $newResources, true())
};
(:  Save $newResources as-is and optionally update the ART menu file with potentially updated translations.:)
declare function art:saveFormResources($packageRoot as xs:string, $newResources as element(artXformResources), $doArtMenuUpdate as xs:boolean) {
let $formResources          := art:getFormResourcesLocal($packageRoot)
let $editedResources        := 
    <artXformResources packageRoot="{$packageRoot}">
    {
        $formResources/(@* except @packageRoot)
        ,
        for $resources in $newResources/resources
        return
            <resources>
            {
                $resources/@*
                ,
                for $key in $resources/*
                order by lower-case(name($key))
                return $key
            }
            </resources>
    }
    </artXformResources>

let $update                 := art:saveFormResourcesLocal($packageRoot, $editedResources)
(:The ART menu depends on the values in the ART package, so update the menu in case any of the relevant strings changed :)
let $update                 := if ($doArtMenuUpdate and $packageRoot='art') then (art:updateArtMenu(())) else ()

return ()
};

(:  Returns result of merging local content with corresponding package contents. Normally called during installation 
:   of a new version of a package that may have new keys which would otherwise be missed.
:   It does not do the save itself. Please refer to art:saveFormResourcesLocal
:
:   Expected format for $newResources:
:
:   <artXformResources packageRoot="{$packageRoot}">
:       <resources xml:lang="en-US" displayName="English (en-US)">
:           <key>value</key>
:           <key update="">value</key>
:       </resources>
:   </artXformResources>
:
:   The local file in art-data is updated whenever new versions of the packages are installed. The way the update works is:
:   - Add any new language from the new package version into the local copy
:   - Add any new <key/> from the new package version into the local copy
:   - Overwrite any <key/> in the local copy with the corresponding <key/> from the new package version unless the 
:     local <key/> carries the attribute @updated indicating that this <key/> was explicitly updated on this server.
:)
declare function art:mergeLocalLanguageUpdates($packageRoot as xs:string) as element(artXformResources) {
let $formResources          := doc(concat($get:root,$packageRoot,'/resources/form-resources.xml'))/artXformResources
(:purposefully not the same element name as in packages (artXformResources):)
let $localResources         := collection($get:strArtData)//artXformResources[@packageRoot=$packageRoot]

let $newResources           :=
    <artXformResources packageRoot="{$packageRoot}">
    {
        $formResources/(@* except (@packageRoot|@xsi:noNamespaceSchemaLocation))
        ,
        if (empty($formResources)) then
            $localResources/resources
        else if (empty($localResources)) then
            $formResources/resources
        else (
            (:merge common languages - if nothing is locally updated, then just copy from install:)
            for $resource in $formResources/resources
            let $localResource      := $localResources/resources[@xml:lang=$resource/@xml:lang]
            return
                if ($localResource/*[@updated]) then (
                    <resources>
                    {
                        $resource/@*
                        ,
                        for $packageKey in $resource/*
                        let $localKey   := $localResource/*[@updated][name()=$packageKey/name()]
                        return 
                            if ($localKey) then $localKey else $packageKey
                    }
                    </resources>
                ) else ($resource)
            ,
            (:copy new languages, but only keys that exist in the original package:)
            for $resource in $localResources/resources[not(@xml:lang=$formResources/resources/@xml:lang)]
            let $formResource       := $formResources/resources[1]
            return
                <resources>
                {
                    $resource/@*
                    ,
                    for $localKey in $resource/*
                    let $packageKey := $formResource/*[name()=$localKey/name()]
                    return 
                        if ($packageKey) then $localKey else ()
                }
                </resources>
        )
    }
    </artXformResources>

return
    $newResources
};

(: Get a the server configured menu file from $adserver:strServerMenuPath or get the supplied menu file. Root element is expected to be <menu/>
:  When you call this function, you should have all localizations readily present. If not, use art:saveArtMenuForDisplay($menufile) to fix.
:
:   @param $menufile The menufile to retrieve, or the configured server menu file if empty
:   @return The menu file as-is or error() if menu file could not be retrieved
:   @since 2015-12-21
:)
declare function art:getArtMenu($menufile as xs:string?) as element(menu)? {
    let $menufile           := if (string-length($menufile)>0) then $menufile else adserver:getServerMenuTemplate()
    let $menufilepath       := concat($adserver:strServerMenuPath,'/',$menufile)
    
    return 
        doc(xs:anyURI($menufilepath))/menu
};
(:  Update an existing server configured menu file from $adserver:strServerMenuPath or the supplied menu file so it contains all localizations 
:   as known in the ART form-resources. Root element is expected to be <menu/>
:   All elements containing @label will be lookup up in the ART form resources for localizations
:
:   @param $menufile The menufile to retrieve, or the configured server menu file if empty
:   @return nothing
:   @since 2015-12-21
:)
declare function art:updateArtMenu($menufile as xs:string?) {
    let $menu               := art:getArtMenu($menufile)
    let $menufile           := util:document-name($menu)
    let $preparedMenu       := art:prepareArtMenuForDisplay($menu)
    return
        xmldb:store($adserver:strServerMenuPath, $menufile, $preparedMenu)
};
(:  Prepare an existing server configured menu file from $adserver:strServerMenuPath or the supplied menu file so it contains all localizations 
:   as known in the ART form-resources. Root element is expected to be <menu/>
:   All elements containing @label will be lookup up in the ART form resources for localizations
:
:   @param $menu The pre-existing menufile (use art:getArtMenu($menufile))
:   @return The prepared menu file
:   @since 2015-12-21
:)
declare %private function art:prepareArtMenuForDisplay($menu as element(menu)) as element(menu) {
    let $menufile           := util:document-name($menu)
    let $language-resources := art:getFormResources('art')/resources
    
    return 
        <menu f="{$menufile}">
        {
            for $node in $menu/node()
            return art:handleMenuPart($node, $language-resources)
        }
        </menu>
};
(:  Recurse through all nodes. Nodes with @label are looped into. Other nodes (comment()|text() are copied as-is.
:   The @label attribute is used as key in in the ART form-resources to get the localization texts.
:
:   @param $node The node to process
:   @param $language-resources The form-resources to look up localizations
:   @return The $node as-is or including localizations
:   @since 2015-12-21
:)
declare %private function art:handleMenuPart($node as item(), $language-resources as element()*) as item() {
    if ($node instance of element()) then (
        if ($node[@label]) then (
            element {$node/name()} {
                $node/@*,
                (:get core languages:)
                for $name in util:eval(concat('$language-resources/',$node/@label))
                return
                    <name language="{$name/../@xml:lang}">{$name/node()}</name>
                ,
                for $subnode in $node/node()
                return
                    art:handleMenuPart($subnode, $language-resources)
            }
        ) else (
            $node
        )
    ) else (
        $node
    )
};

(:  Get form-resources for a package
:   - Check art-data/form-resources.xml and return if available
:   - Fallback to <package>/resources/form-resources.xml
:)
declare %private function art:getFormResourcesLocal($packageRoot as xs:string?) as element(artXformResources)? {
    art:getFormResourcesLocal($packageRoot, true())
};
declare %private function art:getFormResourcesLocal($packageRoot as xs:string?, $local as xs:boolean) as element(artXformResources)? {
let $packageRoot            := if (empty($packageRoot)) then 'art' else $packageRoot
return
    if ($local) then 
        collection($get:strArtData)//artXformResources[@packageRoot=$packageRoot]
    else (
        collection(concat($get:root,$packageRoot,'/resources'))//artXformResources
    )
};

(:  Writes package contents in base package. If the resource doesn't exist yet it will be created.
:   (Over)writes a full artXformResources element with the contents of parameter $newResources
:   There is no content checking other than if there is data at all so you're supposed to have done that before here
:   You MUST be dba for this function.
:)
declare %private function art:saveFormResourcesBase($packageRoot as xs:string?, $newResources as element(artXformResources)) {
let $baseResourcePath       := concat($get:root,$packageRoot,'/resources')
let $baseResourceFile       := 'form-resources.xml'
let $baseResourceFileFull   := concat($baseResourcePath,'/',$baseResourceFile)
let $baseResources          := <artXformResources xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="form-resources.xsd"/>
    
let $check                  := 
    if (sm:is-dba(get:strCurrentUserName()) = false()) then (
        error(QName('http://art-decor.org/ns/error', 'NoPermission'), 'Only a dba user can change language resources')
    )
    else if (empty($newResources)) then (
        error(QName('http://art-decor.org/ns/error', 'NoData'), 'No data to save')
    )
    else if (doc-available($baseResourceFileFull)) then (
    ) else (
        xmldb:store($baseResourcePath,$baseResourceFile,$baseResources)
    )

let $baseResources          := collection($baseResourcePath)//artXformResources
let $update                 := update value $baseResources with $newResources/*

return ()
};

(:  Writes package contents in art-data. If the resource doesn't exist yet it will be created.
:   (Over)writes a full artXformResources element with the contents of parameter $newResources
:   There is no content checking other than if there is data at all so you're supposed to have done that before here
:   You MUST be dba for this function.
:)
declare %private function art:saveFormResourcesLocal($packageRoot as xs:string?, $newResources as element(artXformResources)) {
let $localResourcePath      := concat($get:strArtData,'/resources')
let $localResourceFile      := 'form-resources.xml'
let $localResourceFileFull  := concat($localResourcePath,'/',$localResourceFile)
let $localResources         := <artXformResourcesBundle xsi:noNamespaceSchemaLocation="../../art/resources/form-resources.xsd"/>
    
let $check                  := 
    if (sm:is-dba(get:strCurrentUserName()) = false()) then (
        error(QName('http://art-decor.org/ns/error', 'NoPermission'), 'Only a dba user can change language resources')
    )
    else if (empty($newResources)) then (
        error(QName('http://art-decor.org/ns/error', 'NoData'), 'No data to save')
    )
    else if (doc-available($localResourceFileFull)) then (
    ) else (
        xmldb:store($localResourcePath,$localResourceFile,$localResources)
    )

let $localResources         := collection($localResourcePath)//artXformResourcesBundle
let $update                 :=
    if ($localResources/artXformResources[@packageRoot=$packageRoot]) then
        update value $localResources/artXformResources[@packageRoot=$packageRoot] with $newResources/*
    else (
        update insert $newResources into $localResources
    )

return ()
};

(:  Return language supported in ART by returning all @xml:lang attributes in the art/resources/form-resources.xml
:   Example: ('en-US','nl-NL','de-DE')
:
:   Note: using art:getFormResourcesLocal('art') would have made sense but this kills xforms (http 400) without clear reason
:         for now copy logic from there. Weird. Fixme.
:)
declare function art:getArtLanguages() as xs:string+ {
let $packageRoot            := 'art'
let $localResources         := collection($get:strArtData)//artXformResources[@packageRoot = $packageRoot]
return 
    if ($localResources) then (
        $localResources/resources/@xml:lang
    )
    else (
        let $packageResources   := doc(concat($get:root,$packageRoot,'/resources/form-resources.xml'))/artXformResources
        return (
            $packageResources/resources/@xml:lang
        )
    )
};

(:~ Parse string into xs:dateTime. String may be a (partial) ISO 8601. Year is required. Missing month will be 01 for January. Missing day will 
    be 01. Missing hours, minutes or seconds will be 00. String may also be a relative statement T((([-+]nn(.nn)[YMDhms])({hh:mm:ss})) where 
    T is current-dateTime(). You may deduct nn(.nn) from or add nn(.nn) to T where nn(.nn) is a number and [YMDhms] determines if those are 
    years, months, days, hours, minutes, or seconds. Finally you may give the result a fixed time string.
    
    @param $variablevaguedatetime - optional. String to parse into an xs:dateTime
    @return a valid xs:dateTime or nothing

:)
declare function art:parseAsDateTime($variablevaguedatetime as xs:string?) as xs:dateTime? {
    if ($variablevaguedatetime castable as xs:dateTime) then
        xs:dateTime($variablevaguedatetime)
    else
    if ($variablevaguedatetime castable as xs:date) then
        xs:dateTime(concat($variablevaguedatetime, 'T00:00:00'))
    else
    (: variable logic :)
    if (starts-with($variablevaguedatetime, 'T')) then (
        let $nowdate            := current-date()
        let $nowdatetime        := current-dateTime()
        let $plusminus          := substring($variablevaguedatetime, 2, 1)
        let $plusminus          := if ($plusminus = '+' or $plusminus = '-') then $plusminus else ()
        let $plusminusstring    := 
            if (empty($plusminus)) then () else 
            if (matches($variablevaguedatetime, '^T.(\d+(\.\d+)?[YMDhms]).*')) then replace($variablevaguedatetime, '^T.(\d+(\.\d+)?[YMDhms]).*', '$1') 
            else ()
        let $plusminusnumber    := replace($plusminusstring, '.$', '')[string-length() gt 0]
        let $plusminustype      := replace($plusminusstring, '.+(.)$', '$1')[string-length() gt 0]
        let $fixedtime          := 
            if (matches($variablevaguedatetime, 'T.*\{\d{2}:\d{2}:\d{2}\}.*')) then
                substring-before(substring-after($variablevaguedatetime, '{'), '}')
            else ()
        
        return
        if (empty($plusminus) and empty($fixedtime)) then 
            $nowdatetime
        else
        if (empty($plusminus)) then
            if ($fixedtime castable as xs:time) then xs:dateTime(concat(string($nowdate), 'T', $fixedtime)) else ()
        else (
            let $duration       := 
                switch  ($plusminustype)
                case 'Y' return xs:yearMonthDuration(concat('P', $plusminusnumber, $plusminustype))
                case 'M' return xs:yearMonthDuration(concat('P', $plusminusnumber, $plusminustype))
                case 'D' return xs:dayTimeDuration(concat('P', $plusminusnumber, $plusminustype))
                case 'h' return xs:dayTimeDuration(concat('PT', $plusminusnumber, upper-case($plusminustype)))
                case 'm' return xs:dayTimeDuration(concat('PT', $plusminusnumber, upper-case($plusminustype)))
                case 's' return xs:dayTimeDuration(concat('PT', $plusminusnumber, upper-case($plusminustype)))
                default return ()
            let $calcdatetime   := 
                switch  ($plusminus)
                case '-' return $nowdatetime - $duration
                case '+' return $nowdatetime + $duration
                default return $nowdatetime
            
            return
                if (empty($fixedtime)) then $calcdatetime else 
                if ($fixedtime castable as xs:time) then xs:dateTime(concat(substring-before($nowdatetime, 'T'), 'T', $fixedtime))
                else ()
            )
    )
    (: partial dateTime logic :)
    else (
        let $year       := substring($variablevaguedatetime, 1, 4)
        let $month      := substring($variablevaguedatetime, 5, 2)
        let $month      := if ($month castable as xs:integer) then $month else '01'
        let $day        := substring($variablevaguedatetime, 7, 2)
        let $day        := if ($day castable as xs:integer) then $day else '01'
        let $hour       := substring($variablevaguedatetime, 10, 2)
        let $hour       := if ($hour castable as xs:integer) then $hour else '00'
        let $minute     := substring($variablevaguedatetime, 12, 2)
        let $minute     := if ($minute castable as xs:integer) then $minute else '00'
        let $second     := substring($variablevaguedatetime, 14, 2)
        let $second     := if ($second castable as xs:integer) then $second else '00'
        
        let $dateTime   := concat($year, '-', $month, '-', $day, 'T', $hour, ':', $minute, ':', $second)
        
        return
            if ($dateTime castable as xs:dateTime) then xs:dateTime($dateTime) else ()
    )
};

(:~ Facilitates JSON generation by making sure XML elements are always array. If we don't mark this they "sometimes" might become an object, depending on occurrence in a given instance :) 
declare function art:addJsonArrayToElements($nodes as node()*) {
    for $node in $nodes
    return
    if ($node instance of element()) then
        let $node       := if ($node[@language][*][not(self::decor|self::codeSystem|self::valueSet)] | $node[self::example]) then art:serializeNode($node) else $node
        return
            element {name($node)} { attribute json:array {'true'}, $node/(@* except @json:array), for $child in $node/node() return art:addJsonArrayToElements($child) }
    else
    if ($node instance of text()) then
        if (normalize-space($node) = '') then () else $node
    else (
        $node
    )
};

(: **** LUCENE SEARCHES **** :)
(:~ Returns lucene config xml for a sequence of strings. The search will find yield results that match all terms+trailing wildcard in the sequence
:   Example output:
:   <query>
:       <bool>
:           <wildcard occur="must">term1*</wildcard>
:           <wildcard occur="must">term2*</wildcard>
:       </bool>
:   </query>
:
:   @param $searchTerms required sequence of terms to look for
:   @return lucene config
:   @since 2014-06-06
:)
declare function art:getSimpleLuceneQuery($searchTerms as xs:string+) as element(query) {
    art:getSimpleLuceneQuery($searchTerms, 'wildcard')
};

(:~
:   http://exist-db.org/exist/apps/doc/lucene.xml?q=lucene&field=all&id=D2.2.4.28#D2.2.4.28
:)
declare function art:getSimpleLuceneQuery($searchTerms as xs:string+, $searchType as xs:string) as element(query) {
    <query>
        <bool>
        {
            for $term in $searchTerms
            return
                if ($searchType='fuzzy') then
                    <fuzzy occur="must">{concat($term,'~')}</fuzzy>
                
                else if ($searchType='fuzzy-not') then
                    <fuzzy occur="not">{concat($term,'~')}</fuzzy>
                    
                else if ($searchType='regex') then
                    <regex occur="must">{$term}</regex>
                    
                else if ($searchType='regex-not') then
                    <regex occur="not">{$term}</regex>
                    
                else if ($searchType='phrase') then
                    <phrase occur="must">{$term}</phrase>
                    
                else if ($searchType='phrase-not') then
                    <phrase occur="not">{$term}</phrase>
                    
                else if ($searchType='term') then
                    <term occur="must">{$term}</term>
                    
                else if ($searchType='term-not') then
                    <term occur="not">{$term}</term>
                    
                else if ($searchType='wildcard') then
                    <wildcard occur="must">{concat($term,'*')}</wildcard>
                    
                else if ($searchType='wildcard-not') then
                    <wildcard occur="not">{concat($term,'*')}</wildcard>
                    
                else ()
        }
        </bool>
    </query>
};

(:~ Returns lucene options xml that instruct filter-rewrite=yes :)
declare function art:getSimpleLuceneOptions() as element(options) {
    art:getSimpleLuceneOptions('and', 0, false(), true())
};
declare function art:getSimpleLuceneOptions($default-operator as xs:string, $phrase-slop as xs:integer, $leading-wildcard as xs:boolean, $filter-rewrite as xs:boolean) as element(options) {
    <options>
        <default-operator>{$default-operator}</default-operator>
        <phrase-slop>{$phrase-slop}</phrase-slop>
        <leading-wildcard>{if ($leading-wildcard) then 'yes' else 'no'}</leading-wildcard>
        <filter-rewrite>{if ($filter-rewrite) then 'yes' else 'no'}</filter-rewrite>
    </options>
};